Lenses

A Lens provides access to a particular part of a data structure.

Lenses express a high-level pattern for composition and in that sense belong firmly in Chapter 3, Patterns for Composition. However, the concept of Lens is also deeply entwined with Foldable and Traversable, and so we describe it in this chapter instead.

Lenses relate to the getter and setter functions, which also describe access to parts of data structures. To find our way to the Lens abstraction (as per Edward Kmett's Lens library), we'll start by writing a getter and setter to access the root node of a tree.

Deriving Lens

Let's return to our Tree type from earlier:

data Tree a = Node a (Tree a) (Tree a)
            | Leaf a
deriving Show

intTree
   = Node 2 (Leaf 3)
            (Node 5 (Leaf 7)
            (Leaf 11))

listTree
   = Node [1 ,1 ] (Leaf [2,1 ])
                  (Node [3,2] (Leaf [5,2])
                  (Leaf [7,4]))

tupleTree
   = Node (1 ,1 ) (Leaf (2,1 ))
                  (Node (3,2) (Leaf (5,2))
                  (Leaf (7,4)))

Let's start by writing generic getter and setter functions:

getRoot :: Tree a    -> a
getRoot (Leaf z)     = z
getRoot (Node z _ _) = z

setRoot :: Tree a -> a -> Tree a
setRoot (Leaf z)     x = Leaf x
setRoot (Node z l r) x = Node x l r

main = do
  print $ getRoot intTree
  print $ setRoot intTree 11
  print $ getRoot (setRoot intTree 11)

If, instead of setting a value, we want to pass in a setter function, we can define a modifier function based on the getter and setter functions:

fmapRoot :: (a -> a) -> Tree a -> Tree a
fmapRoot f tree = setRoot tree newRoot
  where newRoot = f (getRoot tree)

We have to get the root of the Tree, apply the function, and then set the result. This double work is akin to the double traversal we saw when writing traverse in terms of sequenceA. In that case, we resolved the issue by first defining traverse and then writing sequenceA in terms of traverse.

We can do the same thing here, by writing fmapRoot to work in a single step (and then rewriting setRoot' in terms of fmapRoot'):

fmapRoot' :: (a -> a) -> Tree a -> Tree a
fmapRoot' f (Leaf z)     = Leaf (f z)
fmapRoot' f (Node z l r) = Node (f z) l r

setRoot' :: Tree a -> a -> Tree a
setRoot' tree x = fmapRoot' (\_ -> x) tree

main = do
  print $ setRoot' intTree 11
  print $ fmapRoot' (*2) intTree

The fmapRoot' function delivers a function to a particular part of the structure and returns the same structure:

  fmapRoot' :: (a -> a) -> Tree a -> Tree a

To make provision for IO we need to define the following function:

  fmapRootIO :: (a -> IO a) -> Tree a -> IO (Tree a)

We can generalize this beyond IO to all monads:

  fmapM :: (a -> m a) -> Tree a -> m (Tree a)

It turns out that if we relax the requirement for Monad and instead generalize to all the Functor container types f', then we get a simple van Laarhoven Lens of type:

type Lens' s a 
   = Functor f' => 
          (a -> f' a) -> s -> f' s

The remarkable thing about a van Laarhoven Lens is that, given the above function type, we also gain get, set, fmap, and mapM, along with many other functions and operators.

The Lens function type signature is all that it takes to make something a Lens that can be used with the Lens library. It is unusual to use a type signature as the"primary interface" for a library. The immediate benefit is that we can define a Lens without referring to the Lens library.

We'll explore more benefits and costs to this approach, but first let's write a few lenses for our tree.

(The derivation of the Lens abstraction used here is based on Jakub Arnold's "Lens tutorial", which is available at http://blog.jakubarnold.cz/2014/07/14/lens-tutorial-introduction-part-1.html.)

Writing a Lens

A Lens is said to provide focus on an element in a data structure.

Our first Lens will focus on the root node of a Tree data­structure. Using the Lens type signature as our guide, we arrive at the following:

lens':: Functor f  => (a -> f' a) -> s      -> f' s
root :: Functor f' => (a -> f' a) -> Tree a -> f' (Tree a)

This is still not very tangible; fmapRootIO is easier to understand, with the Functor f' being IO:

fmapRootIO :: (a -> IO a) -> Tree a -> IO (Tree a)
fmapRootIO g (Leaf z)     = (g z) >>= return . Leaf
fmapRootIO g (Node z l r) = (g z) >>= return . (x -> Node x l r)

displayM x = print x >> return x  

main = fmapRootIO displayM intTree

If we drop down from Monad into Functor, we have a Lens for the root of a Tree:

root :: Functor f' => (a -> f' a) -> Tree a -> f' (Tree a)

root g (Node z l r) = fmap (x ­> Node x l r) (g z)
root g (Leaf z)     = fmap Leaf              (g z)

Since Monad is a Functor, this function also works with Monadic functions:

  main = root displayM intTree

