0

Here is an edited version of Stack sample I see on https://blog.codeship.com/statefulness-in-elixir/ (by Micah Woods). It works by the way.

defmodule Stack do
  def start_link do
    pid = spawn_link(__MODULE__, :loop, [[]])
    {:ok, pid}
  end

  def loop(stack) do
    receive do
      {:size, sender} ->
        send(sender, {:ok, Enum.count(stack)})
      {:push, item} -> stack = [item | stack]
      {:pop, sender} ->
        [item | stack] = stack
        send(sender, {:ok, item})
    end
    loop(stack)
  end
end

Inside the loop() function, the stack variable is rebound in some cases in the receive block, but not others. This seems to be the behavior of a mutable variable, not a variable rebinding.

In my mind, variable rebinding should only be allowed if there is clear delineation between old and new variable. i.e. only if the code can be rewritten without variable rebinding. In a language without variable rebinding, the loop() code would look like this:

def loop(stack) do
  receive do
    {:size, sender} ->
      send(sender, {:ok, Enum.count(stack)})
      ###### stack2 not defined in this case ######
    {:push, item} -> stack2 = [item | stack]
    {:pop, sender} ->
      [item | stack2] = stack
      send(sender, {:ok, item})
  end
  loop(stack2)
end

Notice stack2 is not defined in the first case. So is stack2 assigned the value of stack by default if no assignment occurs, or stack is actually a mutable variable under the hood?

So how do I understand this rebinding concept in Elixir properly and logically? In my mind this is encroaching on the mutable variable territory. How does the rebinding work under the hood?

jackbean818
  • 304
  • 3
  • 7
  • 1
    `if`, `receive` and some other constructs have this "feature" where binding a variable inside their body lets you access the variable outside the construct. I can't find the source of this or I'd post an answer. Thankfully the Elixir compiler now prints a large warning message for such code. – Dogbert Jun 15 '17 at 20:19

2 Answers2

1
iex(1)> stack = [1,2,3]
[1, 2, 3]
iex(2)> if false, do: [head | stack] = stack
nil
iex(3)> stack
[1, 2, 3]
iex(4)> if true, do: [head | stack] = stack
[1, 2, 3]
iex(5)> stack
[2, 3]

This is just a rebind of the variable. There is nothing mutable happening here.

It is a mis-feature of Elixir that has been deprecated. If you try to compile this, you will receive a warning telling you that the variable is unsafe. This should be fully removed soon. Unfortunately, I do not know exactly when.

Justin Wood
  • 9,941
  • 2
  • 33
  • 46
0

** EDIT ** Justin Wood's answer is correct. If anyone is interested, here is the correct way to code this. Return the value you want to use outside the receive block.

def loop(stack) do
  stack2 = receive do
    {:size, sender} ->
      send(sender, {:ok, Enum.count(stack)})
      ###### stack2 not defined in this case ######
      stack
    {:push, item} -> [item | stack]
    {:pop, sender} ->
      [item | stack2] = stack
      send(sender, {:ok, item})
      stack2
  end
  loop(stack2)
end

** EDIT 2 **

Here is a more detailed explanation.

Elixir variables are not "assigned". Instead, they are bound to a value. For example, the pattern match stack = [] binds an empty list to the variable stack. If you then do stack = [1 | stack], the list [1] is bound to variable stack (rebinding since it was already bound).

In the first part of your OP, you were rebinding stack in the last two clauses, but not in the first. BTW, variable rebinding happens anytime you match an already bound value to different value (provide you don't use the pin ^ operator).

The confusion comes from the use of non-hygienic variables in some of elixir constructs like if, case, cond, receive, etc. blocks, allowing their variable bindings to leak outside the block. For one reason or another, this is how Elixir was originally designed and this "feature" was often exploited as you did in your OP. It was later decided, that this was not a desired feature so it was deprecated. In the future it will be removed (work the same as closures I assume).

So, the issue is not really about rebinding, but about the variable binding leaking outside the block.

So, when this is "feature" is finally removed, the second code example of your OP should raise a compile error indicating that stack2 is unbound. It should also raise warnings that stack2 is unused in the 2nd and 3rd clauses of your receive block.

Once you understand that the variable bindings are leaking out of the block, I hope that will remove the magic. The bottom line:

  • only bind temporary valuables inside a if, else, cond, case, and receive blocks.
  • if you need to use a value calculated inside one of these blocks, return match on the return value so you can use that binding later in your code.

Finally, to answer the question is variable rebinding the same as a mutable variable. Not really. On rebinding, the data bound to the variable is not change. A new copy of the data is created and the variable name is bound do the new data. The old data and any other variables bound to the original data are left unchanged. Of course, once there are not variables left bound to the original data, GC kicks in to free it.

Steve Pallen
  • 4,417
  • 16
  • 33
  • I agree, that would be how it is done in a very strict language. The behavior of Elixir rebinding is a little too much magic here. – jackbean818 Jun 15 '17 at 20:14
  • This doesn't really answer the question. It was less of "how do I rewrite this properly" and more of "why is this happening / allowed". – Justin Wood Jun 15 '17 at 23:33
  • @JustinWood I know I was not really answering the question. However, I added the answer to help others know the how to code this. I updated my answer to explain. – Steve Pallen Jun 15 '17 at 23:48
  • I feel @StevePallen's answer is helpful here. My question was not about how variable rebinding works, but how to reason the behavior of variable rebinding inside a receive block that does not seem logically consistent. – jackbean818 Jun 16 '17 at 13:40
  • @jackbean818 Just updated my answer. Hopefully that helps remove the "magic". – Steve Pallen Jun 16 '17 at 17:39
  • Thanks for the more detailed answer. I do have issue with your last paragraph, since I never said this is the behavior of mutable data. Hopefully that does not cause more confusion. – jackbean818 Jun 16 '17 at 19:17
  • I get it. You said "mutable variable", not "mutable variable". Clarified that in my answer. – Steve Pallen Jun 16 '17 at 21:00