Haskell Lecture #10
September 9, 2015
Penultimatum
This is the second-to-last session. Last is on Sept. 23.
I want to focus my energy on building up the Chicago Haskell community. Planning on a 1- or 2-day weekend Haskell class (probably Dec. 5-6).
Last class will be a survey of other topics and include a list of further reading.
Topic of the day: lenses
We often deal with nested records
type Party = [Character]
data Character = Character {
name :: String, klass :: String,
maxHP :: Int, maxMP :: Int, status :: Status}
data Status = Status {currentHP :: Int,
currentMP :: Int}
recall: update syntax
farisa = Character {name = "Farisa",
klass = "Mage",
maxHP = 47,
maxMP = 83,
status = Status {currentHP = 34,
currentMP = 62}}
-- a heal spell
farisa {status = (status farisa) {
currentHP = (min (maxHP farisa) (25 + (currentHP . status $ farisa)),
currentMP = (currentMP . status $ farisa) - 4}
}
Writing a heal spell
farisa {
status = (status farisa) {
currentHP =
(min (maxHP farisa)
(25 + (currentHP .
status $ farisa)),
currentMP =
(currentMP . status $
farisa) - 4}}
farisa.status.mp -= 4
farisa.status.hp += 25
farisa.status.hp =
min(farisa.status.hp,
farisa.maxHP)
“Imperative way” looks cleaner!
Update syntax...
… is not easy to use.
It’s especially when dealing with nested data structures.
What if we could have nice-looking updates in a functional way?
A Lens, conceptually
A Lens captures imperative programming’s often-convenient notion of place.
farisa.status.hp += 25 isn’t about her current hit point value; it’s about the place where the character’s HP is stored.
A place can be get or set.
Purely functional get and set
getA :: s -> a
Not fundamentally different in FP vs. OOP.
setA :: s -> a -> s
setAIntoB :: s -> b -> t (t = s[b/a])
A new object is returned; the old one is not modified. Remember that data sharing makes this performant.
Helper methods...
getHP = currentHP . status
setHP character n =
character {status =
(status character) {currentHP = n}}
Problems with helper methods
Setter/getter pairs
data Place s a =
Place {getter :: s -> a; setter :: s -> a -> s}
currentHPPlace character = Place {
getter = currentHP . status,
setter =
character {status = (status character) {currentHP = n}}
(setter . currentHPPlace) farisa ((getter . currentHPPlace) farisa + 25)
setter/getter pair helper methods...
{-# LANGUAGE RecordWildCards #-}
get' :: Place s a -> s -> a
get' Place{..} = getter
set' :: Place s a -> s -> a -> s
set' Place{..} = setter
modify' :: Place s a -> s -> (a -> a) -> s
modify' place obj f = set' place obj (f (get' place obj))
modify' currentHPPlace farisa (+25)
Three things missing...
type Lens s t a b
type Lens s t a b
A purely functional reference to an a inside an s.
view :: Lens s t a b -> s -> a
set :: Lens s t a b -> s -> b -> t
over :: Lens s t a b -> (a -> b) -> s -> t
_1 on type (a, b)
_1 :: Lens (a, b) (a, b) a a
λ view _1 (1, 2)
1
λ set _1 8 (1, 2)
(8,2)
λ over _1 (+3) (1, 2)
(4,2)
_1 is more general...
_1 :: Lens (a, b) (c, b) a c
λ set _1 "cat" (1, 2)
("cat",2)
_1 :: Lens (a, b, c) (d, b, c) a d
λ set _1 "cat" (5, 7, [Nothing, Just 3])
("cat",7,[Nothing,Just 3])
Actual type sig. of _1
λ :t _1
_1 :: (Functor f, Field1 s t a b) =>
(a -> f b) -> s -> f t
Field1 is a typeclass that allows _1 to cover multiple tuple types. It’s more versatile than fst, which requires a pair.
The use of Functor will make sense when we describe what Lenses actually are.
Lenses compose
λ view (_3 . _2) (8, "dog", (32, 'X'))
'X'
Something weird here. Anyone see it?
Lenses compose
λ view (_3 . _2) (8, "dog", (32, 'X'))
'X'
Something weird here. Anyone see it?
Lenses compose “backward”.
Lens composition
A lens’s “backward” composition makes sense in light of this interpretation (“small to big”) of a Lens:
a Lens s t a b maps an a-to-b transformation at the “focus” into an s-to-t transformation.
_3 . _2 = “Focus on the 3rd element, then focus on the 2nd element of that.”
OR: Mapping from transformations on the (_3 . _2) “place” to those on the larger object.
Lens s t a b = ??
Lens s t a b = (a -> b) -> (s -> t)??
Not quite. That can cover sets but not gets. Without gets, it’s not that useful.
Lens s t a b = ??
Lens s t a b = (a -> b) -> (s -> t)??
Not quite. That can cover sets but not gets. Without gets, it’s not that useful.
Lens s t a b = forall f . Functor f =>
(a -> f b) -> (s -> f t)
f = Identity for over
When f = Identity, the Lens type becomes
(a -> Identity b) -> (s -> Identity t), which is isomorphic to (a -> b) -> (s -> t).
over :: Lens s t a b -> (a -> b) -> (s -> t)
over lens f =
runIdentity . (lens (Identity . f))
set from over
set is just a subcase of over, with f = (\_ -> val), also known as const val.
set :: Lens s t a b -> b -> s -> t
set lens val obj = over lens (const val) obj
What about getters?
We’ve seen how (f = Identity) to get from
Functor f => (a -> f b) -> (s -> f t)
to the setter’s type signature of:
(a -> b) -> (s -> t).
How do we get an s -> a? What Functors might help?
f = Const a for view
When f = Const a, the Lens type becomes
(a -> Const a b) -> (s -> Const a t), which is isomorphic to (a -> a) -> (s -> a).
Conveniently, we have an a -> a for any a. It’s id.
view lens = getConst . (lens Const)
Other Functors...
f = Identity → iso. to (a -> b) -> (s -> t)
f = Const a → iso. to (a -> a) -> (s -> a), usable as s -> a.
f = [] → (a -> [b]) -> s -> [t]
f = Maybe → (a -> Maybe b) -> s -> Maybe t
f = IO → (a -> IO b) -> s -> IO t
f = State r → (a -> State r b) -> s -> State r t
A lens... from a setter and getter?
type Lens s t a b = forall f . (Functor f) =>
(a -> f b) -> (s -> f t)
lens :: (s -> a) -> (s -> b -> t) -> Lens s t a b
lens getter setter =
\f obj ->
(setter obj) `fmap` (f $ getter obj)
Don’t feel bad if this is not at all intuitive.
Ok, so why is this useful?
Recall that we wanted to capture the notion of place in a larger data structure, and use familiar notions of set/get.
farisa.status.mp -= 4
farisa.status.hp += 25
farisa.status.hp =
min(farisa.status.hp,
farisa.maxHP)
Creating your own Lenses
data Character = Character {
name :: String, class_ :: String,
maxHP :: Int, maxMP :: Int, status :: Status}
nameLens = lens name (\c v -> c {name = v})
… works but not necessary!
Auto-deriving lenses
Template Haskell is Haskell’s analogue to Lisp macros. It’s (mostly) beyond the scope of the course.
This is one tool set that I haven’t been able to make work in ghci. So, if you want to auto-derive lenses on your own datatypes, you need to compile.
makeLenses example
{-# LANGUAGE TemplateHaskell #-}
import Control.Lens
data Card =
Card {_rank :: Int,
_suit :: Char} deriving (Eq, Show)
data Board =
Board {_flop :: (Card, Card, Card),
_turn :: Card, _river :: Card} deriving (Eq, Show)
makeLenses ''Card
makeLenses ''Board
λ :t _rank
_rank :: Card -> Int
λ :t rank
rank :: Functor f =>
(Int -> f Int) -> Card -> f Card
-- Lens Card Card Int Int
λ view rank (Card 8 'h')
8
λ set suit 'd' (Card 8 'h')
Card {_rank = 8, _suit = 'd'}
makeLenses example
{-# LANGUAGE TemplateHaskell #-}
import Control.Lens
data Card =
Card {_rank :: Int,
_suit :: Char} deriving (Eq, Show)
data Board =
Board {_flop :: (Card, Card, Card),
_turn :: Card, _river :: Card} deriving (Eq, Show)
makeLenses ''Card
makeLenses ''Board
λ :t (flop . _1 . rank)
(flop . _1 . rank) :: Functor f =>
(Int -> f Int) -> Board -> f Board
λ let board = Board
(Card 8 'd', Card 9 'h', Card 2 's')
(Card 6 'h') (Card 10 'c')
λ view (flop . _3) board
Card {_rank = 2, _suit = 's'}
λ view (flop . _3 . suit) board
's'
Lens laws
These stipulate that view and set round-trip in the expected way and that a 2nd set obliterates the first one.
With great power comes great responsibility…
Beyond view, over, and set
The lens library has a lot (over 100!) of infix operators. They make getting and setting into nested data structures a lot more attractive (arguably).
There’s a lot to learn here and the best way is to look at examples.
(&) :: a -> (a -> b) -> b
(&) = flip ($)
Since lenses tend to flow left-to-right, this is really useful.
λ ((2, 3), (5, 7)) & view (_1 . _2)
3
λ ((2, 3), (5, 7)) & set _2 "Lost!"
((2,3),"Lost!")
(^.) :: s -> Lens s t a b -> a
(^.) = flip get
λ (5, (27, Nothing)) ^. _2
(27, Nothing)
λ (5, (27, Nothing)) ^. _2 . _1
27
(.~) :: Lens s t a b -> b -> s -> t
(.~) is just an infix set.
λ _1 .~ "Yo" $ (3, 12)
("Yo",12)
λ (3, 12) & _1 .~ "Hi"
("Hi",12)
(%~) :: Lens s t a b -> s -> b -> t
(%~) is just an infix over.
λ _1 %~ (^2) $ (5, "cats")
(25,"cats")
λ (5, "cats") & _2 %~ (map toUpper)
(5,"CATS")
(+~), (-~), (*~), etc.
(+~) :: (Num a) => Lens s t a a -> a -> s -> t
λ (5, "cats") & _1 +~ 10
(15,"cats")
λ (5, "cats") & _1 ^~ 3
(125,"cats")
Something should be bugging the crap out of you...
(5, "cats") & _1 +~ 10 doesn’t look like normal Haskell.
How does it work?
Parsing (5, "cats") & _1 +~ 10
λ :info (&)
infixl 1 &
λ :info (+~)
infixr 4 +~
(5, "c") & _1 +~ 10 →
�(&) (5, "c")
(_1 +~ 10) ~
(_1 +~ 10) $ (5, "c")
over _1 (+10) (5, "c")
(+=), (-=), (*=), etc.
(+=) :: (MonadState s m) =>
Lens s s a a -> a -> m ()
λ (_1 += 10) `runState` (5, "cats")
((),(15,"cats"))
λ do {_1 ^= 5; _2 <>= "!"; get} `evalState`
(2, "Hi")
(32,"Hi!")
Heuristics and organization
There’s some regularity to the infix operators. It takes time to figure it out. In general…
Source: A Little Lens Starter Tutorial by Joseph Abrahamson.
at as a lens into maps
λ M.fromList [(1, "one")] ^. at 1
Just "one"
λ M.fromList [(1, "one")] & (at 2) .~ (Just "two")
fromList [(1,"one"),(2,"two")]
λ M.fromList [(1, "one")] & (at 1) .~ Nothing
fromList []
Many, many more...
I’m nowhere near finished with what you can do with lenses. See https://github.com/ekmett/lens/wiki/Examples
for much more.
Beyond lenses
λ :t (+~)
(+~) :: Num a => ASetter s t a a -> a -> s -> t
No reference to lenses per se. It’s more general.
both
You can use the lens-like operators like (+~) with both, which is not a lens and touches more than one location. It’s a (multi-location) setter but not a getter.
λ (2, 3) & both ^~ 6
(64,729)
λ (5, 7) & both .~ "gone"
("gone","gone")
mapped
mapped makes a Functor settable in all locations. It’s not a lens either.
λ [3, 5, 9] & mapped ^~ 2
[9,25,81]
λ (5, ["wørd", ""]) & _2 . mapped %~ length
(5,[4,0])
Prisms, Traversals
Related concepts. Prisms deal with sum types rather than products, and Traversals are setters that can target multiple (or no) locations. With these, you can work on almost all data types (e.g. JSON).
A Lens is a Traversal and a Getter; both and mapped aren’t Getters because they focus on multiple places.
_Left and _Right prisms
λ (Left 5) & _Left +~ 4
Left 9
λ (Left 5) & _Right +~ 4
Left 5
These prisms refer to places that may or may not occur in the respective data.
(^.) with a Monoid
You can only use (^.) on a Prism if your type is a Monoid. You can use to to create a (non-settable) getter that applies a function.
λ (Left 5) ^. _Left . (to Sum)
Sum {getSum = 5}
λ (Left 5) ^. _Right . (to Sum)
Sum {getSum = 0}
(^.) with a Monoid
You can only use (^.) on a Prism if your type is a Monoid. You can use to to create a (non-settable) getter that applies a function.
λ (Left 5) ^. _Left . (to Sum)
Sum {getSum = 5}
λ (Left 5) ^. _Right . (to Sum)
Sum {getSum = 0}
(^..) goes into the list monoid
λ (Left 5) ^.. _Right
[]
λ (Right 9) ^.. _Right
[9]
λ (7, 8) ^.. both
[7,8]
Heal spell revisited
type Party = Map Int Character
heal :: Character -> Character -> (Bool, Character, Character)
heal caster target =
if (caster ^. mp) < 4 then (False, caster, target)
else (True, caster & mp -~ 4, target & hp %= (hpUp 25 tMaxHP))
where hp = status . currentHP
mp = status . currentMP
tMaxHP = target ^. maxHP
hpUp n limit = \hp -> min (hp + n) limit
Finally...
We’ve only scratched the surface of Lens and related types (e.g. Prism, Traversal).
Go here for much more: https://github.com/ekmett/lens/wiki
Questions?