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

Time:02-21

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.

CodePudding user response:

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).

CodePudding user response:

ix 0 doesn't produce a lens, but a traversal.1

Informally, a lens is a "path" that will definitively reach a single value (if you follow it within a hypothetical larger value). A traversal is a path to zero or more values. You can set or view the single target of a lens. And you can set the zero or more targets of a traversal (this simply updates all of them that are present, which is a no-op if there are zero present). But viewing the targets of a traversal is less straightforward.

If view simply took a traversal, and an outer structure, and gave you the target value, then it would have a problem. If there are multiple targets, how should it decide which to return? And if there are zero targets, it can't return anything; it would have to be partial. What it needs is a way of condensing zero-or-more values into a single value, so it can return that. And the Monoid class provides exactly the facilities to do that; mempty for if there aren't any targets at all, and <> to condense multiple values to a single one. So view with a traversal2 actually requires a Monoid constraint on the returned type, and that's why you're getting the complaint about No instance for (Monoid Int) arising from a use of `ix'.

And in case it isn't clear, you can often compose (with .) different types of optics (the general term for "lens-ish things", including lenses, traversals, and several others). But the result has the capabilities of the least capable of the two inputs. So even though your inner and w are full lenses, composing them with a traversal produced by ix results in a traversal, not a lens.

But in this case you know that you're using ix. The specific kind of traversals ix makes end up having zero or one target, rather than the zero or more targets that traversals have in general. So you could use preview instead of view; for a traversal it will produce a Maybe containing Just the first target of the traversal, or Nothing if there weren't any. A Maybe is exactly what the type system deems appropriate here, since ix can't guarantee there will be a target value, but there won't be more than one.

In my experience, when I try to view something and get this Monoid instance error, it almost always means I have an optic that can't guarantee a result and I should actually be using preview to get a Maybe.

Simple example:

λ view (ix 1) [True, False]

<interactive>:16:7: error:
    • No instance for (Monoid Bool) arising from a use of ‘ix’
    • In the first argument of ‘view’, namely ‘(ix 1)’
      In the expression: view (ix 1) [True, False]
      In an equation for ‘it’: it = view (ix 1) [True, False]

λ preview (ix 1) [True, False]
Just False
it :: Maybe Bool

λ preview (ix 1) [True]
Nothing
it :: Maybe Bool

1 ix can't return lenses, because it's supposed to take an index and then represent a "path" into any indexable structure (that can be indexed by the type of index it was given, at least). In ix 0, ix hasn't seen the particular structure you're going to use it on yet, so there's no way it can guarantee there is a value at that index; you could even use let i = ix 0 and then use i multiple times to peek into different structures!


2 More technically, view is supported by traversals because it's supported by folds, and all traversals are folds. A fold knows how to access zero-or-more targets, but unlike a traversal it isn't a "path"; it just accesses the values with no knowledge about their context in the structure (other than a canonical order).

There's a handy but confusing diagram on the main hackage page for the lens package, which attempts to describe how the major functionality is "inherited" by various flavours of optics. It's a little overwhelming... but less so than the full documentation scattered over many different modules.

You should be able to see that in the top right we have Setter providing set and over, which is inherited by Traversal. Whereas in the top left we have view with a Monoid constraint, that is inherited by Traversal. In the mid-left we have Getter having a version of view without the Monoid constraint, which is inherited by Lens but not by Traversal.

  • Related