jfla
jfla

Reputation: 568

Looping over lenses, and using each with view and set

This is a version of some code I have been working on, with the specifics removed. Hopefully it's clear what I'm trying to do, but if not I can clarify. I'm quite new to using lenses, and the complicated types involved often make them seem more trouble than they're worth.

-- some data type
type AType = ...

data Thing = Th { _v, _w, _x :: AType, _otherStuff :: ... }

makeLenses ''Thing

-- some operation that can be performed on corresponding x or w or v values in two Things.
f :: AType -> AType -> AType
f = ...

-- apply f to the corresponding v, and w, and x values in a and b; then store each result in the respective field of a, leaving other fields untouched.
transform :: Thing -> Thing -> Thing
transform a b =
    let transformVorWorX :: Lens' Thing AType -> AType
        transformVorWorX lens = 
            let vwx  = view lens a
                vwx' = view lens b
            in  f vwx vwx'

    in  foldl (\a' lens -> set lens (transformVorWorX lens) a') a [v,w,x]

When I compile, ghc spits out

Could not deduce (f ~ Identity)
from the context (Functor f)
  bound by a type expected by the context:
             Functor f => (AType -> f AType) -> Thing -> f Thing
  at ...
  ‘f’ is a rigid type variable bound by
      a type expected by the context:
        Functor f => (AType -> f AType) -> Thing -> f Thing
      at ...
Expected type: (AType -> f AType) -> Thing -> f Thing
  Actual type: ASetter Thing Thing AType AType
In the first argument of ‘transformVorWorX’, namely ‘lens’
In the second argument of ‘set’, namely ‘(transformVorWorX lens)’

Why doesn't this code work? I found that replacing lens with cloneLens lens lets it compile, but I'm not sure why, and this feels very ugly - is there a more elegant way to do what I want, that is perhaps more general?

Many thanks

Upvotes: 2

Views: 186

Answers (1)

leftaroundabout
leftaroundabout

Reputation: 120711

Lens' is under the hood universally quantified

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

So the type of [v,w,x] would have to be

[∀ f . Functor f => (AType -> f AType) -> Thing -> f Thing]

– with a quantor inside the list. Such types are called impredicative types, and Haskell doesn't support them. (A GHC extension with that name has been around for a while, but it doesn't work.) [Edit: work on impredicatives is ongoing as of 2021.]

There's a relatively easy way that always works to get around this: hide the quantor in a newtype. In the lens library, this is ReifiedLens. With it you can write

transform a b =
    let transformVorWorX :: ReifiedLens' Thing AType -> AType
        transformVorWorX (Lens lens) = 
            let vwx  = view lens a
                vwx' = view lens b
            in  f vwx vwx'
    in  foldl (\a' lens -> set (runLens lens) (transformVorWorX lens) a') a
            [Lens v, Lens w, Lens x]

This code has exactly the runtime behaviour of the one you envisioned, but it's clearly pretty ugly.

The alternative solution you've discovered yourself does effectively the same thing too but is rather nicer. To see what's actually going on there, I'll put the cloneLens within transformVorWorX:

transform a b =
    let transformVorWorX :: ALens' Thing AType -> AType
        transformVorWorX lens = 
            let vwx  = view (cloneLens lens) a
                vwx' = view (cloneLens lens) b
            in  f vwx vwx'
    in  foldl (\a' lens -> set (cloneLens lens) (transformVorWorX lens) a') a [v,w,x]

The reason this works is that, like ReifiedLens – but unlike LensALens' has no quantor. It is just a concrete instantiation of the Functor f, thus it doesn't require impredicativity but you can directly use v, w and x in a [ALens' Thing AType] list. The functor instance is cleverly chosen to not lose any of the generality, hence cloneLens is able to give you back a full, universally-quantised lens.

Rather than cloning it, you can also use the ^# operator, which directly views an ALens, and storing instead of set:

transform a b =
    let transformVorWorX :: ALens' Thing AType -> AType
        transformVorWorX lens = 
            let vwx  = a^#lens
                vwx' = b^#lens
            in  f vwx vwx'
    in  foldl (\a' lens -> storing lens (transformVorWorX lens) a') a [v,w,x]

If we condense that to a less verbose form, it looks quite good IMO:

transform a b = foldl updater a [v,w,x]
 where updater a' lens = a' & lens #~ f (a^#lens) (b^#lens)

Let me also suggest an entirely different solution: you define a single optic that accesses all the values of interest, namely

vwx :: Traversal' Thing AType
vwx f (Thing в ш к otherVals)
    = (\в' ш' к' -> Thing в' ш' к' otherVals)
      <$> f в <*> f ш <*> f к

Instead than defining this yourself, you can also let the Template Haskell helper do it for you:

makeLensesFor [("_v", "vwx"), ("_w", "vwx"), ("_x", "vwx")] ''Thing

Now, a traversal can be used as a simple fold, and thus to get a list of all the values. You can then use a standard zip to perform the transformation of the elements:

transform a b = ...
 where tfmedVals = zipWith f (a^..vwx) (b^..vwx)

To put that list of modified values back in a container, the library has also a function: partsOf. And with that, your entire transformation boils down to

transform a b = a & partsOf vwx .~ zipWith f (a^..vwx) (b^..vwx)

Upvotes: 7

Related Questions