Consider pair(a, b) with first(pair) and rest(pair). I can define first-lens and rest-lens for [get-first, set-first] and [get-rest, set-rest] respectively. I could use them as simple getter/setter groupings with lens-view(rest-lens, pair) or lens-set(first-lens, 4, pair), which isn't so useful by itself, or I can compose then to get third-lens with lens-compose(rest-lens, first-lens), allowing me to lens-set(third-lens, 7, pair) without a nested call.
I just tried to summarize what I could read [1] [2] [3] and I don't claim to entirely understand it, but it's still better than the sibling comment dismissing it as abstract nonsense.
[1] https://docs.racket-lang.org/lens/lens-intro.html
[2] https://hackage.haskell.org/package/lenses-0.1.4/docs/Data-L...
But what optics (often sloppily referred to as lenses) give you are really powerful getters and setters. They can do what getters and setters in Python do, but they can also
- target multiple fields in multiple objects at the same time, or
- target fields only when a condition is true,
- they can target deeply nested fields,
- they can target computed values which have a compound effect on the underlying fields,
- they can return a new full object where the targeted fields are transformed by an arbitrary function, etc.
This means optics are extremely broadly useful. They can be used as OO-style getters and setters, but they can also be used to build and navigate JSON objects, or parse HTML content. It's a common interface that can be used for getting and modifying data in any broader context.
s - the larger data structure
a - a view value
b - a “update” value
t - the data structure after an update is applied
Of course, everything is still immutable in the same way that cons-ing a value onto a list is immutable.
The change in type between s and t allows you to nest these views, hide information, etc.
`b` and `t` are just for type-changing updates, like when you replace a field in a tuple or every element in a list.
Why the HELL would you need objects and object oriented programming? Wherever you do foo.blah(), you could just do foo_class__do_blah(obj)… so what do objects really grant you that you can’t do with basic procedural programming?
Why do you need Python’s function decorators? You can just manually copy and paste whatever logic you need into each and every function, instead of decorating them.
Why have an iterator protocol? Every datum can have it’s own bespoke way of iteration that you simply have to memorize on a case by case basis (and can not be abstracted over in any common way), why make everything conform to the same interface?
Why have anonymous functions that can close over their lexical scope (what people usually refer to as “closures”)? Consider:
let x = 0;
let inc = () => x++;
You could just write: function inc(bindings) { return bindings.x++; }
And then remember to pass along bindings whenever you need to call inc. Besides, that’s more or less what’s going on behind the scenes.I could keep going.
We have these facilities not because they are essential (go program in assembler, and you’ll see how few programming techniques and concepts your processor actually cares about), but because they are nice to have. They are nice to have because they provide us tools for abstraction, so by solving the general case, you solve a host of concrete cases.
Lenses let you abstract over data traversal and projection.
Maybe you don’t appreciate that, and that’s fine. There are C programmers who don’t mind writing 100 different hash tables for different keys and elements. If you are a Python/TS developer, I’m pretty sure you’d find that surprising — if you want a dictionary/hashtable, you just use a dict and call it done — you wouldn’t go build a new dictionary class for each and every key-type/value-type pair.
Different people have different tolerances for the drudgery of solving the same problem for the 100th time.
However: while I don’t understand why someone would choose vanilla ice cream if they’re given the option of chocolate, I wouldn’t act flabbergasted in such a scenario. Why would I? Why should I expect my desires to be the same as everyone else’s?
The only reason I can think of that someone would express shock at someone choosing vanilla over chocolate is if they think that the preference for vanilla indicates something is fundamentally wrong them. Or in other words: being a condescending bigot.
That’s kinda how you’re coming off.
In the event that you were actually asking in good faith: if you like to broaden your skills so you can finish programming work faster, I’d recommend reading up on lenses. If you don’t care about that, that’s fine too, but I’d recommend against expressing shock at the fact that others do care about these types of things.
I agree with the technical points in your post and disagree with its venom. It's important to notice patterns and abstract over them, and to teach people to spot commonalities they haven't yet noticed in their code. Next time I think you'll have better luck starting with the assumption of good faith and going from there.
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-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.