Search code examples
haskellhaskell-lens

Haskell lens : view doesn't reference like over and set?


First time using lens. set and over went easy enough and I thought it would be simple with view: Use the same scheme to reference the inner part, but don't supply a new value or function. But Noooo. tst3 below gives the error below the code. Anyone know what's going on?

-- Testing lenses
tst1 = set (inner . ix 0 . w) 9 outer
tst2 = over (inner . ix 0 . w) (+2) outer
tst3 = view (inner . ix 0 . w) outer       -- this line errors out

    * No instance for (Monoid Int) arising from a use of `ix'
    * In the first argument of `(.)', namely `ix 0'
      In the second argument of `(.)', namely `ix 0 . w'
      In the first argument of `view', namely `(inner . ix 0 . w)'

Here is some pared down code illustrating.

{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE FlexibleInstances #-}

import Control.Lens

data Inner = Inner
    {  _w :: Int
    } deriving (Show, Eq)

data Outer = Outer
    { _inner :: [Inner]
    } deriving Show

outer = Outer
    [
    Inner 0,
    Inner 1,
    Inner 2
    ]

makeLenses ''Inner
makeLenses ''Outer

tst1 = set (inner.ix 0.w) 999 outer
tst2 = over (inner.ix 0.w) (+77) outer
tst3 = view (inner.ix 0.w) outer        -- this errors out

I've read up on view and the simple examples look like mine, as in,

>>> let atom = Atom { _element = "C", _point = Point { _x = 1.0, _y = 2.0 } }
>>> view (point . x) atom
1.0

though I haven't found an example that indexes an inner structure with ix.


Solution

  • The problem is that ix 0 is a traversal, not a lens, so instead of focusing on the 0th element, it focuses on the collection of all elements that are the 0th element. Why does it do this? Well, assuming _inner is a list of type [Inner], usually there's only one element that is the 0th element, but sometimes (if the list is empty), there are no elements that are the 0th element. Having ix 0 be a traversal accounts for this possibility.

    Since ix 0 is a traversal, it "poisons" the whole optic inner . ix 0 . w. Even if inner and w are lenses, the composition with ix 0 is "only" a traversal.

    Now, there's no problem with "setting" or "overing" with a traversal. If there's no 0th element, the operation just doesn't do anything. On the other hand, there is a bit of a problem with viewing such a traversal. If you expect to get an element, you might not get one.

    The error message arises because view tries to handle traversals by assuming the result is a Monoid. This allows it to combine zero, one, or multiple results from the traversal into a single return value of the same type. This functionality is kind of esoteric, which makes the error message pretty confusing, but you get used to seeing it.

    There are several things you can do. You can replace view with a special view operators. The usual view operator ^. is similar to view, so it generates the same error message:

    -- parentheses are optional here, but included for clarity
    tst4 = outer ^. (inner . ix 0 . w)
    

    But the operators ^.., ^? and ^?! will all type check:

    tst5 = outer ^.. (inner . ix 0 . w)
    tst6 = outer ^? (inner . ix 0 . w)
    tst7 = outer ^?! (inner . ix 0 . w)
    

    The first (^..) returns all (zero or more) results of the traversal as a list. The second (^?) returns the first result as a Maybe, using Nothing to indicate there were no results. The last (^?!) returns the first result directly, generating an error if there are zero results (like using head on an empty list).