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