4 ms·
(Author here.) It's a very fair question. I'm pretty sure lenses were invented because Haskell's built-in records are not ergonomic, but then the community dis
by _jackdk_ 2y ago
(Author here.)
It's a very fair question. I'm pretty sure lenses were invented because Haskell's built-in records are not ergonomic, but then the community discovered that they're extremely useful, general, and flexible.
Here is a Redux (JS) example of the problem that lenses help solve, where you are working with immutable data and want to replace one field of a deeply-nested structure: https://redux.js.org/usage/structuring-reducers/immutable-update-patterns https://redux.js.org/usage/structuring-reducers/immutable-up...
You can think of a lens as a getter/setter, stored together as a first-class value. I'm not great at TS but hopefully you can see what I'm getting at with these types:
A Lens<S,T,A,B> is a pair of functions:
* get: (s: S) => A
* set: (s: S, b: B) => T
The most obvious lenses you can make are lenses that represent record fields (object properties?). But you can make lenses into other things, like a function that returns a lens into the Nth bit of an integer, or a function that takes a map key and returns a Lens from Map<K,V> to Optional<V>.
You use lenses by passing them to functions that then operate on your data. Three common ones are `view`, `set`, and `modify`:
* view: (lens: Lens<S,T,A,B>, s: S) => A
* set: (lens: Lens<S,T,A,B>, s: S, b: B) => T
* modify: (lens: Lens<S,T,A,B>, s: S, f: (a: A) => B) => T
So far, it doesn't look like we've bought much. But the real payoff comes from when you notice that lenses compose:
* compose: (outer: Lens<S,T,X,Y>, inner: Lens<X,Y,A,B>) => Lens<S,T,A,B>
This solves the "nested record problem" because you can compose the lenses for each field in the chain to get a lens from the outer structure to the field you're interested in, and functions like `set` and `modify` will correctly copy all the fields at each layer when you call `set` or `modify`. Haskell's lens libraries give you compact operators for doing this, so it all looks neat and tidy.
A reasonable response to all this machinery is, "OK, your language has bad records and you built a library to paper over them. So what?" To this I say:
1. Every language has this problem when working with nested immutable data, though not every language has the tools to compactly encode lenses and make them ergonomic to use; and
2. Lenses generalise to give you other "optics". A `Traversal<S,T,A,B>` picks out zero or more `A`s from `S`, and can replace them all with `B`s. `modify` and `set` then generalise, and perform their update on _every_ matching `A` selected by the `Traversal`, and you also get `toListOf`, which collects all matching `A`s into a list. There are other optics too, but this post is getting long.
Once you have the more powerful optics, you get a very expressive toolkit for querying and updating data, that works on any data structure that has optics defined. (And Haskell has tools to derive many optics automatically.)
Chris Penner's wonderful book, _Optics By Example_[1], has a bunch of exercises that involve slicing and dicing a big slab of Kubernetes YAML with queries like "collect a list of all 'containerPort's alongside their Pod's 'name' (from 'metadata')" and operations like "set a resource request of `memory: "256M"` for every container". The answers are extremely compact and elegant, and I don't know how you'd achieve anything similar with other methods.
I hope this helped, because there is definitely a really useful and powerful toolkit hiding behind that absurd-looking type signature.
[1]: https://leanpub.com/optics-by-example/ https://leanpub.com/optics-by-example/