Hacker News new | past | comments | ask | show | jobs | submit login
Lenses: Composable Getters and Setters for Functional Programming (medium.com/javascript-scene)
89 points by ericelliott on Dec 23, 2018 | hide | past | favorite | 22 comments



I love lenses and other optics like prisms and folds, but only in a statically typed language like Haskell. It seems to me that it would be very hard to get things right in a dynamically typed language:

* First, it's possible that different elements of an array or different properties of an object has different shapes. I can't imagine how lenses can deal with that in an idiomatic way.

* Second, it's just difficult to get things right without a compiler's help. Forget a `traverse`? Now you are operating nonsense on the wrong level of your nested data structure. Trying to modify an inner structure but you only have a Fold instead of a Traversal? You won't know it won't work without running it to get some cryptic error.

* Furthermore, without types, we obscure the fact that setters and getters are unified: if `a` stands for the part and `s` for the whole, we just have

    (a -> f a) -> (s -> f s)
that is, given a way to transform some inner part, we can transform the whole as well. Getters and setters are just supplying a concrete choice of f, namely the const functor and the identity functor. If we pick f to be the identity functor, then with some unwrapping we have

    (a -> a) -> (s -> s)
so if there is a function that can modify the part, we have a function that can modify the whole by keeping the rest the same and modify that part. If we pick f to be the const functor, then with some unwrapping we have

    (a -> r) -> (s -> r)
so if there is a function that can extract the value from the part, we can extract the value from the whole as well. Without types, this rather elegant unification of getter and setter seems to be lost.


Everything you said has nothing to do with lenses and everything to do with trying to satisfy some statically-typed aesthetics.

> First, it's possible that different elements of an array or different properties of an object has different shapes

I can't imagine how this would introduce any sort of difficulties w.r.t. lenses that aren't already present with normal getting and setting of nested structures (aka probably not much).

> Second, it's just difficult to get things right without a compiler's help

This has nothing to do with lenses and everything to do with you preferring the feel of static types. You go on to talk about a bunch of issues with complex constructs that really only exist in static typing.

Sometimes you write a wrong thing and get a runtime error. This happens in all dynamically typed languages (and many statically typed as well).

> Furthermore, without types... this rather elegant unification of getter and setter seems to be lost.

Like ya do. I don't see how this has any impact at all on developing applications.


I use the lentes[1] library in Clojure all the time, doing all these things. With dynamic types lenses/prisms are the same thing, which I find nice. The library exposes a function called `id_setter` to go along with the core function `identity` to make use of the last bit of lens behaviour you describe.

I'm pretty sure it isn't types that are causing the aesthetic and usability differences you are seeing. I feel the same way about looking at js and typescript lens use. I thought it was just the result of using a concept from one paradigm in a language designed for another, which caused some awkwardness.

[1] http://funcool.github.io/lentes/latest/


> Furthermore, without types, we obscure the fact that setters and getters are unified.

This is way beyond what most people need to know to benefit from lens though.

Haskell itself has a counterexample: aeson-lens. It gives you lenses and prisms for working with JSON ASTs. Any unityped language could benefit from lenses for their unityped structures (JS and Clojure come to mind.)


Does unification of getter and setter have to be lost?

Assuming you are in a unityped context, hkts are gone, functor is not a thing. so

  (a -> f a) -> (s -> f s)
is not meaningful. If we pretend that unityped means everything is parametrically polymorphic, for the purpose of this discussion, can't we leverage parametricity to choose a representation that provides this unification?

for example, if i provide laws that show for a given

  lens :: (s -> a, s -> a -> s)
that

  (snd lens) s (fst lens $ s) == id s
doesn't that provide the same assurance, even if less elegant and not typechecked (the latter of which is much broader than just applicability of lenses)


In a language like C, the dot '.' and the arrow '->' are used for access into structures - both for setting and retrieving values. I think intuitively about a lens as a programmable dot which I can use to compose an access path. (You might have heard that the bind operator in a monad can be thought of as a programmable semicolon - this is similar.) This intuition might mean little if you haven't looked at lenses in more detail, but it does make sense once you have.


