6 ms·
I have to admit I don’t really understand the point of doing this instead of just obj.a = 2 or whatever.
by binary132 11mo ago
I have to admit I don’t really understand the point of doing this instead of just obj.a = 2 or whatever.
- aap_ 11mo agoImmutability is a central concept in functional programming.
- dullcrisp 11mo agoYou can uhhh abstract over the property which seems cool if you’re into abstracting things but also probably shouldn’t be the thing you’re abstracting over in application code. Or on second look the sibling comment is probably right and it’s about immutability maybe.
- laszlokorte 11mo agoSay you want to do obj.child.foo[3].bar += 2 but without mutation, but instead all the data is immutable and you need to do a deep copy along the path. Lenses are an embedded dsl for doing this via syntax that reads similar to to the mutable variant. Additionally it allows to compose many of such transformations.
- o11c 11mo agoThis is equivalent to that for people who are irrationally terrified of mutability, and are willing to abandon performance.
- BoiledCabbage 11mo agoIt's a similar idea to map() but for more complex objects than arrays. When people use "map" in Javascript (or most any other language that supports it) do they do so because "they are terrified of mutability, and are willing to abandon performance?" Your comment reads like the response of someone who is struggling to understand a concept.
- o11c 11mo agoOnly the get half is `map`-like. In combination it's more like a property descriptor, which is far easier to understand and much more efficient. And, if it wasn't obvious, it's only the `set` half where lenses suck for performance.
- pasteldream 11mo agoImmutability gives you persistence, which can be practically useful. It’s not just fear.
- leiroigh 11mo agoYes. O(1) snapshots are awesome! Persistent datastructures are a monumental achievement. But that comes at a performance price, and in the end, you only really need persistent datastructures for niche applications. Good examples are: ZFS mostly solves write amplification on SSD (it almost never overwrites memory); and snapshots are a useful feature for the end user. (but mostly your datastructures live in SRAM/DRAM which permit fast overwriting, not flash -- so that's a niche application) Another good example is how julia uses a HAMT / persistent hash-map to implement scoped values. Scoped values are inheritable threadlocals (tasklocal; in julia parlance, virtual/green thread == task), and you need to take a snapshot on forking. Somebody please implement that for inheritable threadlocals in java! (such that you can pass an O(1) snapshot instead of copying the hashmap on thread creation) But that is also a niche application. It makes zero sense to use these awesome fancy persistent datastructures as default everywhere (looking at you, scala!).
- majoe 11mo agoCounterintuitively Julia recommends the use of immutable data types for performance reasons, because immutability enables more flexible compiler optimisations An immutable variable can be savely shared across functions or even threads without copying. It can be created on the stack, heap or in a register, whatever the compiler deems most efficient. In the case, where you want to change a field of an immutable variable (the use case of lenses), immutable types may still be more efficient, because the variable was stack allocated and copying it is cheap or the compiler can correctly infer, that the original object is not in use anymore and thus reuses the data of the old variable for the new one. Coming from the C++ world, I think immutability by default is pretty need, because it enables many of the optimisations you would get from C++'s move semantics (or Rust's borrow checker) without the hassle.
- leiroigh 11mo agoThere is nothing counter-intuitive or julia-specific about it: Fastest way is to have your datastructure in a (virtual) register, and that works better with immutable structures (ie memory2ssa has limitations). Second fastest way is to have your datastructure allocated on the heap and mutate it. Slowest way is to have your datastructure allocated on the heap, have it immutable, copy it all the time, and then let the old copies get garbage collected. The last slowest way is exactly what many "functional" languages end up doing. (exception: Read-copy-update is often a very good strategy in multi-threading, and is relatively painless thanks to the GC) The original post was about local variables -- and const declarations for local variables are mostly syntactic sugar, the compiler puts it into SSA form anyway (exception: const in C if you take the address of the variable and let that pointer escape). So this is mostly the same as in every language: You need to learn what patterns allow the current compiler version to put your stuff into registers, and then use these patterns. I.e. you need to read a lot of assembly / llvm-IR until you get a feeling for it, and refresh your feelings with every compiler update. Most intuitions are similar to Rust/clang C/C++ (it's llvm, duh!), so you should be right at home if you regularly read compiler output. Julia has excellent tooling to read the generated assembly/IR; much more convenient than java (bytecode is irrelevant, you need to read assembly or learn to read graal/C2 IR; and that is extremely inconvenient).
- ssivark 11mo agoThe difference doesn't matter when you have a shallow structure and can access fields directly and have a few lines of code. But field access does not compose easily if you have a nested hierarchy of objects. Your natural choice in the "OOP style" is to write a lot of boiler plate to point to each different field you want to get/set. Say you get bored of the tedium and want "higher-order" accessors that compose well -- because ultimately all look-up operations are fundamentally similar in a sense, and you only need to write traversals once per data structure. Eg: Instead of writing yet another depth-first search implementation with for loops, you could easily tie together a standard DFS implementation (traversal) from a library, with accessors for the fields you care to work with. One way to think of the goal of functional paradigm is to allow extreme modularity (reuse) with minimal boilerplate [1]. The belief is minimal boilerplace + maximum reuse (not in ad-hoc ways, but using the strict structure of higher-order patterns) leads to easily maintainable bug-free code -- especially in rapidly evolving codebases -- for the one-time cost of understanding these higher-order abstractions. This is why people keep harping on pieces that "compose well". The emphasis on immutability is merely a means to achieve that goal, and lenses are part of the solution to allow great ergonomics (composability) along with immutability. For the general idea, look at this illustrative blog post [2] which rewrites the same small code block ten times -- making it more modular and terse each time. [1] https://www.cs.kent.ac.uk/people/staff/dat/miranda/whyfp90.pdf https://www.cs.kent.ac.uk/people/staff/dat/miranda/whyfp90.p... [2] https://yannesposito.com/Scratch/en/blog/Haskell-the-Hard-Way/ https://yannesposito.com/Scratch/en/blog/Haskell-the-Hard-Wa... Once the language is expressive enough to compose pieces well and write extremely modular code, the next bit that people get excited about is smart compilers that can: transform this to efficient low-level implementations (eg. by fusing accesses), enforce round-trip consistency between get & set lenses (or complain about flaws), etc.
- pasteldream 11mo ago> Your natural choice in the "OOP style" is to write a lot of boiler plate to point to each different field you want to get/set. Your natural alternative to lenses in imperative languages is usually to just store a reference or pointer to the part you want to modify. Like a lens, but in-place.
- electroly 11mo agoThe other replies covered the answer about immutability well, but I have the further question: why isn't this built into languages as syntax sugar, so that OP's suggested line would work with immutable structures? As a dilettante at programming language design, I have my own toy language. It uses exclusively immutable data structures (C++ "immer"). I present it to the programmer as simple value semantics. `obj.foo[5].bar.a = 2` works and sets `obj` to a new structure where the path through `foo`, `bar`, to `a` has all been rewritten. Since I put it in as a language feature, users don't have to learn about lenses. Why isn't this technique more common in programming language design? Is it so offensive that the syntax `obj.a = 2` ends up rebinding `obj` itself? The rule in my language is that assignment rebinds the leftmost "base" of a chain of `.` field and `[]` array index accesses on the LHS. I'm ignorant of the theory or practical consideration that might lead a competent designer not to implement it this way.
- lioeters 11mo agoIt's an interesting question, why immutability is not built into more languages as the default, so that the most intuitive syntax of assignment produces new values. Without having any expertise in the matter, I'd guess that mutability has the advantage of performance and efficient handling of memory. obj.foo[5].bar.a = 2 An immutable interpretation of this would involve producing new objects and arrays, moving or copying values. Another possible advantage of the mutable default is that you can pass around references to inner values.
- electroly 11mo agoThat's always the case with immutable data structures; this assignment syntax didn't create that problem. If you used lenses to write 2 into "a", and you expected to get back a new "obj", you would still need to produce all those new objects and arrays. That's just immutable data structure stuff. I'm only asking about the assignment syntax here.
- WorldMaker 11mo agoOne of the reasons functional languages like to make mutable code look very different from immutable code is to try to make it clear which is which when reading the code, to help avoid mistakes. > Is it so offensive that the syntax `obj.a = 2` ends up rebinding `obj` itself? That does imply that the `obj` binding itself is mutable, so if you are trying for entirely immutable data structures (by default), you do probably want to avoid that. This is why the syntax sugar, in languages that have been exploring syntax sugar for it, starts to look like: let newObj = { obj with a = 2 } You still want to be able to name the new immutable binding.
- endgame 11mo agoLenses also let you take interesting alternate perspectives on your data. You can have a lens that indexes into a bit of an integer, letting you get/set a boolean, for example.