Because root is a Lens, the Lens library enables the following:

-– import Control.Lens
main = do
  -- GET
  print $ view root listTree
  print $ view root intTree
  -- SET
  print $ set root [42] listTree
  print $ set root 42   intTree
  -- FMAP
  print $ over root (+11) intTree

The over function is the Lens way of mapping a function to a Functor value.

Composable getters and setters

Let's define a Lens that provides focus on the rightmost leaf of a Tree data structure:

rightMost :: Functor f' => 
  (a -> f' a) -> Tree a -> f' (Tree a)

rightMost g (Node z l r) 
  = fmap (
' -> Node z l r') (rightMost g r)
rightMost g (Leaf z)      
  = fmap (x -> Leaf x) (g z)

The Lens library provides several lenses for Tuple (for example, _1 which brings focus to the first Tuple element). We can compose our rightMost Lens with these Tuple lenses:

main = do
  print $ view rightMost tupleTree
  print $ set rightMost (0,0)  tupleTree

  -- Compose Getters and Setters
  print $ view (rightMost._1) tupleTree
  print $ set (rightMost._1) 0 tupleTree
  print $ over (rightMost._1) (*100) tupleTree

A Lens can serve as a getter, setter, or modifier.

Moreover, we compose Lenses using the regular function composition (.)! Note that the order of composition is the reverse of regular function composition: in (rightMost._1), the rightMost Lens is applied before the _1 Lens.

Lens Traversal

A Lens focuses on one part of a data structure, not several; for example, a Lens cannot focus on all the leaves of a Tree:

  set leaves 0 intTree
  over leaves (+1) intTree

To focus on more than one part of a structure, we need a Traversal, the Lens generalization of Traversable. While Lens relies on Functor, Lens Traversal relies on Applicative. Other than this, the signatures are exactly the same:

traversal :: Applicative f' => 
    (a -> f' a) -> Tree a -> f' (Tree a)
lens :: Functor f'=>
    (a -> f' a) -> Tree a -> f' (Tree a)

Let’s define a leaves Traversal that delivers a setter function to all the leaves of a Tree:

leaves :: Applicative f' => (a -> f' a) -> Tree a -> f' (Tree a)
leaves g (Node z l r)
  = Node z <$> leaves g l <*> leaves g r
leaves g (Leaf z)     
  = Leaf <$> (g z)

We can use set and over with our new Traversal:

  set leaves 0 intTree
  over leaves (+1) intTree

Traversals compose seamlessly with Lenses:

main = do 
  -- Compose Traversal + Lens
  print $ over (leaves._1) (*100) tupleTree
  
  -- Compose Traversal + Traversal
  print $ over (leaves.both) (*100) tupleTree

  -- map over each elem in target container (e.g. list)
  print $ over (leaves.mapped) (*(-1)) listTree

  -- Traversal with effects
  mapMOf leaves displayM tupleTree

(both is a Tuple Traversal that focuses on both tuple elements).

Lens.Fold

The Lens.Traversal abstraction lifts Traversable into the realm of lenses, while Lens.Fold does the same for Foldable:

main = do
  print $ sumOf leaves intTree
  print $ anyOf leaves (>0) intTree

As is the case for Foldable sum and foldMap, we can write Lens sumOf in terms of foldMapOf:

  getSum $ foldMapOf lens Sum

where foldMapOf is a generalization of Foldable.foldMap.

The Lens library

We've used only simple Lenses; a fully parameterized Lens would allow for replacing parts of a data structure with different types:

  type Lens s t a b = Functor f' => (a -> f' b) -> s -> f' t
  –- vs simple Lens
  type Lens' s a = Lens s s a a

Lens library function names do their best not to clash with existing names; for example, postfixing of idiomatic function names with "Of" (sumOf, mapMOf, and so on), or using different verb forms such as droppingWhile instead of dropWhile. While this creates a burden in terms of having to learn new variations, it does have a big plus: allowing for easy unqualified import of the Lens library.

By leaving the Lens function type transparent (and not obfuscating it with a newtype), we get Traversals by simply swapping out Functor for Applicative in the Lens type definition. We also get to define lenses without having to reference the Lens library. On the downside, Lens type signatures can be bewildering at first sight. They form a language of their own that requires an effort to get used to, for example:

mapMOf :: Profunctor p =>
  Over p (WrappedMonad m) s t a b -> p a (m b) -> s -> m t

foldMapOf :: Profunctor p =>
  Accessing p r s a -> p a r -> s -> r

On the surface, the Lens library gives us composable getters and setters, but there is much more to lenses than that. By generalizing Foldable and Traversable into Lens abstractions, the Lens library lifts getters, setters, lenses, and Traversals into a unified framework in which they all compose together.

The Lens library has been criticized for not reflecting idiomatic Haskell and for simply taking on too much responsibility. Nevertheless, Edward Kmett's Lens library is a sprawling masterpiece that is sure to leave a lasting impact on Haskell.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset