tromp 3 hours ago

> 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?

  • torginus 2 hours ago

    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 an hour ago

      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 an hour ago

        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 32 minutes ago

          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.
  • finder83 3 hours ago

    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 29 minutes ago

      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]
      ?
mrkeen 4 hours ago

> 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.

  • bux93 3 hours ago

    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.

  • finder83 3 hours ago

    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.

  • colonwqbang 3 hours ago

    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.

sailorganymede 4 hours ago

I really enjoyed reading this because it explained the topic quite simply. It was well written !

fracus 4 hours ago

This was very enlightening for me on the subject of immutability.