I've been reading A wreq tutorial:
A lens provides a way to focus on a portion of a Haskell value. For example, the
Response
type has aresponseStatus
lens, which focuses on the status information returned by the server.ghci> r ^. responseStatus Status {statusCode = 200, statusMessage = "OK"}
The
^.
operator takes a value as its first argument, a lens as its second, and returns the portion of the value focused on by the lens.We compose lenses using function composition, which allows us to easily focus on part of a deeply nested structure.
ghci> r ^. responseStatus . statusCode 200
I can't come up with a way how function composition done with this order of arguments could treat the nesting structure in that order.
Look: r ^. responseStatus . statusCode
could be either r ^. (responseStatus . statusCode)
or (r ^. responseStatus) . statusCode
.
In the first one says we construct a function which first treats statusCode
(gets it from the record Status
? -- as I can deduce from the shown value Status {statusCode = 200, statusMessage = "OK"}
), and then passes it to responseStatus
which must treat the response status. So, it's the other way round: in reality, the status code is a part of the response status.
The second reading also doesn't make sense to me because it treats the status code first, too.
The right reading of r ^. responseStatus . statusCode
is r ^. (responseStatus . statusCode)
. This is only natural, since function composition returns a function when applied to two arguments, thus (r ^. responseStatus) . statusCode
must return a function, as opposed to any value that could be printed out.
This still leaves open the question why lenses compose in the "wrong" order. Since the implementation of lenses is a bit magical, let's look at a simpler example.
first
is a function that maps over the first element of a pair:
first :: (a -> b) -> (a, c) -> (b, c)
first f (a, b) = (f a, b)
What does map . first
do? first
takes a function acting on the first element, and returns a function acting on a pair, which is more apparent if we parenthesize the type this way:
first :: (a -> b) -> ((a, c) -> (b, c))
Also, recall the type of map
:
map :: (a -> b) -> ([a] -> [b])
map
takes a function acting on an element and returns a function acting on a list. Now, f . g
works by first applying g
and then feeding the result to f
. So map . first
takes a function acting on some element type, converts it to a function acting on pairs, then converts it to a function acting on lists of pairs.
(map . first) :: (a -> b) -> [(a, c)] -> [(b, c)]
first
and map
both turn functions acting on a part of a structure to functions acting on the whole structure. In map . first
, what is the whole structure for first
becomes the focus for map
.
(map . first) (+10) [(0, 2), (3, 4)] == [(10, 2), (13, 4)]
Now take a look at the type of lenses:
type Lens = forall f. Functor f => (a -> f b) -> (s -> f t)
Try to ignore the Functor
bits for now. If we squint slightly, this resembles the types for map
and first
. And it happens so that lenses also convert functions acting on parts of structures into function acting on whole structures. In the signature above s
denotes the whole structure and a
denotes a part of it. Since our input function can change the type of a
to b
(as indicated by a -> f b
), we also need the t
parameter, which roughly means "the type of s
after we changed a
to b
inside it".
statusCode
is a lens that converts a function acting on an Int
to a function acting on a Status
:
statusCode :: Functor f => (Int -> f Int) -> (Status -> f Status)
responseStatus
converts a function acting on a Status
to a function acting on a Response
:
responseStatus :: Functor f => (Status -> f Status) -> (Response -> f Response)
The type of responseStatus . statusCode
follows the same pattern as we've seen with map . first
:
responseStatus . statusCode :: Functor f => (Int -> f Int) -> (Response -> f Response)
It remains to be seen how exactly ^.
works. It's intimately tied to the core mechanic and magic of lenses; I will not reiterate it here, since there are quite a few writings about it. For an introduction I recommend looking at this one and this one, and you could also watch this excellent video.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With