Interactive code snippets not yet available for SoH 2.0, see our Status of of School of Haskell 2.0 blog post

Joseph Abrahamson's Basic Lensing tutorial digested and extended with action examples and links


intro

source

This is a digest of Joseph Abrahamson's Basic Lensing FPComplete tutorial. I have

  • shown his examples in action

  • added links

  • verified examples via unit testing

see also:

A lens "focuses" on a smaller part of a larger object.

{-# LANGUAGE TemplateHaskell #-}

module UserTelBasicLensing where

import Control.Lens
import Test.HUnit -- for unit testing examples

data Arc      = Arc      { _degree   :: Int, _minute    :: Int, _second :: Int } deriving (Eq, Show)
data Location = Location { _latitude :: Arc, _longitude :: Arc }                 deriving (Eq, Show)

Underscores in record names above are a Control.Lens convention for generating template haskell (TH).

The following is a TH splice. It generates lenses automatically based on record functions in Location:

$(makeLenses ''Location)

Above creates two lenses:

:t latitude
-- latitude  :: Functor f => (Arc -> f Arc) -> Location -> f Location

:t longitude
-- longitude :: Functor f => (Arc -> f Arc) -> Location -> f Location

The type of Lens is

type Lens s t a b = forall f. Functor f => (a -> f b) -> s -> f t
type Lens' s a = Lens s s a a

So the types above can be viewed as:

-- latitude  :: Lens' Location Arc
-- longitude :: Lens' Location Arc

lenses used as getters/setters

A lens is a function that get be used to get (via view) or set (via set) a part of a data structure.

In this example case, longitude (likewise for latitude) can be used to view or set that part of Location :

view

:t view
-- view :: Control.Monad.Reader.Class.MonadReader s m => Getting a s a -> m a

:i Getting
-- type Getting r s a = (a -> Accessor r a) -> s -> Accessor r s

:i Accessor
-- newtype Accessor r a = Accessor {runAccessor :: r}

:t runAccessor
-- runAccessor :: Accessor r a -> r

:t view longitude
-- view longitude :: Control.Monad.Reader.Class.MonadReader Location m => m Arc
--   i.e.,:
-- view longitude :: Location -> Arc

set

:t set
-- set :: ASetter s t a b -> b -> s -> t

:i ASetter
-- type ASetter s t a b = (a -> Mutator b) -> s -> Mutator t

:i Mutator
-- newtype Mutator a = Control.Lens.Internal.Setter.Mutator {Control.Lens.Internal.Setter.runMutator :: a}

:t Control.Lens.Internal.Setter.runMutator
-- Control.Lens.Internal.Setter.runMutator :: Mutator a -> a

:t set longitude
-- set longitude :: Arc -> Location -> Location

The following examples use unit tests (rather than GHCI input/output) to ensure correctness.

t :: (Eq a) => (Show a) => String -> a -> a -> Test
t testName actual expected  = TestCase $ assertEqual testName expected actual

l = Location (Arc 1 2 3) (Arc 4 5 6)

t01 = t "01"
      (view longitude l)
      (Arc 4 5 6)

t02 = t "02"
      (set longitude (Arc 40 50 60) l)
      (Location (Arc 1 2 3) (Arc 40 50 60))

t03 = t "03"
      l
      (Location (Arc 1 2 3) (Arc 4  5  6))

getters/setters without lenses

Lenses are useful because, in immutable Haskell, to change nested fields in a data structure you need to recreate all the objects wrapped around the value that you are changing:

getLongitudeR :: Location -> Arc
getLongitudeR (Location { _longitude = lat }) = lat

setLongitudeR :: Arc -> Location -> Location
setLongitudeR lat loc = loc { _longitude = lat }

t04 = t "04"
      (setLongitudeR (Arc 44 55 66) l)
      (Location (Arc 1 2 3) (Arc 44 55 66))

The lens version does this for you "automatically".


another way to build lenses using lens

:t lens
-- lens :: Functor f => (s -> a) -> (s -> b -> t) -> (a -> f b) -> s -> f t
--   i.e.,:
-- lens :: (c -> a) -> (c -> a -> c) -> Lens' c a

The following are identical:

:t lens getLongitudeR (flip setLongitudeR)
-- lens getLongitudeR (flip setLongitudeR)      :: Functor f => (Arc -> f Arc) -> Location -> f Location

:t lens (view longitude) (flip $ set longitude)
-- lens (view longitude) (flip $ set longitude) :: Functor f => (Arc -> f Arc) -> Location -> f Location

:t longitude
-- longitude                                    :: Functor f => (Arc -> f Arc) -> Location -> f Location

Above shows a law of lenses: for all lenses, l:

l == lens (view l) (flip $ set l)

lens benefits

Benefits of wrapping getters/setters together:

  • export just the lenses instead of the record functions

  • use other kinds of combinators to operate on these lenses for affecting the "focal" record values

E.g., modification via combinator named over:

{-# ANN modifyLongitude "HLint: ignore Redundant bracket" #-}
modifyLongitude  :: (Arc -> Arc) -> (Location -> Location)
modifyLongitude  f = longitude `over` f

arcTimes11 :: Arc -> Arc
arcTimes11 (Arc a b c) = Arc (a*11) (b*11) (c*11)

longitudeTimes11 :: Location -> Location
longitudeTimes11 = modifyLongitude arcTimes11

t05 = t "05"
      (longitudeTimes11 l)
      (Location (Arc 1 2 3) (Arc 44 55 66))

over lifts given function between getter and setter to create a function which modifies a part of the greater whole.


composing lens via (.) to go deeper into structure

$(makeLenses ''Arc)

:t degree
-- degree :: Functor f => (Int -> f Int) -> Arc -> f Arc

:t minute
-- minute :: Functor f => (Int -> f Int) -> Arc -> f Arc

:t second
-- second :: Functor f => (Int -> f Int) -> Arc -> f Arc

Now use (.) to get deeper inside Location:

:t (.)
-- (.) :: (b -> c) -> (a -> b) -> a -> c
--   i.e.,:
-- (.) :: Lens' a b -> Lens' b c -> Lens' a c

:t longitude . degree
-- longitude . degree :: Functor f => (Int -> f Int) -> Location -> f Location
--   i.e.,:
-- longitude . degree :: Lens' Location Int

:t view (longitude . degree)
-- view (longitude . degree) :: Control.Monad.Reader.Class.MonadReader Location m => m Int
--   i.e.,:
-- view (longitude . degree) :: Location -> Int

:t set  (longitude . degree)
-- set  (longitude . degree) :: Int -> Location -> Location

Using the above type signatures as a guide, we can get/set specific parts of Location:

t06 = t "06"
      (view (longitude . degree) l)
      4

t07 = t "07"
      (set  (longitude . degree) 202 l)
      (Location (Arc 1 2 3) (Arc 202 5 6))

t08 = t "08"
      (view (longitude . second) l)
      6

t09 = t "09"
      (set  (longitude . second) 202 l)
      (Location (Arc 1 2 3) (Arc 4 5 202))

combining lenses as pairs or Either

pairs, i.e., (,)

p :: Lens' (Location, Location) (Arc, Arc)
p = latitude `alongside` longitude

l10  = Location (Arc  10  20  30) (Arc  40  50  60)
l100 = Location (Arc 100 200 300) (Arc 400 500 600)

t10 = t "10"
      (view p (l10, l100))
      (Arc 10 20 30, Arc 400 500 600)

t11 = t "11"
      (set p (Arc 111 222 333, Arc 444 555 666) (l10, l100))
      (Location (Arc 111 222 333) (Arc 40 50 60), Location (Arc 100 200 300) (Arc 444 555 666))

Either

ei :: Lens' (Either Arc Arc) Int
ei = choosing degree second

a10  = Arc  10  20  30
a100 = Arc 100 200 300

t12 = t "12"
      (view ei (Left   a10))
      10
t13 = t "13"
      (view ei (Right  a10))
      30
t14 = t "14"
      (view ei (Left  a100))
      100
t15 = t "15"
      (view ei (Right a100))
      300

t16 = t "16"
      (set ei (-1) (Left   a10))
      (Left (Arc (-1) 20 30))
t17 = t "17"
      (set ei (-1) (Right a100))
      (Right (Arc 100 200 (-1)))

summary

lens abstraction

  • idea of holding on to a value that's focused on a smaller part of a larger type

  • algebra for combining (via pairs and eithers, products and coproducts), composing, and modifying these values

  • subsumes record syntax

  • minimizes book-keeping on getters and setters

Lens can do lots more.

Feedback/discussion at: http://haroldcarr.com/posts/2013-10-06-intro-to-haskell-lenses.html


example accuracy

main = runTestTT $ TestList[t01, t02, t03, t04, t05, t06, t07, t08, t09, t10, t11, t12, t13, t14, t15, t16, t17]

main
-- Counts {cases = 17, tried = 17, errors = 0, failures = 0}

Thanks for to Gabriel Gonzalez for useful feedback incorporated before publishing.