Remix.run Logo
solid_fuel 3 days ago

I haven't encountered this pattern before. Is there some more information on what problems this is designed to solve?

dkarl 3 days ago | parent | next [-]

The problem most programmers would be familiar with is making an update inside a deeply nested immutable data structure. For example, suppose you want to update a user's billing address, and you have an immutable data structure that looks like this:

    user { billingInfo: { card, address }, name, subscription { level, expiration }, status { level, since } }
The structure is immutable, so you can't make the update in place. On the other hand, you're only changing one field, so it would be wasteful to make a complete deep copy. The efficient way to create an updated instance is to create a new user instance and a new billingInfo instance while reusing the name, subscription, and status instances.

You can think of this as the equivalent of a setter for immutable data structures.

This is an artificial example, because the cost of making a deep copy of this user structure is probably not that bad, and the boilerplate to make an efficient update is not all that bad, either. You would use an optics library when 1) you need efficient updates and 2) it's worth investing a little effort to hide the boilerplate.

Optics also let you concisely express access into a deeply nested structure, the getter paired with the setter. In my experience, updates are the motivation for setting up optics, and concise access is a nice thing you get as a bonus.

afandian 2 days ago | parent | next [-]

Is this much different from Clojure’s `update-in` ? You just express a path as a sequence of keys / indexes, and a function to apply, and let the persistent data structure get updated.

It's very neat. But depends on a type system built for this kind of use.

https://clojuredocs.org/clojure.core/update-in

usrusr 3 days ago | parent | prev [-]

Does it offer creating a mutable view on top of the immutable structure that can be used to accumulate a whole set of changes to later be materialized into a copy with the changes? (or to be used directly, at whatever cost would be required) That's something I've been wondering why it's not more of an established thing. It would basically be docker, but for in-memory reference graphs instead of filesystems.

wk_end 3 days ago | parent | next [-]

Immer does this for JS/TS.

https://immerjs.github.io/immer/

munchler 3 days ago | parent | prev | next [-]

You are perhaps looking for software transactional memory.

https://en.wikipedia.org/wiki/Software_transactional_memory

lmm 3 days ago | parent | prev [-]

You can compose together a bunch of edit operations (an edit command, if you like) and apply them at once at the end, is that what you mean?

usrusr 2 days ago | parent [-]

Kind of. Can you use the interim state fork (original plus changes, not yet committed into a new immutable) as a "readable" type, the union of mutable and immutable variants? That would resolve choice dilemmas like the decision between mutable and immutable builders: keep the mutable where it is only used by the creating thread, materialise into an immutable when it needs to cross thread boundaries, fork where a thread needs to adapt a little and have all consumers typed to the readable version so that they can work with either, without requiring a new immutable.

lmm 2 days ago | parent [-]

> Can you use the interim state fork (original plus changes, not yet committed into a new immutable) as a "readable" type, the union of mutable and immutable variants?

Not really, not at a language semantics level - although in practice if you applied your current mutation to the original object, did something with the result, and then threw the "partially edited object" away, the JVM's escape analysis might catch that and never fully materialise the "partially edited object". Note that nothing is really mutable, not even your edit operations - you form your combined edit by composing together a bunch of smaller edits, but all those edits are immutable.

usrusr 20 hours ago | parent [-]

Thanks for your patience. I guess the big blocker for the hypothetical "full triangle of readable-mutable-immutable" is that deep read operations would have to create a potentially long lived object for each intermediate step in e.g. a getter chain if you want to avoid special syntax. And then you're back to extreme reliance on escape analysis. And at some point even correctly identified nonescaping allocations start summing up.

Nullabillity 3 days ago | parent | prev | next [-]

If monads are programmable semicolons (ways to chain operation), lenses are programmable dots (ways to delegate access to data). Other optics are largely generalizations of that pattern.

AlotOfReading 3 days ago | parent | prev | next [-]

Lens are the functional version of getters and setters. A lens takes a product type (struct, class, etc) and allows you to view or update part of it. Prisms are something similar for sum types (variants) that allow you to look at a value if it's present and err otherwise.

The optical analogy comes from how these operations resemble zooming in on structures with a magnifying glass and the entire family of related transformations is called optics.

kqr 2 days ago | parent | prev | next [-]

Aside from making it convenient to build getters and setters for nested data, optics also have conditional accessors and collection accessors as building blocks.

Conditional accessors have since become a popular language feature with e.g. the elvis operator ?. in C#. Optics make it possible to innovate on features like that in library code rather than as language features.

Something I've yet to see made into a language feature that is common in optics libraries are iterating accessors. E.g. to reset all counters we can, with optics, say something like (in Haskell syntax, since that is what I know)

    stats.each.count .= 0
and that sets the count to zero for all objects in the stats array. If not all stats objects have a count (some might be of a gauge type, for example) we can compose in a conditional accessor that resolves the count field only if it is not null:

    stats.each.count._Just .= 0
In the above statement, nothing is language syntax --it's all library functions and operators. But it still combines really well on the page. Once one knows optics, one rarely has to think very hard about how to do any get or update operation, even when the data types become complicated.
62951413 2 days ago | parent | prev | next [-]

There's one book that discusses a few related questions from a very mainstream perspective: Functional and Reactive Domain Modeling (https://www.manning.com/books/functional-and-reactive-domain...). The emphasis here is on practical while most similar resources tend to go too hard in the Haskell-on-the-JVM direction.

jeremyjh 3 days ago | parent | prev [-]

Lenses make it easier to read and update members deep in a hierarchy of read-only data structures.