How Haskell's "composing lenses using function composition" with that weird order of arguments could be implemented?
Asked Answered
D

1

12

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 a responseStatus 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.

Devan answered 8/3, 2015 at 17:18 Comment(3)
statusCode must be a lens, not the record field selector. I guess they must have hid the field selector and exported a lens of the same name; pretty confusing if you ask me. (Or wrote a custom Show instance.)Fillip
@ReidBarton That must be a part of the puzzle. Since I know very little about lenses, my question is also a question about the underlying idea: how normal function composition can be used for accessing the structure in reverse order? (This reminds me continuation-passing style a bit.)Devan
That's essentially correct. Lenses are kind of like CPS computations of a kind so they, as a side effect, flip function composition.Befoul
A
15

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.

Awed answered 8/3, 2015 at 18:25 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.