These are chat archives for ramda/ramda

Jul 2015
Shane Keulen
Jul 19 2015 01:03 UTC
@scott-christopher : thanks for trying, that comment is completely over my head, can you recommend a good resource for learning that type annotation?
Scott Christopher
Jul 19 2015 04:00 UTC
@SeeThruHead the type annotations are more or less taken from Haskell’s type defintions.
Scott Christopher
Jul 19 2015 04:12 UTC

For the definition:

Lens a b :: Functor f => (b -> f b) -> (a -> f a)

You can think of Lens a b as an alias for something that represents a lens with a focus on some type b that can be accessed within some type a. So headLens that focussed on the first element of an array of strings would be a Lens Array String if you swap out the a and b type variables.

The Functor f => indicates a constraint such that any f in the type signature that follows must be a Functor (something that can be mapped over)
(b -> f b) can be read as a function that takes some type b and returns a Functor of bs.
(b -> f b) -> (a -> f a) can be read as a function which takes a (b -> f b) function and returns an (a -> f a) function.
Scott Christopher
Jul 19 2015 04:22 UTC
It should be emphasized that the plumbing of lenses shouldn’t need to be understood to make use of them. If you stick with generating lenses via R.lens and use them via R.over, R.view and R.set then you shouldn’t need to think about what a lens looks like on the inside.
If however you’re interested in understanding how they work, I’ll gladly have a go at explaining.
Scott Christopher
Jul 19 2015 04:58 UTC
We can start by looking at how R.lens creates a lens from a getter and setter function to fit the type alias above (using ES6 arrows for brevity):
// toFunctor :: focusType -> someFunctorOf focusType
// getter :: outerType -> focusType
// setter :: focusType -> outerType -> outerType
const lens(getter, setter) = toFunctor => outerThing =>
  map(focusValue => setter(focusValue, outerThing), toFunctor(getter(outerThing)));
Scott Christopher
Jul 19 2015 05:04 UTC
So if we were to create a headLens via lens(R.head, R.update(0)), we’d end up with something equivalent to the following:
const headLens = toFunctor => list =>
  map(headVal => update(0, headVal, list), toFunctor(R.head(list)));
The trick to getting both setter and getter behaviour out of this one function lies in the toFunctor function passed as the first argument, which is used for lifting the output of the getter function into a given functor.
Scott Christopher
Jul 19 2015 05:09 UTC
We can then use the Const functor for getting a value (which happens in R.view), or the Identity functor for setting a value (which happens in R.over and R.set)
Scott Christopher
Jul 19 2015 05:16 UTC
Mapping over a Const will always produce a constant value, essentially ignoring the mapping function. e.g., Const(10)).value //=> 10
And mapping over an Identity is effectively just applying the mapping function to the value in the Identity. e.g., Identity(10)).value //=> 11

So the getter (R.view) for headLens would effectively be equivalent to:

list => map(headVal => update(0, headVal, list), Const(R.head(list))).value

and given that mapping over a Const is effectively moot, this can be reduced to:

list => Const(R.head(list)).value
// or
list => R.head(list)
Scott Christopher
Jul 19 2015 05:37 UTC

And a setter (R.over) would look like:

(fn, list) => {
  const toFunctor = R.compose(Identity, fn);
  return map(newVal => update(0, newVal, list), toFunctor(R.head(list))).value

and given map(f, Identity(x)) === Identity(f(x)), this can be reduced to:

(fn, list) => Identity(update(0, fn(R.head(list)), list)).value
// which is equivalent to
(fn, list) => update(0, fn(R.head(list)), list)
and then set can just be defined in terms of over and always:
const set = (someLens, newVal, thing) => over(someLens, R.always(newVal), thing)
Scott Christopher
Jul 19 2015 05:42 UTC
and that’s enough rambling for now :smile:
David Chambers
Jul 19 2015 06:59 UTC
Nice explanation, @scott-christopher
Scott Christopher
Jul 19 2015 09:03 UTC
I didn’t quite explain anything about how they compose above beyond what I posted in the github issue, but perhaps with the plumbing out of the way I can try explain further.

Suppose we want to create a lens that will focus on the 'id' field of the first element in a list of things.

//:: [{ 'id': Number }]
var things = [{ 'id': 0 }, { 'id': 42 }];

We could build this by composing R.lensIndex(0) and R.lensProp('id').

var headLens = R.lensIndex(0);
var idLens = R.lensProp('id');
var headIdLens = R.compose(headLens, idLens);
We can then see how headLens and idLens compose by substituting the type variables in the lens signature with those from things and aligning by their common types.
idLens     :: (Number -> f Number) -> ({ 'id': Number } -> f { 'id': Number })
headLens   ::                         ({ 'id': Number } -> f { 'id': Number }) -> ([{ 'id': Number }] -> f [{ 'id': Number }])
headIdLens :: (Number -> f Number)                                             -> ([{ 'id': Number }] -> f [{ 'id': Number }])
In terms of how they compose using the plain ol' R.compose, you can see that idLens produces a toFunctor function (a -> f a) with the same type that headLens accepts
as its argument:
({ 'id': Number } -> f { 'id': Number })
This is why headLens is composed to the left of idLens using R.compose, because headLens can receive the toFunctor function produced by idLens.
R.view or R.over can then pass the initial Const or Identity toFunctor functions to the right-most of the composed lenses to kick things off.
Again, I don’t know whether this helps or not, so please feel free to ask any questions to help clarify.
Kevin Wallace
Jul 19 2015 15:10 UTC
@scott-christopher thanks for the explanation!
I haven't studied lenses yet so I can't be a million percent sure about this, but it sounds to me like the reason lenses compose "backwards" is very analogous to the reason transducers compose backwards
where Const or Identity corresponds with the f argument to transduce
Kevin Wallace
Jul 19 2015 15:16 UTC
and the compositions of lenses, eg var headIdLens = R.compose(headLens, idLens); corresponds to the xf argument to transduce, eg R.compose(, R.filter(isOdd))
maybe a small insight into transducers for those who understand lenses better
Hardy Jones
Jul 19 2015 16:16 UTC
Yeah, that's part of the big picture: transducers are a less generalized version of lenses