[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)