Though these operators can be used in modifiable lvalue expressions, while lenses are (according to this article) pure functions.


As pointed out in this thread, a lens has to return the modified object as it can't update it as a side effect. That is the reason no universal dot-like lens operator exists in Haskell but it needs to be composed based on the data structures that it targets.


Well, yes, but bear in mind that the absence of side effects is seen as a virtue in functional programming, and it is a point of view that I have considerable agreement with.


I recommend this post as well: https://medium.com/@gcanti/introduction-to-optics-lenses-and...

The author of the post is also the author of a TypeScript port of Monocle for Scala.


Has anyone done a performance analysis on the use of the lens concept in various languages? Given it is essentially duplicating data for every set it would seem to produce a lot of garbage for whatever memory allocator style is in use.


In haskell they're optimized out, but some delicate work has gone into ensuring that. They're actually faster than alternative methods in some cases, uniplate was rewritten in lenses and got faster and with better type safety as well.


In C you don't need lens, either you set a member of a struct or you copy the struct and you set a member of the copy, trivial stuff. Why do you need a library for this kind of thing in Haskell?


In Haskell, structures are immutable. You cannot set a member on a copy, because the copy is immutable too. You have to provide the new value to the constructor. The lenses are syntactic sugar to create a copy with only one member changed. It's not pretty nor efficient, but a result of having only immutable structures.


You make it sound like lenses are a sort of a crutch, but in my opionion they are not. The idea of having a first-class, composable way to “focus” on a part of a larger structure makes a lot of sense. And can be very pretty, too – see Swift’s key paths. That kind of type safety and simplicity is very appealing to me. As for efficiency, copying stuff usually means improved thread safety, so that’s win for me. Also, copy on write makes things often much more efficient in practice.


Is there a common idiom in C++ to deep copy a multi-level structure so you can then change a field? In C I know there isn't, and you can not create a generic function for it either.

How do you pass a member identification as a parameter to a function that may receive any kind of structure, and then update that member?

Haskell has record accessors too, that are more equivalent to the C functionality. Lenses are a different thing.


You are trying to apply concepts where the paradigm your are operating in (C or C++'s) doesn't solve the problem the same way.

In C++, you forward responsibility for knowing object shape down the line of composition. You also generally avoid deep copies because they are expensive. This leads to using getter/setter methods to abstract object shape at module boundaries. If you're dealing with immutable objects, then you bake that into the interface (such as std::string).

If you were deep copying objects in C++, first I would ask whether you really need to. If you really did, then the idiomatic way is to either provide a function like ".clone" or to implement the copy constructor to do it.

How do you pass a member identification as a parameter to a function that may receive any kind of structure, and then update that member?

You don't, because that's not something C++ as a language can do. So instead, the question is how you got into a situation in C++ where you need to.

On the point about composability, in C++ the composition happens at the object level. You don't compose getting/setting functions because that's not how C++ addresses the same problem.


> How do you pass a member identification as a parameter to a function that may receive any kind of structure, and then update that member?

You don't. You just update that member.


So, no generics available (templates are an option, but way to troublesome to use for that). That aligns with my experience, but mine is currently out of date, thus the question.


The main takeaway is that this isn't necessary with mutable structures. Nobody would think of even trying to write a generic solution to a problem that doesn't exist in C++.


Lens are about composition and abstracting your operations over unknown structures. I don't know where did you find anything about immutability, as plain data accessors are perfectly fine for that.

The motivation on the article is basically this paragraph:

"Lenses allow you to abstract state shape behind getters and setters. Instead of littering your codebase with code that dives deep into the shape of a particular object, import a lens. If you later need to change the state shape, you can do so in the lens, and none of the code that depends on the lens will need to change."

Immutability does not make its way there.


`set(lens, b, set(lens, store, a)) ≡ set(lens, b, store)`

Somehow i sense this code has arguments in the wrong order.




Join us for AI Startup School this June 16-17 in San Francisco!

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: