[HN Gopher] Immutability is not enough (2016) ___________________________________________________________________ Immutability is not enough (2016) Author : jt2190 Score : 115 points Date : 2021-06-26 14:40 UTC (8 hours ago) (HTM) web link (codewords.recurse.com) (TXT) w3m dump (codewords.recurse.com) | FpUser wrote: | IMHO this is atrocious waste of computer resources. Especially if | the stage gets big and must survive the duration of application. | To me it looks like crusade for the sake of idea anything else be | damned. | | There are multiple better ways to deal with the state. For | example more sane approach would involve single owner of the main | state which can serve / update particular slices of said state | and has the ability to broadcast state change events to | subscribers. | didibus wrote: | This is a common misconception, functional code is order | independent in its evaluation model, but if you are modeling | order dependent operations, it still lets you specify order for | them, and if you specify the wrong order for what you're doing | it'll do the wrong thing. | | So you can think of it as it solves accidental complexity, but | leaves essential complexity still for you to solve. | | A very simple example is: 30 - 2 * 10 | | This is order dependent, but it's part of the essential | complexity of the problem at hand. | | Now where accidental complexity would creep in is with something | like: position - 2 * position + 10 | | Now first we have the essential complexity, what order is the | behavior we want? Let's say here we want: | (position - 2) * (position + 10) | | In imperative we might do: position = 10 | position.minus(2) position.plus(10) | position.times(position.value) | | And hopefully you're already seeing the accidental complexity | created by the imperative approach. This doesn't work, and makes | no sense. It isn't just that the order of the operations we're | modeling is defined wrongly, in fact you could say it's defined | correctly, we want to substract, add and then multiply. But in | this case it's the mutable state itself which makes things more | complex then they need to be. | | So in imperative you'd have to do: position = | 10 firstPosition = position.value secondPosition | = position.value firstPosition.minus(2) | secondPosition.plus(10) position = | firstPosition.times(secondPosition) position.value | | That's a lot of added complexity due to us having to manage the | fact that the state is mutable. We need three memory locations, | we need to make sure that we copy the state values at the right | times and we need to make sure we are mutating the right memory | locations and than adding them back and all that ourselves. | | In functional you'd just do: position = 10 | times(minus(position, 2) plus(position, 10)) | | All you need to model is just the essential ordering inherent to | the behavior you want. | | Not surprisingly, most modern programming language actually use | pure functions for their math operations. That's why in Java: | int position = 10; (position - 2) * (position + 10) | | will work, because minus, plus and multiply are implemented as | pure functions, even in Java. That said, if you use the | imperative ones instead it won't work: int | position = 10; (position -= 2) * (position += 10) | | or if you went all imperative you'd have what I showed before: | int position = 10; position -= 2; position += 10; | position *= position; | | Now using math operations I think shows very clearly the | accidental complexity that imperative has over functional in this | case. What happens is all your other operations that are not | math, but related to your business logic or program logic that | you also model using the imperative style suffers from this added | complexity which is removed if you move to the functional style. | | Now back to the article, they're expecting functional programming | to somehow know the behavior they want, and magically figure out | the essential complexity of it. It won't give you that, but it | will get rid of a large amount of accidental complexity, and that | was the point of the "No Silver Bullet" paper, that since the | amount of essential complexity is fixed and inherent to your | problem, only accidental complexity can be simplified when | implementing said problem. Functional programming argues to | dramatically simplify your accidental complexity, letting you | focus all your attention on the essential parts. | | Having said that, there are things that tries to tame essential | complexity as well and functional programming tends to mix with | them very simply and easily. For example, the ideas around | declarative programming, metaprogramming, interactive | programming, static code analysis, and testable code all touch on | the essential complexity parts of a problem. | | If you have more independently reusable pieces, that you can | simply declare compositions of, or rules around them, it allows | you to tackle some aspects of essential complexity. | Metaprogramming lets you abstract things behind code-gen so you | can more quickly reuse parts of each essential features that is | the same. Interactive programming (like a REPL in Lisps or | smalltalk systems) will let you have a quicker feedback loop to | evaluate the effect of your essential problem and validate if | it's correct letting you more quickly implement the essential | complexity. Static code analysis can validate assertions of your | essential complexity and quickly tell you if you might have made | a mistake in it, it can also help you see a clearer mental map of | how the essential complexity is modeled which can help you | understand it better. Finally testable code, which functional | code often is by virtue of its style, similarly lets you assess | the validity of your essential behavior, and thus you can more | quickly iterate over your essential complexity. | z3t4 wrote: | Throw in in scale, dot, angle etc and the functional style | would be hard to reason about for me, I think your imperative | example is more explicit and you can sprinkle a bunch of | console log's between the lines. | | I think pure functional programming works best when there is | _no state_. So move the state out from the program. Only use | local buffers if you need for performance... | | The order of things is very important so make your coding life | easier by making sure the are no concurrent state - like TCP | for networking and single threaded business logic. | | For example in a chat app, buffer text in a textarea, but when | a user hit "send" don't render the message before it has been | received by the server, that way everyone connected to the | server will see the messages in the same order. | | The more possible state a program can have the more you will | benefit from testing. | didibus wrote: | You find this: position = 10 | firstPosition = position.value secondPosition = | position.value firstPosition.minus(2) | secondPosition.plus(10) position = | firstPosition.times(secondPosition) position.value | | easier to reason about then this: position | = 10 times(minus(position, 2) | plus(position, 10)) | | ? | | Are you sure? I think maybe you're just conflating syntax and | semantics. Syntax can be made in different ways either | functional or imperative, the semantics are what matters | here. | | Like would this be better for you (still functional, but | different syntax): position = 10 | firstPosition = position.minus(2) secondPosition = | position.plus(10) result = | firstPosition.times(secondPosition) | leoc wrote: | Today, on episode 42 of /Putting Things In-Band Doesn't Make Them | Go Away/ ... | chowells wrote: | This is an odd complaint. Immutability doesn't make order of | operations go away. (x+1)/2 is not the same operation as (x/2)+1. | | I find the tangent into effect systems at the end to be somewhat | ironic, given what it follows. Effect systems _also_ don 't make | order of operations not matter. The order in which effect | handlers are run can change the semantics of code using them. | | Pairs of operations don't commute in general. There's no way | around knowing which order things need to apply in. (x/2)+1 and | (x+1)/2 are just different operations. Nothing saves you from | needing to choose which one you mean. | manmal wrote: | Thanks for voicing this - in the system I work with (Swift | Composable Architecture), concurrently dispatched effects are | processed in the order in which they return the result of their | work. The scheduler processes effect results on a single | thread, so no real concurrency can happen there. If two | supposedly concurrent effects mutate the same substate, then | the outcome is (in theory) not even deterministic - the effect | returning from their work first will ,,win". | didibus wrote: | Exactly this ^ | | While effect systems are cool, they don't solve the problem | described here, which is just that you need to be clear about | the order of operations that are order dependent. | | The question would be, what programming style lets you most | simply describe the operations and their needed order? | | Functional programming is often also talked to as a type of | dataflow programming, and the two share a lot in common. A | dataflow approach in my opinion is best suited here, and | functional programming can easily be used in a dataflow style. | | The author's first example is actually pretty great, it defines | a pipeline of operations, the pipeline is a very clear way to | declare the order of operations. | | Another popular approach is instead of explicitly declaring the | order using dataflow constructs like a pipeline or a DAG, that | you define the dependencies on prior operations and data, and | the construct infers the order from those declared | dependencies. | dgb23 wrote: | I have to agree here. The author confuses what immutability is | about by implicitly extending to _control_ issues which are | orthogonal to immutability. The author describes the problem | very well but misuses the term "state" to describe "control". | | The solution to control issues is not having your code depend | on order. Logic programming, SQL, schema, type declarations, | regex... Many of us use these all the time but | logic/declarative programming in general is not the norm in | $dayJobLang. | oblak wrote: | That's only the second immutability (also from 2016) thread on | first page. | | What are the chances? | | Is immutability a new religion? | weego wrote: | I don't understand how the writer didn't realise somewhere during | writing this that they're conflating immutable state concerns and | higher-level state validation in logic loops. | | Collision detection validation and the game loop, to use the | example, has absolutely nothing to do with what kind of data | structure you're using, and is absolutely not what functional | programming was 'supposed to help us avoid'. | dkarl wrote: | I don't know if the author meant to, but he describes a problem | that people run into with concurrent systems, and which creates | a lot of real-life bugs if people don't anticipate it and | design for it. In the author's case, the solution is trivial | because it can be solved by specifying the order of updates, | but in concurrent systems that isn't always an option. Let's | say instead of Manuel the Carpenter's position on the screen, | we're updating the status of Monte the Money Launderer's | application for a line of credit. The CreditCheck system | receives an external credit report and updates Monte's | application state to indicate he has passed the credit check. | The Approval system sees the credit check result, sees that all | other conditions have been met, and updates the application | state to Approved. At the same time, a loan officer who has | received a call from the FBI about Monte uses the Control | system to put a manual Hold on the loan. If the concurrent | updates from the Approval and Control systems are combined | naively, the application can end up in the state of Approved | and Hold simultaneously. The loan officer, seeing that the Hold | has been successfully placed on the application, believes that | Monte has been blocked, but in fact Monte is able to access the | Approved line of credit. | | Like the problem described by the author, this is a pretty | basic problem that most HN readers would design for from the | start, but the title "Immutability is not enough" should | (theoretically) select for readers who are a little bit | surprised by it. | [deleted] | einpoklum wrote: | It's almost as if the author is telling us that "There is no | silver bullet". Now where have I heard that already? ... | | http://worrydream.com/refs/Brooks-NoSilverBullet.pdf | Aaargh20318 wrote: | While making the state immutable made it more visible what code | was affecting the state, the end result is still multiple pieces | of code directly affecting what is essentially a global state. | Sure, a copy is passed from one function to the next, but there | is still one 'current' state and everything is messing with it | directly. | | The real problem here is not the mutability of the state, it is | the _ownership_ of it. Who is responsible for keeping the state | internally consistent ? In this code the answer is: no one. | | To solve his problem, there needs to be a clear owner of the | state, and that code should be the only code directly affecting | the state and and be _responsible_ for keeping the state | internally consistent. | | Wether this 'owner' is a collection of functions that operate on | a global state in a language like C, or on a state passed in and | returned, or an object in an OO language, or whatever. Doesn't | really matter. | | For example. Moving the character and collision detection should | not be two separate function that affect the state but that can | be called separately (or in the wrong order) and keep the system | in an incorrect state. Only the code responsible for modifying | the state should do so, and it should guarantee to leave it in a | correct state on returning. Moving without collision detection | can leave the system in an incorrect state and thus should not | even be a function that exists. | | When designing a system this is always something I keep in the | back of my head: who is responsible for what ? Once you have that | clear, things become much easier. | MuffinFlavored wrote: | > Who is responsible for keeping the state internally | consistent ? | | What does this even mean? | | > The real problem here is not the mutability of the state, it | is the ownership of it. | | 1. Start with an initial state | | 2. Pass a copy of that initial state into a function, let it | return a slightly modified version of that initial state | | 3. Pass that slightly modified version into another function, | wash rinse repeat | | The only ownership that is happening that needs to be worried | about is the parent function passing a copy of state to a child | function for the duration of that call, right? | Aaargh20318 wrote: | > What does this even mean? | | It means that you need to concentrate the actual code that | manipulates the state and perform all state changes through | this code to ensure that the state is always correct. OO | solves this through the concept of encapsulation, but that is | just one way of doing it. | | If you have code all over the place that can manipulate the | state, then it becomes extremely hard to ensure that the | state remains valid. | | I'm not talking about the ownership of the particular | instance, I'm talking about what code is allowed to, and | responsible for, making alterations to state instances in | general. | | If I ask you what code can make changes to state, you should | be able to point to a small-ish part of your codebase and | say: only these functions can make alterations. Each of those | functions should guarantee that the state they return is a | valid state, that is: they are responsible owners. | | All functions that operate on the state have the main | responsibility to keep the state valid (e.g. no player | character inside an object). Each specific function has | additional responsibilities (e.g. move the character if | possible). | | In the example, the move function can take an existing valid | state, and turn it into an invalid state, the player can be | moved inside an object. So you when you think about what that | function's responsibility should be: it is to attempt to move | the main character to a new, valid position. If you think of | it like this, you quickly realise that the move and collision | detection functions should be combined. | | > The only ownership that is happening that needs to be | worried about is the parent function passing a copy of state | to a child function for the duration of that call, right? | | If you're passing a copy of state from one function to the | next, and every function can just modify it in whatever way | it wants to, they you basically have globals but with more | copying. | tcgv wrote: | > What does this even mean? | | He's referring to the Single-responsibility principle [1] | | [1] https://en.wikipedia.org/wiki/Single- | responsibility_principl... | saiojd wrote: | I feel like, what is confusing you is that the parent is | talking using broader/more general terms, rather than about | specifics i.e. the state as an object which you can pass | inside functions. His point, I believe, is that what causes | problems/bugs is often when partially invalid state is | _shared_ , not so much specific implementation details like | "are the modifications to the state visible because of | mutation or because a copy of the state is passed around | explicitly". | nijave wrote: | I think the parent post is suggesting a lack of cohesion. | E.g. the things changing state are sprinkled around in too | many different places--it makes it hard to reason about | | The solution really depends on the design pattern so the | message tends to be fairly vague. From an OOP perspective, | maybe more state modifiers should be instance methods are at | least belong in the same namespace | hypertele-Xii wrote: | But what language constructs are the most universally efficient | at expressing the distribution of that ownership responsibility | in a way that everyone can both understand and agree upon? | | Objects with getters and setters? | | Constant self-reflection? | | Serialization and schema? | | Complex query engines in smart databases? | | A political process of trust management? | | Or maybe just the soothing chaos of an evolving bio-electro- | mechanical planetary supersystem of expanding consciousness. | mikojan wrote: | In statically typed languages you declare x an unsigned 8-bit | integer. Everybody may write to x and still; you'd never | anxiously expect x to be anything but an unsigned 8-bit | integer. | | There is nothing wrong with "multiple pieces of code" "directly | affecting global state" if your business rules are encoded in | such a way. | Strs2FillMyDrms wrote: | I am super bad with terminology so I'll apologize beforehand. | | I've found that a good way to avoid this ownership conflict in | OO is to categorically prohibit any public accessors to | _inherited_ variables, be it at construction phase or later, be | it passively (via setters) or actively (via observers). | | And there should be only ONE provider of said value, also I've | found is sometimes better to have a hot spot where all nodes | converge and use it as a nursing node, and JUST THEN, fork this | nursing node into every let's say "logic gate requirement" node | (with a cached state each). | | This is a good approach IMO as long as these smaller nodes are | required by more than 2 observers, if not, then a simple | specialized observer is the way to go. | manmal wrote: | Can this be summarized as ,,Composition > Inheritance"? | rojoca wrote: | This is what state machines/charts are for. They prevent you | from entering invalid states, and take responsibility for all | changes in state. | hinkley wrote: | There are two (good) ways I know how to wrangle ownership of | information: where, or when. But in all of the sane systems I | know, at the end of the day it's really all "when". | | If there is no "where" for state alterations and they can | happen any time, then you are in full global shared stage | anarchy mode, which some people seem to be perfectly fine with. | | If the system of record is the source of authority then "when" | is at write time, regardless of _who_ does the write. | | If you know when the data was last altered, you can reason | about every interaction that happens "after" because what you | see is what you get. | | The smartest thing about Angular was that there was a layer of | the code - the services - that was expected to do all state | transformations on data from the server. Anything in your app | was "after" so you could trace the interactions by reading the | code. | | Plus, it was easier to convince the REST endpoint to do the | tranforms for you because you had a contiguous block of working | code that explained the difference between what you got and | what you wanted. A few sniffs at the data to determine if the | modifications had already been made was all the migration | strategy you needed. If the transform was cacheable upstream, | or found its way into the database, the more's the better. | | The upshot is that if you don't know a priori what information | a unit of work requires, then you don't have an information | architecture. And if you don't solve _that_ problem then you're | going to fall into a concurrency tarpit that often gets called | Cache Invalidation Hell, but that's just the dominant symptom. | dnautics wrote: | > But in all of the sane systems I know, at the end of the | day it's really all "when". | | Aren't databases basically "where"? | hinkley wrote: | Only if your logic is all stored procedures. It doesn't | matter what the database says if you mangle the read | operation. | hypertele-Xii wrote: | What if the database itself contained all business logic? | (but no plumbing) | | Every table has a function that is _the only function_ | that can write /insert into that table. The functions | themselves are just records in a specific table, which | the database schedules to run based on global access | activity (it prefers to prioritize functions that consume | more records than they produce, to keep space tight). | speed_spread wrote: | For a second I thought this was the title of a new James Bond. | orangepanda wrote: | No Mr Bond, I expect you to die(); | inbx0 wrote: | Licence to kill -9 | einpoklum wrote: | You mean kill -9 007 :-) | inbx0 wrote: | Why'd he kill himself though? Unless | elwell wrote: | Here's a little immutable + effects (Re-frame) game with some | physics I'm working on: https://github.com/celwell/wordsmith | | See the 'pipleline' of transformations applied here: | https://github.com/celwell/wordsmith/blob/0dff5446278b22a5b0... | lmilcin wrote: | I am little bit confused. Why do you expect immutability to solve | _all_ your problems? | | Immutability is a tool. Like every tool, it has its limits. Its | benefits are finite. Expecting it to solve _all_ problems is | silly -- there does not exist a single technique that can solve | all problems in every circumstances. | | Rather than looking for a silver bullet it is better to study | various techniques, their pros, cons and applicability and build | varied repertoire of solutions you know well enough to be able to | predict the results and achieve high chance of success. | | Don't discount techniques just because they are old and have | problems. If something was popular in the past it is likely it | has some merit to it -- try to understand it rather than dismiss | it because it is not new and shiny. | hypertele-Xii wrote: | > there does not exist a single technique that can solve all | problems in every circumstances. | | The Universal Algorithm. Wouldn't that be something. | solipsism wrote: | It's rather a non sequitur, as no one ever said immutability was | enough. | | You also need design, rigor, error handling, common sense, some | caffeine, testing, and fast builds. All together... that's | enough! | catlifeonmars wrote: | > fast builds | | Interestingly, immutability and fast builds are often closely | related, because immutability (in build systems) can be used to | ensure referential transparency, which makes it trivial to | implement caching of intermediate build artifacts. | NaturalPhallacy wrote: | I just want to point out that this is really funny to see at the | same time on hn. | | This article, and one from 7 hours ago: | | Immutability Changes Everything (2016) (acm.org): | https://queue.acm.org/detail.cfm?id=2884038 | | Posted here: https://news.ycombinator.com/item?id=27640308 | tyingq wrote: | Yes, but it's not a coincidence. Someone read | https://news.ycombinator.com/item?id=27640700 and thought it | was worth posting. | jt2190 wrote: | Reposting this, as it was mentioned by belter [1] in another | discussion on Immutability changes everything (2019) [2] and I | thought it was interesting enough for another visit. | | Previous discussion five years ago: | https://news.ycombinator.com/item?id=11388143 | | [1] https://news.ycombinator.com/item?id=27640700 | | [2] https://news.ycombinator.com/item?id=27640308 | JadeNB wrote: | > Reposting this, as it was mentioned by belter [1] in another | discussion on Immutability changes everything (2019) [2] and I | thought it was interesting enough for another visit. | | "Immutability changes everything" | (https://queue.acm.org/detail.cfm?id=2884038) also seems to be | from 2016. | cloogshicer wrote: | I don't understand the negativity in some of the comments. I | thought this was an excellent article. | | Two main takeaways for me: | | - While immutability can prevent some state-related bugs, it will | not prevent them if the state changes are not commutative | | - I especially liked this sentence: | | > This is actually very similar to the problem with imperative | languages - since everything runs sequentially, it's hard to see | what parts of the code have a true sequential dependency and | which parts do not. | edem wrote: | No, you also need persistent data structures. | rowanG077 wrote: | How is immutability supposed to help mitigate these bugs? This | seems like the programmer just not encoding the allowed state | change accurately. In the real world(tm) you should always update | the state either through functions that make sure you are not | reaching undesirable states or encode you state at the type | level. This is completely orthogonal to immutability though. | layoutIfNeeded wrote: | Tldr: a buggy imperative program can be mechanically translated | into an equally buggy functional program. | BoiledCabbage wrote: | The post while interesting - puts forward a bit of a false | argument. And the flaw is seen in the title: "Immutability is not | enough" | | Enough for what? | | If the author had finished the title, it'd be pretty clear that | what they're arguing is kinda obvious. | | "Immutability is not enough to solve every ordering / dependency | problem in programming" | | I'm trying to come up with the most charitable completion of the | title, but written out they all sound pretty patronizing. (maybe | someone else can do better than me?) | | Now all of that said. It is a great example of an article for | someone to see the benefits of transitioning from inline | imperative updates. To better factored code, immutable data and | explicit passing of dependencies. | | And then finally to introduce the motivation and benefits of an | effect system. | | For that I applaud the author. ___________________________________________________________________ (page generated 2021-06-26 23:00 UTC)