[HN Gopher] Serialization Is the Secret
       ___________________________________________________________________
        
       Serialization Is the Secret
        
       Author : borromakot
       Score  : 250 points
       Date   : 2024-09-29 13:26 UTC (3 days ago)
        
 (HTM) web link (www.zachdaniel.dev)
 (TXT) w3m dump (www.zachdaniel.dev)
        
       | fracus wrote:
       | This was very enlightening for me on the subject of immutability.
        
       | mrkeen wrote:
       | > I would argue that, from the perspective of our program, it is
       | not more or less mutable than any other thing. The reason for
       | this, is that in Elixir, all mutating state requires calling a
       | function to observe it.
       | 
       | Are you never not inside a called function?
       | 
       | This just sounds like pervasive mutability with more steps.
        
         | colonwqbang wrote:
         | It sounds like all old bindings to the value stay the same. So
         | you have a "cell" inside which a reference is stored. You can
         | replace the reference but not mutate the values being referred
         | to.
         | 
         | If so, this sounds a lot like IORef in Haskell.
        
         | finder83 wrote:
         | The functions don't return a mutable version of a variable or
         | anything. You still get an immutable copy (it may not be an
         | actual copy, I don't know the internals) of the state, and the
         | state he's referencing in a Genserver is the current state of a
         | running process that runs in a loop handling messages. For
         | example in liveview, each connection (to an end-user) is a
         | process that keeps state as part of the socket. And the editing
         | is handled through events and lifecycle functions, not through
         | directly mutating the state, so things tend to be more
         | predictable in my experience. It's kind of like mutation by
         | contract. In reality, it's more like for each mailbox message,
         | you have another loop iteration, and that loop iteration can
         | return the same value or a new value. The new values are always
         | immutable. So it's like going from generations of variables,
         | abandoning the old references, and using the new one for each
         | iteration of the loop. In practice though, it's just message
         | handling and internal state, which is what he means by "from
         | the perspective of our program".
         | 
         | You typically wouldn't just write a Genserver to hold state
         | just to make it mutable (though I've seen them used that way),
         | unless it's shared state across multiple processes. They're not
         | used as pervasively as say classes in OOP. Genservers usually
         | have a purpose, like tracking users in a waiting room, chat
         | messages, etc. Each message handler is also serial in that you
         | handle one mailbox message at a time (which can spawn a new
         | process, but then that new process state is also immutable), so
         | the internal state of a Genserver is largely predictable and
         | trackable. So the only way to mutate state is to send a
         | message, and the only way to get the new state is to ask for
         | it.
         | 
         | There's a lot of benefits of that model, like knowing that two
         | pieces of code will never hit a race condition to edit the same
         | area of memory at the same time because memory is never shared.
         | Along with the preemptive scheduler, micro-threads, and process
         | supervisors, it makes for a really nice scalable (if well-
         | designed) asynchronous solution.
         | 
         | I'm not sure I 100% agree that watching mutating state requires
         | a function to observe it. After all, a genserver can send a
         | message to other processes to let them know that the state's
         | changed along with the new state. Like in a pub-sub system. But
         | maybe he's presenting an over-simplification trying to explain
         | the means of mutability in Elixir.
        
           | borromakot wrote:
           | `send` is a function. `receive` is a special form but in this
           | context it counts as a function
        
         | bux93 wrote:
         | I think the author means "I said everything is immutable, and
         | rebinding is obviously changing something, but the thing it
         | changes doesn't count!". The idea being, if you read a bunch of
         | code, none of the variables in that piece of code can have the
         | value of it changed unless there is some explicit line of code.
        
         | borromakot wrote:
         | I didn't mean you "must be inside of a function".
         | 
         | If you call `Process.put(:something, 10)`, any references you
         | have to whatever was already in the process dictionary will not
         | have changed, and the only way to "observe" that there was some
         | mutating state is that now subsequent calls to
         | `Process.get(:something)` return a different value than it
         | would have before.
         | 
         | So with immutable variables, there is a strict contract for
         | observing mutation.
        
       | sailorganymede wrote:
       | I really enjoyed reading this because it explained the topic
       | quite simply. It was well written !
        
       | tromp wrote:
       | > One of the major elements that sets Elixir apart from most
       | other programming languages is immutability.
       | 
       | It's interesting to compare Elixir to that other immutable
       | programming language: Haskell.
       | 
       | In Elixir, a binding                   counter = counter + 1
       | 
       | binds counter to the _old_ value of counter, plus 1. In Haskell
       | it instead binds counter to the _new_ value plus 1.
       | 
       | Of course that doesn't make sense, and indeed this causes an
       | infinite loop when Haskell tries to evaluate counter.
       | 
       | BUT it does make sense for certain recursive data structures,
       | like an infinite list of 1s:                   ones = 1 : ones
       | 
       | We can check this by taking some finite prefix:
       | ghci> take 5 ones         [1,1,1,1,1]
       | 
       | Another example is making a list of all primes, where you don't
       | need to decide in advance how many elements to limit yourself to.
       | 
       | Can you define such lazy infinite data structures in Elixir?
        
         | chucky_z wrote:
         | https://hexdocs.pm/elixir/Stream.html
         | 
         | Although I don't think it'll be quite as elegant as the Haskell
         | code.
        
         | finder83 wrote:
         | Infinite, yes, but I would say it's not quite as core to the
         | language as it is in Haskell where everything's lazy. Infinite
         | streams are quite simple though:
         | Stream.iterate(1, fn(x) -> x end)        |> Enum.take(5)
         | [1, 1, 1, 1, 1]
        
           | tromp wrote:
           | How do you use that for lists that do not simply iterate,
           | like                   ghci> fibs = 0 : 1 : zipWith (+) fibs
           | (drop 1 fibs)         ghci> take 10 fibs
           | [0,1,1,2,3,5,8,13,21,34]     ?
        
             | darcien wrote:
             | You can use Stream.unfold/2:
             | Stream.unfold({0,1}, fn {a,b} -> {a,{b,a+b}} end)       |>
             | Enum.take(10)       [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
             | 
             | https://rosettacode.org/wiki/Fibonacci_sequence#Elixir
        
         | torginus wrote:
         | I'm a bit confused - isn't this how all Static Single
         | Assignment representations in compilers work? And those are
         | used in things like LLVM IR to represent C and C++ code. Is C++
         | immutable now?
        
           | kqr wrote:
           | The difference at the high level is that assigning a variable
           | creates a new scope. E.g. in C I would expect to be able to
           | int i = 0;         while (i < 5) {             i = i+1;
           | printf("i: %d\n", i);         }
           | 
           | whereas in Haskell I could hypothetically something like
           | let i = 0 in             whileM (pure (i < 5)) $
           | let i = i + 1 in                     printf "i: %d\n" i
           | 
           | but the inner assignment would not have any effect on the
           | variable referenced by the condition in the while loop - it
           | would only affect what's inside the block it opens.
           | 
           | (And as GP points out, i=i+1 is an infinite loop in Haskell.
           | But even if it was used to build a lazy structure, it would
           | just keep running the same iteration over and over because
           | when the block is entered, i still has the value that was set
           | outside.)
        
             | eru wrote:
             | Btw, Haskell also supports mutable re-assignment of
             | variables. But it's not something that's built into the
             | language, you get mutable variables via a library. Just
             | like you can get loops in Haskell via a library.
        
               | kqr wrote:
               | Oh, yeah, it's right there in the standard library. But
               | one has to be a little more explicit about the fact that
               | one is accessing truly mutable variables. For example,
               | given the helper utilities                   checkIORef r
               | p = fmap p (readIORef r)         usingIORef r a =
               | readIORef r >>= a
               | 
               | the example can be written as                   main = do
               | i <- newIORef (0 :: Int)           whileM_ (checkIORef i
               | (< 5)) $ do             modifyIORef i (+1)
               | usingIORef i (printf "i: %d\n")
               | 
               | That said, even if one actually needs to mix mutable
               | variables with I/O actions like printing, I'm not sure I
               | would recommend using IORefs for it. But opening the can
               | of MonadIO m => StateT Int m () is for another day.
        
               | eru wrote:
               | You can also use the State monad, instead of IORef.
        
               | kqr wrote:
               | Not while printfing. That is what takes a MonadIO m =>
               | StateT s m () transformer, or at least a stack with a
               | Writer in it.
        
               | eru wrote:
               | Yes, that's true.
        
       | cies wrote:
       | I'm much stricter when it comes to what means immutable.
       | counter = counter + 1
       | 
       | vs                   counter += 1
       | 
       | Are exactly the same to me. In both cases you bind a new value to
       | counter: I don't care much if the value gets updated or new
       | memory is allocated. (sure I want my programs to run fast, but I
       | dont want to be too much worried about it, the
       | compiler/interpreter/runtime should do that "good enough" most of
       | the times)
       | 
       | In the absence of type safety immutability --IMHO-- becomes a bit
       | of a moot point. This is valid Elixir:                   x = 10
       | y = 25         z = x + y         y = "yeahoo"         IO.puts
       | "Sum of #{x} and #{y} is #{z}"
       | 
       | Trying to add another line "z = x + y" to the end, and you have a
       | runtime error.
       | 
       | The "feature" of Elixir that allows scoped rebinding to not
       | affect the outer scope, looks frightening to me. Most of the IDEs
       | I've worked with in the past 15 years warn me of overshadowing,
       | because that easily leads to bugs.
       | 
       | Haskell was already mentioned. There we can see real
       | immutability. Once you say a = 3, you cannot change that later
       | on. Sure sometimes (in many programs this can be limited) you
       | need it, and in those cases there's Haskell's do-notation, which
       | is basically syntactic sugar for overshadowing.
        
         | nuancebydefault wrote:
         | >Most of the IDEs I've worked with in the past 15 years warn me
         | of overshadowing
         | 
         | Most IDEs adapt their rules for warnings to the file type.
         | 
         | As i understand it, Elixir leans more to the functional
         | paradigm, so the rules are different. This different paradigm
         | has the pros described in the article. Of course it also has
         | cons.
         | 
         | If shadowing is a feature of the language, a feature that is
         | often used, the programmer, who has shifted their thinking to
         | that paradigm, knows that, and a warning is not needed.
        
           | cies wrote:
           | I'd say that rebinding a value to a variable is similar to
           | shadowing IN THE SAME SCOPE. Pretty much what Haskell does
           | with the do-notation.
        
           | cies wrote:
           | I find FP and shadowing have nothing to do with eachother
           | (maybe except though Haskell's do-notation)
        
             | nuancebydefault wrote:
             | You have a point. Maybe the shadowing is a paradigm on
             | itself, supporting FP. If you know it happens all the time,
             | and you know every var lives in the current scope, all you
             | have to care about is the current scope. Just like in C
             | function-local-vars (which might or not shadow globals) ,
             | but then applied to every code block.
        
               | mrkeen wrote:
               | > Maybe the shadowing is a paradigm on itself, supporting
               | FP.
               | 
               | Not really. Shadowing is compiler-warning in Haskell.
        
         | borromakot wrote:
         | The point wasn't to encourage shadowing bindings from a parent
         | scope, only to illustrate that it is not the same thing as
         | mutable references. Whether to, and when to, use rebinding is a
         | different conversation, but in some cases it can lead to subtle
         | bugs just like you described.
        
           | cies wrote:
           | I still think the difference to me as a programmer is largely
           | semantic (except when the performance has to be considered):
           | "rebinding" or "in-place mutation", in both cases it is a
           | mutable variable to me.
        
             | borromakot wrote:
             | There is a guarantee that you have when rebinding is the
             | only option.
             | 
             | with rebinding:                   def thing() do
             | x = 10           do_something(x)           # x is 10, non
             | negotiably, it is never not 10         end
             | 
             | with mutability                   def thing() do
             | x = 10           do_something(x)           # who knows what
             | x is? Could be anything.         end
             | 
             | Additionally, because rebinding is a syntactic construct,
             | you can use a linter to detect it: https://hexdocs.pm/credo
             | /Credo.Check.Refactor.VariableRebind...
        
               | cies wrote:
               | I find this a much better example than the example shown
               | in the article. I even find it quite useful when
               | explained like this.
               | 
               | Thanks!
        
         | mrkeen wrote:
         | > Haskell's do-notation, which is basically syntactic sugar for
         | overshadowing.
         | 
         | Do-notation does not relate to variable shadowing.
         | 
         | It's syntactic sugar over excessive flatmapping / pyramid of
         | doom.
        
       | nmadden wrote:
       | Rebinding is nicer than mutation, but neither are referentially
       | transparent.
        
         | xavxav wrote:
         | What do you mean? let-bindings don't interfere with referential
         | transparency. `let x = 1 in let x = 2 in foo` is referentially
         | transparent.
        
           | Izkata wrote:
           | I think you're thinking of shadowing, not re-binding.
        
             | kreetx wrote:
             | Yup, as a Haskeller, it's important to remember that
             | rebinding means something else in other languages.
        
           | nmadden wrote:
           | The example given in the article is:
           | counter = 0         counter = counter + 1
           | 
           | This is very different to shadowing where there is a clear
           | scope to the rebinding. In this case, I cannot employ
           | equational reasoning within a scope but must instead trace
           | back through every intervening statement in the scope to
           | check whether the variable is rebound.
        
             | tonyg wrote:
             | It's a straightforward syntactic transformation. The two
             | are equivalent. The scope of the rebound variable begins at
             | the rebinding and ends when the surrounding scope ends.
             | Perfectly clear - the only difference is a "let" keyword.
             | counter = 0       ...       counter = counter + 1       ...
             | 
             | vs                 let counter = 0 in       ...       let
             | counter = counter + 1 in       ...
        
               | nmadden wrote:
               | The ellipses in your straightforward transformation are
               | doing some heavy lifting there. Typically the let...in
               | construct has some way to indicate where the scope of the
               | "in" part ends: indentation (Haskell), explicit "end"
               | marker (SML) etc. Even with that, shadowing does make
               | equational reasoning harder (you have to look at more
               | surrounding context) and should generally be avoided.
        
         | jerf wrote:
         | Referential transparency is not a property of Elixir or Erlang
         | anyhow. In Haskell terms, everything is always in IO. So this
         | doesn't seem particularly relevant.
        
       | akoboldfrying wrote:
       | In most other languages, your newScore() example would indeed
       | race as you claim, but in JS it actually won't. JS uses a single-
       | threaded event loop, meaning asynchronous things like timers
       | going off and keys being pressed pile up in a queue until the
       | currently executing call stack finishes, and are only processed
       | then.
       | 
       | In your example, this means profile.score will remain at 3 every
       | time. Interestingly this would still be the case even if
       | setTimeout() was replaced with Promise.resolve(), since the sole
       | "await" desugars to ".then(rest-of-the-function)", and handlers
       | passed to .then() are always added to the job queue, even if the
       | promise they are called on is already settled [0].
       | 
       | To fix this (i.e., introduce an actual race), it would be enough
       | to add a single "await" sometime after the call to newScore(),
       | e.g., "await doSomethingElse()" (assuming doSomethingElse() is
       | async). That would cause the final "profile.score" line to appear
       | in the job queue at some indeterminate time in the future,
       | instead of executing immediately.
       | 
       | [0]: https://developer.mozilla.org/en-
       | US/docs/Web/JavaScript/Refe...
        
         | borromakot wrote:
         | Gross :)
         | 
         | But I will update my example in that case. Someone else
         | mentioned this but I was waiting to hear back. I will alter the
         | example to `await doSomethingElse`.
        
       | Closi wrote:
       | So VB6 had it right all along?
        
         | vanderZwan wrote:
         | In more ways than one, I've been told.
        
           | iamwil wrote:
           | What are the other ways?
        
             | avaldez_ wrote:
             | On error resume next /jk
        
         | snapcaster wrote:
         | i loved VB6 (our school intro programming class used it) and i
         | sometimes wonder how much influence that had on my future
         | career choices. It was a programming environment that felt fun,
         | powerful and not scary at all
        
       | LegionMammal978 wrote:
       | I'd note that 'immutability everywhere' isn't the only way to
       | solve the issue of uncontrolled observation of mutations, despite
       | that issue often being cited as a justification. You can also
       | design a language to just directly enforce static restrictions on
       | who may mutate a referenced value and when. Rust with its
       | aliasing rules is easily the most famous implementation of this,
       | but other languages have continued to experiment with this idea.
       | 
       | The big benefit is that you can still have all the usual
       | optimizations and mental simplicity that depend on non-
       | observability, while also not having to contort the program into
       | using immutable data structures for everything, alongside the
       | necessary control flow to pass them around. (That isn't to say
       | that they don't have their use cases in a mutable language,
       | especially around cross-thread data structures, but that they
       | aren't needed nearly as frequently in ordinary code.)
        
         | packetlost wrote:
         | I think having some sort of structured mutability is a very,
         | very good idea. Look at Clojure's atoms and transient data-
         | structures for some other ways that this has been done. There's
         | probably others, I'd love to see more examples!
        
           | jerf wrote:
           | The historically-ironic thing to me is that Erlang/BEAM
           | brushed up against the idea and just didn't _quite_ get it.
           | What 's important to the properties that Erlang maintains is
           | that actors can't reach out and directly modify other actor's
           | values. You have to send messages. It is sufficient to
           | maintain this properly that you can't send references in
           | messages, and it is sufficient to maintain that property to
           | simply not have references, which Erlang and BEAM do not.
           | Full immutability is sufficient but not necessary.
           | 
           | Erlang was a hair's breadth away from having mutation
           | contained within the actor's variable space, with no external
           | mutation, which for the time would have been quite
           | revolutionary. Certainly Rust's mutation control is much
           | richer, but Rust came a lot later, and at least based on its
           | current compile performance, wasn't even on the table in the
           | late 1990s.
           | 
           | But the sort of understanding of mutability explained in the
           | original post was not generally understood. Immutability was
           | not a brand new concept chronologically, but if you define
           | the newness of a computer science concept as the integration
           | of its usage over time, it was still pretty new by that
           | metric; it had been bouncing around the literature for a long
           | time but there weren't very many programming languages that
           | used it at the time. (And especially if you prorate
           | "languages" by "how easy it is to write practical programs".)
           | 
           | Elixir does a reasonable job of recovering it from the
           | programmer's perspective, but I think an Erlang/BEAM that
           | just embraced mutability within an actor probably would have
           | done incrementally better in the programming language market.
        
             | mrkeen wrote:
             | > What's important to the properties that Erlang maintains
             | is that actors can't reach out and directly modify other
             | actor's values. You have to send messages.
             | 
             | I just cannot make this mental leap for whatever reason.
             | 
             | How does 'directly modify' relate to immutability? (I was
             | sold the lie about using setters in OO a while back, which
             | is also a way to prevent direct modification.)
        
               | jerf wrote:
               | So, this is something I think we've learned since the
               | 1990s as a community, and, well, it's still not widely
               | understood but: The core reason mutability is bad is not
               | the mutation, it is "unexpected" mutation. I scare quote
               | that, because that word is doing a lot of heavy lifting,
               | and I will not exactly 100% nail down what that means in
               | this post, but bear with me and give me some grace.
               | 
               | From a the perspective of "mutability", how dangerous is
               | this Python code?                   x = 1         x = 2
               | print(x)
               | 
               | Normally little snippets like this should be understood
               | as distilled examples of a general trend, but in this
               | case I mean literally three lines. And the answer is,
               | obviously, not at all. At least from the perspective of
               | understanding _what_ is going on. A later programmer
               | reading this probably has questions about _why_ the code
               | is written that way, but the _what_ is well in hand.
               | 
               | As the distance between the two assignments scales up, it
               | becomes progressively more difficult to understand the
               | what. Probably everyone who has been in the field for a
               | few years has at some point encountered the Big Ball Of
               | Mud _function_ , that just goes on and on, assigning to
               | this and assigning to that and rewriting variables with
               | wild abandon. Mutability makes the "what" of such
               | functions harder.
               | 
               | Progressing up, consider:                   x = [1]
               | someFunction(x)         print(x)
               | 
               | In Python, the list is mutable; if someFunction appends
               | to it, it will be mutated. Now to understand the "what"
               | of this code you have to follow in to someFunction. In an
               | immutable language you don't. You still need to know what
               | is coming out of it, of course, but you can look at that
               | code and know it prints "[1]".
               | 
               | However, this is still at least all in one process. As
               | code scales up, mutation does make things harder to
               | understand, and it can become hard enough to render the
               | entire code base pathologically difficult to understand,
               | but at least it's not as bad as this next thing.
               | 
               | Concurrency is when mutation just blows up and becomes
               | impossible for humans to deal with. Consider:
               | x = [1]        print(x)
               | 
               | In a concurrent environment where another thread may be
               | mutating x, the answer to the question "what does the
               | print actually print?" is "Well, anything, really." If
               | another thread can reach in and "directly" mutate x, at
               | nondeterministic points in your code's execution, well,
               | my personal assertion is nobody can work that way in
               | practice. How do you work with a programming language
               | where the previous code example could do anything, and it
               | will do it nondeterministically? You can't. You need to
               | do something to contain the mutability.
               | 
               | The Erlang solution is, there is literally no way to
               | express one actor reaching in to another actor's space
               | and changing something. In Python, the x was a mutable
               | reference that could be passed around to multiple
               | threads, and they all could take a crack at mutating it,
               | and they'd all see each other's mutations. In languages
               | with pointers, you can do that by sharing pointers; every
               | thread with a pointer has the ability to write through
               | the pointer and the result is visible to all users.
               | There's no way to do that in Erlang. You can't express
               | "here's the address of this integer" or "here's a
               | reference to this integer" or anything like that. You can
               | only send concrete terms between actors.
               | 
               | Erlang pairs this with all values being immutable.
               | (Elixir, sitting on top of BEAM, also has immutable
               | values, they just allow rebinding variables to soften the
               | inconvenience, but under the hood, everything's still
               | immutable.) But this is overkill. It would be fine for an
               | Erlang actor to be able to do the equivalent of the first
               | example I wrote, as long as nobody _else_ could come in
               | and change the variable unexpectedly before the print
               | runs. Erlang actors tend to end up being relatively
               | small, too, so it isn 't even all that hard to avoid
               | having thousands of variables in a single context. A lot
               | of Erlang actors have a dozen or two variables tops,
               | being modified in very stereotypical manners through the
               | gen_* interfaces, so having in-actor truly mutable
               | variables would probably have made the language generally
               | easier to understand and code in.
               | 
               | In the case of OO, the "direct mutation" problem is
               | related to the fact that you don't have these actor
               | barriers within the system, so as a system scales up,
               | this thing "way over there" can end up modifying an
               | object's value, and it becomes very difficult over time
               | to deal with the fact that when you operate that way, the
               | responsibility for maintaining the properties of an
               | object is distributed over the entire program.
               | Technically, though, I wouldn't necessarily chalk this up
               | to "mutability"; even in an immutable environment
               | distributing responsibility for maintaining an object's
               | properties over the entire program is both possible and a
               | bad idea. You can well-encapsulated mutation-based
               | objects and poorly-encapsulated immutable values. I'd
               | concede the latter is harder than the former, as the
               | affordances of an imperative system seems to beg you to
               | make that mistake, but it's certainly possible to
               | accidentally distribute responsibilities incorrectly in
               | an immutable system; immutability is certainly not a
               | superset of encapsulation or anything like that. So I'd
               | class that as part of what I mentioned in this post
               | before I mentioned concurrency. The sheer size of a
               | complex mutation-based program can make it too hard to
               | track what is happening where and why.
               | 
               | Once you get used to writing idiomatic Erlang programs,
               | you contain that complexity by writing focused actors.
               | This is more feasible than anyone who hasn't tried
               | thinks, and is one of the big lessons of Erlang that
               | anyone could stand to learn. It is then also relatively
               | easy to take this lesson back to your other programming
               | languages and start writing more self-contained things,
               | either actors running in their own thread, or even
               | "actors" that don't get their own thread but still are
               | much more isolated and don't run on the assumption that
               | they can reach out and directly mutate other things
               | willy-nilly. It can be learned as a lesson on its own,
               | but I think one of the reasons that learning a number of
               | languages to some fluency is helpful is that these sorts
               | of lessons can be learned much more _quickly_ when you
               | work in a language that _forces_ you to work in some way
               | you 're not used to.
        
               | AnimalMuppet wrote:
               | I've run into something like this when working on
               | embedded systems using OO. You have persistent mutable
               | data, you have an object that encapsulates some data, you
               | have multiple sources of control that each have their own
               | threads that can modify that data, and you have
               | consistency relationships that have to be maintained
               | within that data.
               | 
               | The way you deal with that is, you have the object defend
               | the consistency of the data that it controls. You have
               | some kind of a mutex so that, when some thread is messing
               | with certain data, no other thread can execute functions
               | that mess with that data. They have to wait until the
               | first thread is done, and then they can proceed to do
               | their own operations on that data.
               | 
               | This has the advantage that it puts the data and the
               | protection for the data in the same place. Something "way
               | over there" can still call the function, but it will
               | block until it's safe for it to modify the data.
               | 
               | (You don't put semaphores around all data. You think
               | carefully about which data can be changed by multiple
               | threads, and what consistency relationships that could
               | violate, and you put them where you need to.)
               | 
               | Is that better or worse than Erlang's approach? Both,
               | probably, depending on the details of what you're doing.
        
               | jerf wrote:
               | That's possibly what I meant by the "actors that don't
               | get their own thread" at the very end. I've switched away
               | from Erlang and write most of my stuff in Go now, and
               | while I use quite a few legit "actors" in Go, that have
               | their own goroutine, I also have an awful lot of things
               | that are basically "actors" in that they have what is
               | effectively the same isolation, the same
               | responsibilities, the same essential design, except they
               | don't actually need their own control thread. In Erlang
               | you often just give them one anyhow because it's the way
               | the entire language, library, and architecture is set up
               | anyhow, but in Go I don't have to and I don't. They
               | architecturally have "one big lock around this entire
               | functional module" and sort of "borrow" the running
               | thread of whoever is calling them, while attaining the
               | vast majority of benefits of an actor in their design and
               | use.
               | 
               | If you have an "actor", that never does anything on its
               | own due to a timer or some other external action, that
               | you never have to have a conversation with but are
               | interacting with strictly with request-response and
               | aren't making the mistake someone discussed here [1],
               | then you can pretty much just do a One Big Lock and call
               | it a day.
               | 
               | I do strictly follow the rule that no bit of code ever
               | has more than one lock taken at a time. The easiest way
               | to deal with the dangers of taking multiple locks is to
               | not. Fortunately I do not deal in a performance space
               | where I have no choice but to take multiple locks for
               | some reason. Though you can get a long way on this rule,
               | and building in more communication rather than locking.
               | 
               | [1]: https://news.ycombinator.com/item?id=41722440
        
               | mrkeen wrote:
               | > You don't put semaphores around all data
               | 
               | You're talking about putting semaphores around _code_.
               | 
               | Locking _data_ , not _code_ , is a great way to do
               | things. It composes and you don't run into too-many-
               | locks, too-few-locks, forgetting-to-take-a-lock, or
               | deadlocking problems.
               | 
               | https://www.adit.io/posts/2013-05-15-Locks,-Actors,-And-
               | STM-...
        
               | mrkeen wrote:
               | > In the case of OO, the "direct mutation" problem is
               | related to the fact that you don't have these actor
               | barriers
               | 
               | Right, the OO guys said to use "encapsulation" rather
               | than direct mutation.
               | 
               | > so as a system scales up, this thing "way over there"
               | can end up modifying an object's value
               | 
               | Can you not send a message way over there?
        
               | asa400 wrote:
               | You're correct that any process can more or less send a
               | message to any other process, but the difference is what
               | guarantees the Erlang runtime provides around that idea.
               | 
               | For example, in Erlang, if I have processes A, B, and C,
               | and B and C both send messages to A at the same time, the
               | runtime guarantees that A processes the messages one at a
               | time, in order, before moving on to the next message
               | (there is some more detail here but it is not important
               | to the point).
               | 
               | The runtime guarantees that from A's perspective, the
               | messages from B and C cannot arrive "simultaneously" and
               | trample on each other. The runtime also guarantees that A
               | cannot process both messages at the same time. It
               | processes the messages one at a time. All code in A is
               | run linearly, single-threaded. The VM takes care of
               | scheduling all of these single-threaded processes to run
               | on the same hardware in parallel.
               | 
               | As other posters have pointed out, the runtime also
               | guarantees that B and C cannot reach in and observe A's
               | raw memory in an uncontrolled fashion (like you could in
               | C, Java, etc.), so B and C cannot observe any
               | intermediate states of A. The only way for B and C to get
               | any information out of A is to send a message to A and
               | then A can send a reply, if it wants. These replies are
               | just normal messages, so they also obey all of the
               | guarantees I've already described, so A will send the
               | replies one at time, and they will end up in the
               | mailboxes of B and C for their own processing.
               | 
               | Given all this (and more which I haven't gone into),
               | Erlang doesn't have the concept of a data race where 2 or
               | more threads are concurrently accessing the same memory
               | region, as you might have in say, the C language (note
               | that this is different than a logical race condition,
               | which Erlang of course still can have).
               | 
               | I hope this is useful, you're asking good questions.
        
             | LegionMammal978 wrote:
             | IIRC, Rust's idea of controlled mutability originally came
             | directly from the Erlang idea of immutable messages between
             | tasks. Certainly, in the classic "Project Servo"
             | presentation [0], we can see that "no shared mutable state"
             | refers specifically to sharing _between different tasks_. I
             | think it was pretty early on in the project that the idea
             | evolved into the fine-grained aliasing rules. Meanwhile,
             | the lightweight tasks stuck around until soon before 1.0
             | [1], when they were abandoned in the standard library, to
             | be later reintroduced by the async runtimes.
             | 
             | [0] http://venge.net/graydon/talks/intro-talk-2.pdf
             | 
             | [1] https://rust-lang.github.io/rfcs/0230-remove-
             | runtime.html
        
               | dartos wrote:
               | I have mixed feelings about rust's async story, but it is
               | really nice having good historical documentation like
               | this.
               | 
               | Thanks for the links!
        
             | toast0 wrote:
             | I think you're right that "interior immutability" of actors
             | isn't really necessary to the programming model that you
             | get from requiring message passing between actors.
             | 
             | However, interior immutability is not without its benefits.
             | It enables a very simple GC. GC is easily done per-actor
             | because each actor has independent, exclusive, access to
             | its own memory. But the per-actor GC is very simple because
             | all references are necessarily backwards in time, because
             | there's no way to update a reference. With this, it's very
             | simple to make a copying GC that copies any active
             | references in order; there's no need for loop checking,
             | because loops are structurally impossible.
             | 
             | I don't know that this was the intent of requiring
             | immutability, but it's a nice result that pops out. Today,
             | maybe you could pull in an advanced GC from somewhere else
             | that already successfully manages mutable data, but these
             | were not always available.
             | 
             | Of course, it should be noted that BEAM isn't entirely
             | immutable. Sometimes it mutates things when it knows it can
             | get away with it; I believe tuples can be updated in some
             | circumstances when it's clear the old tuple would not be
             | used after the new one is created. The process dictionary
             | is direct mutable data. And BIFs, NIFs, and drivers aren't
             | held to strict immutability rules either, ets has interior
             | mutability, for example.
        
               | jerf wrote:
               | "Of course, it should be noted that BEAM isn't entirely
               | immutable."
               | 
               | Mutability is relative to the layer you're looking at.
               | BEAM is, of course, completely mutable from top to bottom
               | because it is constantly mutating RAM, except, of course,
               | that's not really a helpful way of looking at it, because
               | at the layer of abstraction you program at values are
               | immutable. Mutatable programs can be written in terms of
               | immutable abstractions with a well-known at-most O(n log
               | n) penalty, and immutable programs can be written on a
               | mutable substrate by being very careful never to visibly
               | violate the abstraction of immutability, which is good
               | since there is (effectively for the purposes of this
               | conversation) no such thing as "immutable RAM". (That is,
               | yes, I'm aware of WORM as a category of storage, but it's
               | not what this conversation is about.)
        
       | gr4vityWall wrote:
       | This article is exceptionally well written. The author did a good
       | job at making the subject approachable.
       | 
       | I disagree with this phrase:
       | 
       | > By forcing the mutation of state to be serialized through a
       | process's mailbox, and limiting the observation of mutating state
       | to calling functions, our programs are more understandable
       | 
       | My experience is quite the opposite - that's a mental model for
       | programs that goes against how most people I know reason about
       | code.
       | 
       | The examples in Elixir all looked more complicated for me to
       | understand, generally, although I understand the value brought by
       | that model. The cognitive load seemed fairly higher as well.
        
         | azeirah wrote:
         | > My experience is quite the opposite - that's a mental model
         | for programs that goes against how most people I know reason
         | about code.
         | 
         | > The examples in Elixir all looked more complicated for me to
         | understand, generally, although I understand the value brought
         | that model. The cognitive load seemed fairly higher as well.
         | 
         | Of course it's not as understandable to someone who's not used
         | to it.
         | 
         | When I read articles about a different paradigm, I assume that
         | "better" means "with equal experience as you have in your own
         | paradigm"
         | 
         | So if someone says "this convoluted mess of a haskell program
         | is better than js", I will say "yes, if you spent 100 hours in
         | Haskell prior to reading this".
        
           | gr4vityWall wrote:
           | > Of course it's not as understandable to someone who's not
           | used to it.
           | 
           | Sorry, I didn't mean to imply otherwise. Perhaps the original
           | quote should make what you said ("with equal experience as
           | you have in your own paradigm") explicit.
           | 
           | I do believe that the paradigm proposed in the article has a
           | much higher learning curve, and expect it to not be adopted
           | often.
        
           | cpill wrote:
           | Hey, and hence the rampant adoption of Haskell :P
        
         | toast0 wrote:
         | I think understandable can mean different things here. If we're
         | looking at a state dump and your data is in a weird place, I'd
         | want to understand how the data got into that weird place.
         | 
         | In a model with state owned by a process, and changes coming in
         | through the mailbox, I know that all of the state changes
         | happened through processing of incoming messages, one at a
         | time. If I'm lucky, I might have the list of messages that came
         | in, and be able to run them one a time, but if not, I'll just
         | have to kind of guess how it happened, but there's probably
         | only a limited number of message shapes that are processed, so
         | it's not too hard. There's a further question of how those
         | messages came to be in the process's mailbox, of course.
         | 
         | In a model with shared memory mutability, it can be difficult
         | to understand how an object was mutated across the whole
         | program. Especially if you have errors in concurrency handling
         | and updates were only partially applied.
         | 
         | There's certainly a learning curve, but I've found that once
         | you've passed the learning curve, the serialized process
         | mailbox model makes a lot of understanding simpler. Individual
         | actors are often quite straight forward (if the application
         | domain allows!), and then the search for understanding focuses
         | on emergent behavior. There's also a natural push towards
         | organizing state into sensible processes; if you can organize
         | state so there is clear and independent ownership of something
         | by a single actor, it becomes obvious to do so; it's hard to
         | put this into words, but the goal is to have an actor that can
         | process messages on that piece of state without needing to send
         | sub-requests to other actors; that's not always possible,
         | sometimes you really do need sub-requests and the complexity
         | that comes with it.
        
           | gr4vityWall wrote:
           | That's an interesting point of view, thanks for taking the
           | time to write that.
           | 
           | I wonder how much that learning curve is worth it. It reminds
           | me of the Effect library for TypeScript, in that regard, but
           | Elixir looks more readable to me in comparison.
        
             | toast0 wrote:
             | > I wonder how much that learning curve is worth it.
             | 
             | It really depends on how well your application fits the
             | model. If it's a stretch to apply the model and you're
             | already comfortable with something else, it might not be
             | worth it. But if you're building a chat server, or
             | something similar, I think it's very worthwhile. I think it
             | can be pretty useful if you have a lot of complex state per
             | user, in general, as your frontend process could send
             | process inbound requests into messages into a mailbox where
             | each user would only be processed by a single actor (each
             | user doesn't need their own actor, you could hash users in
             | some way to determine which actor handles a given user).
             | Then you have a overall concurrent system, but each user's
             | complex state is managed in a single threaded manner.
             | 
             | I think process/actor per connection is a superior mental
             | model to explicit event loops for something like a large
             | HTTP server, but unless the server is performing complex
             | work within it, I would assume without testing that the
             | explicit event loop would win on performance. I think it
             | would be more fun to build a large HTTP server in Erlang
             | than with c and kqueue, but nginx and lighttpd are already
             | written, and quite fast. When I needed to do a TCP proxy
             | for million + connections, I took HAProxy and optimized it
             | for my environment, rather than writing a new one in
             | Erlang; that was still fun, but maybe different fun; and
             | being able to handle tens of thousands of connections with
             | minutes of configuration work was a better organization
             | choice than having fun building a proxy from scratch. :)
        
           | Nevermark wrote:
           | Well said.
           | 
           | The essence: understanding scales better with mutation
           | serialization.
           | 
           | Any little bumps of complexity at the beginning, or in local
           | code, pays off for simpler interactions to understand across
           | longer run histories, increased numbers of processes, over
           | larger code bases.
        
         | rkangel wrote:
         | > The examples in Elixir all looked more complicated for me to
         | understand, generally, although I understand the value brought
         | that model. The cognitive load seemed fairly higher as well.
         | 
         | I can see that might be the case with simple examples, but with
         | more complex systems I find the cognitive load to be much
         | lower.
         | 
         | The Elixir/Erlang approach naturally results in code where you
         | can do local reasoning - you can understand each bit of the
         | code independently because they run in a decoupled way. You
         | don't need to read 3 modules and synthesise them together in
         | your head. Similarly the behaviour of one bit of code is much
         | less likely to affect the behaviour of other code unexpectedly.
        
           | gr4vityWall wrote:
           | Question: do you know any of such more complex systems which
           | are Free Software, so that I could take a look?
           | 
           | Sounds like it would be a fun learning experience.
        
             | rkangel wrote:
             | I'd love to point you at some code from work but it's all
             | closed source unfortunately.
             | 
             | One example that gets used is https://github.com/hexpm/hex
             | which is the code behind the Elixir package management
             | website. It's more a good example of a Phoenix app than it
             | is general OTP stuff, but there are some GenServers in
             | there.
        
         | jimbokun wrote:
         | > that's a mental model for programs that goes against how most
         | people I know reason about code.
         | 
         | But the mental model most of us have for reasoning about code
         | in environments with concurrent execution is simply wrong.
         | 
         | So the Elixir model is more understandable, if you want a
         | correct understanding of what your code will do when you run
         | it.
        
           | gr4vityWall wrote:
           | > the mental model most of us have for reasoning about code
           | in environments with concurrent execution is simply wrong.
           | 
           | Could you elaborate on that?
        
             | jimbokun wrote:
             | It is very difficult to write thread based code with no
             | bugs.
        
       | prerok wrote:
       | What I don't understand about immutability is performance. How do
       | these languages achieve small memory footprints and avoiding
       | continuous allocations of new object versions, because a single
       | property changed?
       | 
       | I mean, all I see are small scale examples where there are only a
       | few properties. The production Rust code I did see, is passing
       | copies of objects left and right. This makes me cringe at the
       | inefficacy of such an approach.
       | 
       | Disclaimer: I have 0 experience in immutable languages, hence the
       | question :)
        
         | greener_grass wrote:
         | 1. Structural sharing - most data doesn't change so you create
         | a copy that reuses much of the original structure
         | 
         | 2. Garbage collection and lexical scopes - you clean up memory
         | quickly and in batch
         | 
         | 3. Compiler optimizations - you turn functional constructs into
         | imperative ones that reuse and mutate memory at compile-time,
         | where it is provably safe to do so
         | 
         | Roc Lang might interest you:
         | https://www.youtube.com/watch?v=vzfy4EKwG_Y
        
         | whateveracct wrote:
         | "Purely Functional Data Structure" by Chris Okasaki is a
         | classic and imo a must-read data structures book.
         | 
         | https://www.cs.cmu.edu/~rwh/students/okasaki.pdf
        
         | heeton wrote:
         | Small example to show that performance can be great: Phoenix
         | (the Rails-comparable web framework for Elixir) defaults to
         | listing microseconds instead of milliseconds for response
         | times.
        
           | prerok wrote:
           | Hmm, is rails really comparable to anything that's
           | precompiled (genuine question, I don't mean this
           | dismissively).
           | 
           | What I really meant was, as a backend engineer, I frequently
           | deal with optimizations on too many object allocations and
           | long running/too frequent GC cycles even without immutability
           | built into the language.
           | 
           | On the Rust front, the problem is in small memory
           | allocations, fragmented memory and then more calls to kernel
           | to alloc.
        
             | samatman wrote:
             | Ruby/Rails and Elixir/Phoenix both run on a garbage-
             | collected virtual machine. I think that makes the
             | comparison fair.
        
         | munificent wrote:
         | _> The production Rust code I did see, is passing copies of
         | objects left and right._
         | 
         | You might be surprised how fast memcpy() is in practice on
         | modern hardware. It's worth sitting down and writing a little C
         | program that moves memory around and does some other stuff to
         | get a feel for what the real world performance is like.
        
           | sroussey wrote:
           | I too come from a time of worrying about memory access
           | patterns and usage (pre virtual memory in hardware), so my
           | initial reaction is like the parent comment, at least
           | instinctively.
           | 
           | And yes, memcpy is fast, but I would not use a little program
           | to convince myself. You will end up with stuff in CPU caches,
           | etc, which will give you a very incorrect intuition.
           | 
           | Better to take a large program where there is a base factory
           | and make some copies there or something and see how it
           | affects things.
           | 
           | That said... for most businesses these days, developer time
           | is more expensive than compute time, so if you're not
           | shipping an operating system or similar, it simply doesn't
           | matter.
           | 
           | And an optimizing compiler could do something like copy on
           | write, and make much of the issue moot.
           | 
           | I had a brief period of time designing a simple CPU and it's
           | made everything since turn my stomach a little bit.
        
         | steveklabnik wrote:
         | Just because something is semantically a copy doesn't mean that
         | they will not be optimized out. Take a look at this example:
         | https://godbolt.org/z/roxn43eMc
         | 
         | While create() semantically copies the struct Foo out to its
         | caller, with optimizations on, the function isn't even invoked,
         | let alone a copy be made.
         | 
         | That said, of course sometimes this optimization is missed, or
         | it can't happen for some reason. But my point is just that some
         | Rust code may look copy heavy but is not actually copy heavy.
        
           | prerok wrote:
           | Thank you!
           | 
           | Yeah, I will have to take a closer look at just why the copy
           | elision isn't happening in the cases I looked at...
        
       | skybrian wrote:
       | I can see this being useful for the same reason that it's useful
       | that non-async functions in JavaScript can't be interrupted -
       | it's as if you held a lock for the entire function call.
       | 
       | Between any two event handlers, anything could change. Similarly
       | for await calls in JavaScript. And to get true parallelism, you
       | need to start a separate worker. Concurrency issues can still
       | happen, but not in low-level operations that don't do I/O.
       | 
       | I don't see anything wrong with mutating a local variable in
       | cases when it's purely a local effect. It's sometimes cleaner,
       | since you can't accidentally refer to an obsolete version of a
       | variable after mutating it.
        
       ___________________________________________________________________
       (page generated 2024-10-02 23:00 UTC)