10 ms·
Lenses
- kqr 4y agoWhat's really neat about lenses to me (more of a practitioner than a theoretician) is that while the level 0 explanation of them is "they're like getters and setters in your OOP language" they extend the concept much further. Lenses can be "indirect" or "pseudo" getters or setters, for want of a better term. I.e. they can filter among many fields to set, then modify multiple fields at once, change only non-null values, replace only null values, and so on. So in a sense they're like multiple cursors at the code execution level. But they can also contain more complicated transformations if that makes your code simpler (sort of like C# properties.) They are also first class concepts meaning you can pass these accessors around without caring about which object you will use them on. And they compose with each other very nicely. Those three properties make them super powerful and I really with I had them in more languages. ---- Lenses are the sort of thing where if they had been the default in more languages, people would wonder why on earth anyone would want to have the obviously inferior getters and setters of today's OOP languages.
- hither_shores 4y ago> I.e. they can filter among many fields to set, then modify multiple fields at once, and so on. These generalizations are usually referred to as "optics", with "lenses" reserved for the single-target case.
- kqr 4y agoYet ekmetts package is called "lens" and not "optic". While I agree with you in principle, I think that ship has sailed and for all practical purposes, "lenses" has come to mean both the narrower definition that is arguably correct, and optics more generally. It's a historical accident, much like a lot of people use "function" to mean all sorts of subprogram, including procedures and sometimes even methods!
- nerdponx 4y agoI would love to see some kind of table or diagram that shows all the various "optics" in some kind of structured format. Lenses are the only one that I can keep straight in my head.
- endgame 4y agohttps://raw.githubusercontent.com/wiki/ekmett/lens/images/Hierarchy.png https://raw.githubusercontent.com/wiki/ekmett/lens/images/Hi... If you need to find it again, it shows up in the README which is in the github repo and the page on Hackage.
- hither_shores 4y agoThere are infinitely many, so all is out, but the standard ones are covered here: https://oleg.fi/gists/posts/2017-04-18-glassery.html https://oleg.fi/gists/posts/2017-04-18-glassery.html Page 10 of this paper has a table with references to some more exotic optics, though it's going to be hard to parse without some category theory: https://arxiv.org/pdf/2001.07488.pdf https://arxiv.org/pdf/2001.07488.pdf
- Tarean 4y agoThe optics library is a bit stricter, using separate types with implicit conversions to give better type errors, and the subtyping diagram is here https://hackage.haskell.org/package/optics-0.4.2/docs/Optics.html#g:3 https://hackage.haskell.org/package/optics-0.4.2/docs/Optics... The lens documentation has this diagram https://i.imgur.com/ALlbPRa.png https://i.imgur.com/ALlbPRa.png These both skip over indexed versions, and some niche optics like grates aren't in the libraries yet afaik
- sfvisser 4y agoAnd you can weave effects into them just like normal functions, without losing the ability of composition. Fun stuff! In defense of OO-style access pattern, they’re a lot simpler and syntactically well integrated into most languages. The flexibility of lenses is clearly visible in types (in most current implementations). Ideally you want to hide this when you don’t need the flexibility.
- jiggawatts 4y agoNotably, this feature would be very useful in SQL or similar data processing languages. I wish these query languages had the ability to select "all key columns", "all writeable columns", "non-system-generated columns", etc... In most database or query platforms this is either impossible, or requires breaking out into another system/approach entirely. For example, it's possible to generate a SQL query to do above by querying the column metadata from INFORMATION_SCHEMA views, but this is obviously not what most people want to be doing when writing a query. Worse, even if the database platform has features like this, when it gets processed through an ORM in typical languages like Java or C#, any such features are once again lost... Lenses and optics could apply well to SQL-like query languages, because those are largely functional as well, and thanks to "JSON columns" often have nested structures where the composability of these abstractions would be more elegant than some hideous "tree query syntax" as typically seen in XPath and the like.
- avgcorrection 4y ago> What's really neat about lenses to me (more of a practitioner than a theoretician) is that while the level 0 explanation of them is "they're like getters and setters in your OOP language" they extend the concept much further. This doesn’t tell me anything since getters/setters are almost useless in my experience.
- fho 4y agoRight ... And lenses are not. Imho a better comparison would be to (safe) pointers that can point to multiple, conditional positions in a data structure. Example: in a game you want to update the health points of all enemies that are affected by an area of effect attack. With lenses you would compose a lens from multiple simpler lenses: ```enemiesAtDistanceToPoint pos radius = enemies . filtered (distanceTo pos . (< radius))``` and then you could modify those enemies: ```enemiesAtDistanceToPoint (10,20) 5 . health -= 5``` The benefit here is that you abstract the work of extracting the enemies and can reuse it elsewhere. Also note the lack of any loop to affect multiple entities.
- c-cube 4y agoWould multiple-cursors editing be an apt comparison? :-)
- fho 4y agoSomewhat, but even my pointer analogy just scratches the surface. Filtering/selecting is big, but another interesting thing is that there are lenses (which I think are called prisms?) that only provide a view of something. Eg you have a text document in Unicode, select all words marching a certain pattern, and then look as if they were encoded in ASCII. (No idea why this would useful :-) ).
- hither_shores 4y agoA "lens" that only provides a view of something is just a plain function. Prisms are first class constructors: a `Prism a b` can build a `b` from an `a`, or attempt to match on a `b` and `Maybe` get an `a` that could construct it.
- BrainVirus 4y agoGetters and setters are not a "concept" in OOP. They're just a naming convention that is occasionally built into language syntax for brevity. FP people constantly miss something fundamental about OOP. Objects only exist at runtime. Any facility that provides powerful tools to deal with objects has to be runtime-oriented and would require corresponding language support. E.g. Mirrors: https://bracha.org/mirrors.pdf https://bracha.org/mirrors.pdf
- hither_shores 4y agoThey're using "getters and setters" to refer to reading from and writing to fields, not `get => ... ; set => ...` style syntactic sugar.
- misja111 4y agoGreat article. But I'm wondering, how many of you have used Lenses in larger, or legacy projects? It is this sentence that gives me some doubts: > The formulation is wonky, and very difficult to grasp. Don't worry if the intuition hasn't kicked in. It turns out that you can use lenses quite a bit without fully grokking them. Whenever I read something like this about a framework, I imagine that sooner or later some colleague will fall in love with the power of the framework too much and write code that is very hard to understand/maintain. But I have to admit that I haven't worked much with Lenses myself yet so maybe things are not so bad?
- kccqzy 4y agoI have. Like many other concepts in Haskell, it takes a bit of time for intuition to kick in. For example you likely wouldn't understand Traversable or Applicative when you first heard of it but it will become second nature in a few days. The only rule I would have is to ban all the operators and force everyone to use actual words for names. For example (^.) is not allowed but `view` is.
- pmoriarty 4y ago> The only rule I would have is to ban all the operators and force everyone to use actual words for names. For example (^.) is not allowed but `view` is. Yeah, this is what I view as a enormous weakness of Haskell compared to Lisp/Scheme, where in the latter descriptive naming is a community norm. It makes for much more readable code... especially if I've been away from the language for a while.
- rssoconnor 4y agoDo you also consider v^.x hard to read when it is Pascal?
- HelloNurse 4y agoPascal has a much smaller number of much simpler operators than Haskell + fancy libraries, which puts a low ceiling on the incomprehensibility of Pascal expressions.
- the_duke 4y agoLenses let you do some amazing things, but I think most of their existence is just thanks to how truly horrible (nested) record field access and updates were. This is a lot better now with record dot syntax. [1] [1] https://ghc-proposals.readthedocs.io/en/latest/proposals/0282-record-dot-syntax.html#record-dot-syntax https://ghc-proposals.readthedocs.io/en/latest/proposals/028...
- ParetoOptimal 4y ago> Lenses let you do some amazing things, but I think most of their existence is just thanks to how truly horrible (nested) record field access and updates were. Only for the most basic uses. Architecting programs as folds or traversals can be powerful. One simple example of lenses i can think of being useful for more than record access is: > sumOf (folded . folded) [Just 1, Just 2] 3 More are in this presentation: https://m.youtube.com/watch?v=QZy4Yml3LTY https://m.youtube.com/watch?v=QZy4Yml3LTY
- WJW 4y agoNot to be a lens hater, but I would definitely flag it in PR review if someone came up with that instead of something like this: > sum $ map (fromMaybe 0) <some list of Maybe Int> Possibly with a different default value for Nothings. A lot of the optics libraries seem to focus a lot on making the general case as easy as possible while overcomplicating the specific case. `sumOf (folded . folded)` does have some advantages in working for lists of other types but I question how often you would reuse such a function for different types the first place. It just doesn't come up all too often in my own code, perhaps it is different elsewhere.
- ParetoOptimal 4y ago> `sumOf (folded . folded)` does have some advantages in working for lists of other types but I question how often you would reuse such a function for different types the first place. The common refactor here would be to Either: > sumOf (folded . folded) [Right 1, Right 2] 3 I don't like the "unwrap the Maybe" or "get rid of the pesky monad in my way" intuition that fromMaybe and catMaybes give beginners. > A lot of the optics libraries seem to focus a lot on making the general case as easy as possible while overcomplicating the specific case. I kind of agree with this, but contend the universality of a single powerful and composable lens syntax simplifies things more overall even if it makes a single use site more complicated.
- m4lvin 4y agoFunny coincidence. Yesterday I watched the talk by SPJ about Lenses and it finally clicked for me :-) https://skillsmatter.com/skillscasts/4251-lenses-compositional-data-access-and-manipulation https://skillsmatter.com/skillscasts/4251-lenses-composition... Maybe someone has an actual public link to this with no login needed? edit: thanks for the subcomment! https://www.youtube.com/watch?v=k-QwBL9Dia0 https://www.youtube.com/watch?v=k-QwBL9Dia0
- hither_shores 4y agohttps://www.youtube.com/watch?v=k-QwBL9Dia0 https://www.youtube.com/watch?v=k-QwBL9Dia0
- chombier 4y agoThank you so much, watching that talk finally made it click for me as well!
- chowells 4y agoThis is such a good presentation. The story it tells of the derivation of the type is ahistorical, but it makes a lot of sense. If you are looking for an intuition of how the types work, it's just fantastic.
- malft 4y agoThe type looks a little insane, but it's just a combination of three mundane hacks that also exist in other languages. The first: If you need a serializer/deserializer in a simple language like old-school Java, you can either write two methods for each class, or you can combine them into one method like this: class HoveringSkull inmplements Streamable { String desc; float height; void stream(Stream c) { desc = c.stream(desc); height = c.stream(height); } ... and have two different implementations of Streamable. That's what the choice of (pro-)functor instance does -- you can swap out the implementation to get 'read' and 'write' (and 'deepClone' and 'equals' and 'hashCode' and 'defaultGui' and 'treeMatch' and ...)
- sriram_malhar 4y agoGood God! This kind of stuff is built for a very different kind of brain than mine, that's for sure. I always come away feeling that Haskell makes the simple things tough, and the tough things beyond my reach. For me, it seems so much simpler to deal with in-place single-writer mutation, where aliasing is controlled by the type system, ala Rust or D, or, something like Elm's record updating syntax. Even Erlang will do in a pinch. I hold Haskellers' abilities in high regard, but at the end of the day, I'm not sure if they produce better software than other equally capable/rigorous/knowledgeable developers. (Don't mean to dredge up the static/dynamic typing debate, but given the literature I have read, there is no clear winner).
- 4ad 4y ago> This kind of stuff is built for a very different kind of brain than mine, that's for sure. No it isn't, and this attitude doesn't do you any good. This stuff only seems difficult because it's unfamiliar, but I can assure you it's simpler than whatever "real life" technology you are proficient in. FP concepts are very simple (so very easy to understand!), the real power comes from their true compositionality, which is also why you don't need a "special brain" to understand what is happening -- concepts compose!
- sriram_malhar 4y agoAt a 50000 foot level, I will accept your statement. Concepts composition is a delightful property. But at a lower level, it is a slog. I have tried somewhat hard (e.g understanding different monads, writing a tutorial --- doesn't everybody?) to plough through the language and the libraries, but after some point I got tired having to learn one more new concept, one more language extension, all for doing fairly routine things. Now, I'm reasonably confident that I _could_ master it if my life depended on it, but it feels so .... tedious. > it's simpler than whatever "real life" technology you are proficient in. That has certainly not been my experience, alas. I'm constantly unsure of whether a piece of code is idiomatic enough, abstract enough or whether it is a simple composition of some other concepts. I suppose there is a particular hump beyond which these things click into place, but I haven't come close to it. Secondly, producing any real life technology requires me to fit that tech and Haskell bindings (should they exist) together in one brain. That said, Haskell's been a wonderful fit for writing a compiler for a pet toy language. And monads changed the way I write code in mainstream languages.
- norswap 4y agoI'm kind of befuddled as to why the original "Modifier functions" solution is not better than the "And now we can finally unify our getter and modify lenses into one:" solution.
- yakshaving_jgt 4y agoYou can do it, but it becomes tedious to write all that boilerplate as your data model grows.
- ufo 4y agoDoes anyone know what's the situation with lenses and type errors these days? Many moons ago, when I played around with lenses, I remember that the type error messages were could get very scary. Paragraphs of inscrutable type errors for a single typo, something that could make even C++ template errors look nice.
- chowells 4y agoThere are a couple type errors that are weird, like complaining about missing Monoid instances when you try to use a Fold as if it was a Getter. A few other things can have error messages that only make sense if you ignore the weird type aliases and focus instead on their expansions. There is now an alternative library, optics, which does a lot to improve error messages at the expense of removing extensibility. Since the ecosystem (in terms of types and operations) is closed, it can be very precise in the error messages it produces. It's a good compromise if you're more worried about error messages than some of the less common lens operations. (I tend to stick with lens, as I am comfortable with its error messages and often find myself using things optics doesn't support. But the option is there for those who have different needs.)
- ParetoOptimal 4y agoTry optics. See their error message comparison: https://hackage.haskell.org/package/optics-0.4.2/docs/Optics.html#g:4 https://hackage.haskell.org/package/optics-0.4.2/docs/Optics...
- Rumudiez 4y agoFor anyone looking to play around with lenses outside of Haskell, Ramda JS has a great implementation along with many other FP utilities. Been a happy user for a few years now with several production use cases, all still going strong
- infogulch 4y agohytradboi 2022 hosted a 10m talk [1] about Project Cambria [2] which imagines bi-directional lenses as the basis for deep compatibility between software systems across time and domain. [1]: https://www.hytradboi.com/2022/cambria-schema-translations-in-distributed-systems-using-bidirectional-lenses https://www.hytradboi.com/2022/cambria-schema-translations-i... [2]: https://www.inkandswitch.com/cambria/ https://www.inkandswitch.com/cambria/
- okasaki 4y ago> alice.address.city = "Los Angeles"; > The first issue in Haskell is that we can't mutate alice; we instead have to return a new Person value with the updated city. I don't get it. Ok, so it can't mutate, but why can't haskell have an easy syntax and just return a copy with the updated value? Why does it need this very convoluted way for updating records? eg you could have {{ }} denote an "update block", and this has some special syntax to change values, add or remove items from lists/dicts and similar let new_alice = {{ alice.address.city = "Los Angeles", alice.cart.items += 123 }}
- cercatrova 4y agoThis is basically what Immer [0] does for Javascript apps if you use immutable-like libraries like Redux which expect a new object created every time a property changes so that it can detect that it's changed and propagate that to any observing components. Perhaps Haskell needs something similar. [0] https://github.com/immerjs/immer https://github.com/immerjs/immer
- hither_shores 4y agoThat's exactly what lenses do: let new_alice = set (address . city) "Los Angeles" $ over (cart . items) (+ 123) alice
- grumpyprole 4y agoYes it could. But lenses offer much more than that. They are sometimes called "functional references", because a lens can be used for both reading and updating; and can be passed around. They can also be composed and combined in arbitrarily complex ways, e.g a lens into a map, option or a list.
- operator-name 4y agoIt does, but was only introduced more recently: https://ghc-proposals.readthedocs.io/en/latest/proposals/0282-record-dot-syntax.html#record-dot-syntax https://ghc-proposals.readthedocs.io/en/latest/proposals/028... Lenses are still more powerful as they can be composed. Consider your (and the record dot) syntax for having to modify alice.cart.items on one branch of the conditional and alice.cart.discount on another branch of the conditional.
- nephanth 4y agoThis is the kind of operations that make me like ocaml's down to earth approach let new_alice = {alice with address=new_address}