Lens
Deprecated, use Ramda lenses
A Lens is a function which focuses on a particular aspect, property, index, ... of a data structure. Their purpose is to make changes in a data structure in a non-mutating functional way.
Lenses compose with themselves, traversal functions (Type.traverse(of_f)
), mapping functions (Type.map
) and isomorphism functions. All these items piped together drill deep down into a data structure. Thereby a particular aspect deep in this complex data structure can be modeled by such a composed lens function.
Lenses are put into action by a set of helper functions (view
, over
, set
) which take the lens and source data as arguments and return the result data.
For a thorough introduction read "Lenses with Immutable.js" by Brian Lonsdorf, for the details of the implementation for StaticLand data structures read "Partial Lenses Implementation" by Vesa Karvonen
Types
MapF = Functor F => (a -> b) -> Fa -> Fb
Lens sa = MapF => MapF -> (a -> Fa) -> sa -> Fsa
ComposableLens sa = (a -> Fa) -> sa -> Fsa
Note that ComposableLens
bears the same signature as map
.
Static-Land vs. Fantasy-Land (Ramda) Lenses
A lens is primary implemented as
lens = getter, setter => Fmap => x2Fy => xs =>
Fmap(y => setter(y, xs), x2Fy(getter(xs)));
While in Fantasy-Land the Fmap
argument is not needed, since the functor's map function is referenced by the [fantasy-land/map]
property.
lens = getter, setter => x2Fy => xs =>
x2Fy(getter(xs))[fantasy-land/map](y => setter(y, xs));
Thus, in StaticLand composability requires specifying a particular functor's map and therefore is much more laborious or requires a layer of helper functions.
Creation
In general two procedures are required, one for extracting the item of interest, the other for re-assembling the data structure. The returned functions (i.e. lenses) themselves cannot be composed together. Instead, they serve as initial building blocks for the entire composition of the data aspect.
lens(getter, setter)
:: (sa -> a) -> ((a, sa) -> sa) -> Lens sa
var fooLens = R.lens(R.prop("foo"), R.assoc("foo"));
var oneLens = R.lens(R.nth(1), R.update(1));
var firstFooLens = R.compose(oneLens, fooLens);
R.over(firstFooLens, R.toUpper, [{foo: "foo"}, {foo: "bar"}, {foo: "baz"}]);
// =>[ { foo: 'foo' }, { foo: 'BAR' }, { foo: 'baz' } ]
indexLens(n)
:: Number -> Lens sa
propertyLens(key)
Create Composable Lenses
These can be composed with map
, traverse
and themselves to define the focus on the data aspect.
makeComposableViewLens(lens)
F ≡ Constant
Creates one map
-like function for viewing / extracting a data aspect.
makeComposableOverLens(lens)
F ≡ Identity
Example, the data is
- inside a
bar
property - inside a
Promise
// data aspect
// ↓
const data = { bar: Promise.resolve("BAR") },
barLens = propertyLens("bar"),
// decide to "mutate" the data structure, so make an OverLens:
composableBarLens = makeComposableOverLens(barLens),
// define the whole path to the data
aspect = compose(composableBarLens, mapPromise);
// employ in a functional pipeline
changeAspect = over(aspect);
changeAspect(reverse)(data); // { bar: Promise.resolve("RAB") }
makeComposableSequenceLens(map_f, lens)
F ≡ f
Employ Lenses
view(viewLens)
:: Lens sa -> a
over(overLens)
:: Lens sa -> (a -> a) -> sa -> sa
set(overLens)
:: Lens sa -> a -> sa -> sa
sequence(lens, subject)
sequence(lens, subject)
:: ComposableLens sa -> saf -> fsa
Use Ramda lens
const lensedSequence = lens => lens(R.identity);
lensedSequence(R.lensIndex(1))(["foo", just("bar")])
// -> Just ["foo", "bar"]
Turn an array with a Maybe
element at second place into a Maybe
of an array.
const focusL = indexLens(1),
subjectA = ["foo", just("bar")],
subjectB = ["foo", nothing()];
sequence(focusL(Maybe.map), subjectA) // -> just(["foo", "bar"])
sequence(focusL(Maybe.map), subjectB) // -> nothing