What's the difference between variable binding and assignment?

immutability

#1

What’s the difference between variable binding and assignment? How does that work on a low level?


#2

As I understand it (read it with scepticism), in case of Elixir:

  1. binding would be an expression, assignment is statement.
  2. In Elixir = is basically a pattern matching with binding(?)
  3. Since BEAM has only immutable values, binding making much more sense. Maybe I’m simplifying a lot - assignment assigns some literal (textual representation) to a place in memory, so changing variable X changes data in memory it was pointing to. In case of binding changing X changes only it’s pointer (so it points to another place in memory and does not affect original data it was pointing to)

Hope I did not write some non sense :smiley:


#3

A couple clarifications:

There is no “assignment” in Elixir[1], when you use ‘=’ you are doing a pattern match and it either succeeds or fails. If it fails, the current process crashes.

The compiler/runtime has a set of rules that it uses to attempt to make the pattern match succeed. One of these is “binding” and “re-binding”. Binding creates a variable that references a term in memory, if that variable does not already exist in the current scope. In Erlang, all further uses of that variable in it’s current scope are fixed to that term in memory. Elixir cheats a bit and allows you to “re-bind”[2] a variable to a new term in memory in certain situations, however the actual term in memory from the original binding does not change and will need to be garbage collected at some point.

[1]- Except where there is… Process Dictionary comes to mind.

[2]- If I am understanding it correctly: Under the hood the Elixir compiler actually creates a new variable name and just remembers that x is really x1 from now on.


#4

That is precisely correct, though the name is a bit different, but yep. :slight_smile:

You can see the Erlang Core code by running erlang output manually through the internal compiler and out to erlang to dump out the Core if you are curious how it implements anything (I should probably document how sometime…). :slight_smile:


#5

Great question :023:

I’ve also wondered the same thing.

So would it be safe to say the following?

Binding has to do with giving names to things (or values) in a given well delimited context. Assignment is about storing things (or values) in some location (a variable)

From: https://cs.stackexchange.com/questions/39525/what-is-the-difference-between-assignment-valuation-and-name-binding


#6

Seems pretty good. :slight_smile:

I’d think the most ‘pure’ way of thinking of bindings are as unevaluated closures, so even doing just a = 4 means define a 0-arity function a in the current scope with the running environment that just returns a 4 and does nothing else, or that b = a * 64 is a 0-arity function like the above and uses the a in the current scope, calls it, takes its value and returns it multiplied by 64. In old functional design see:

let tester =
  let a = 4
  let b c = a * c
  let d e f = e * b f
  d 4 4

Is essentially just like a 0-arity function a that returns an integer, a 1-arity function b that returns an integer, a 1-arity function d that returns a 1-arity function <anonymous> that returns an integer, and the top 0-arity tester function that returns an integer. In Functional programming everything is a Function in concept. :slight_smile:


#7

Is there any problem in code like this?

  var1 = 123
  var1 = get_new_value(var1, :something)
  var1 = get_new_value2(var1, :something2)

#8

The code you showed works. However, we usually try to reduce the number of single use variables. So, the “Elixir way” of writing that code is

var1 = 
  123
  |> get_new_value(:something)
  |> get_new_value(:something2)

Furthermore, if it was the last statement in a function, then we would drop the var1 like this:

def my_fun do
  # ...
  123
   |> get_new_value(:something)
   |> get_new_value(:something2)
end

#9

The other issue with rebinding is that the name var1 may not only be bound to a new value but that the value could be an entirely different type.

var1 = 123 # this is an integer
var1 = get_new_value(var1, :something) # this could be a tuple
var1 = get_new_value2(var1, :something2) # this could be a map

… which does not help code clarity.

Also another way of looking at the pipeline operator is that it “threads” the result of the previous function into the next function (which is why in Clojure it’s called the thread first macro). In a pipeline there is the expectation that the return type of the previous function is compatible the expected parameter type of the next function - there is no expectation of the values travelling through pipe being all of the same type.


#10

I’m not asking how to simplify that.

moreover, how’s that diffrent from?

def my_fun do
   # ...
  get_new_value(123, :something)
  |> get_new_value(:something2)
end

#11

my question was about multithreading safety and related stuff.


#12

Since everything is immutable and also there is nothing shared between 2 BEAM processes[1], there is no issue with that code in terms of concurrent processing.

[1] Well, in fact, large binaries are shared across processes, but as they are still immutable we do not get any problems because of this as well.


#13

That code ignores two guidelines:

I’m not sure how anybody could be expected to “divine” this given that this was the first mention of it and since Erlang/Elixir’s BEAM implements a share-nothing architecture (as @NobbZ already mentioned).

  • The BEAM runs inside an OS process
  • The BEAM starts a scheduler (i.e. with an OS thread) for each physical core available
  • The schedulers pre-emptively schedule BEAM processes on their respective cores.

i.e. there is no concept of “multi-threading” in Erlang/Elixir. If BEAM processes need to coordinate their activities they need to go through the BEAM process mailboxes.


#14

The difference is more obvious in a language that has both.

  • Bindings introduce new variables, while assignments use existing ones.

    my_var = 3 # bind my_var to 3
    assert my_var == 3
    
    # Assuming a version of Elixir with an assignment operator called `set`
    #set my_var = 3 # COMPILATION FAIL
    
  • Bindings are subject to their own lexical scope, while assignment makes the new value accessible through the scope that the initial binding was in.

    my_var = 3
    if true do
        my_var = 4
    end
    assert my_var == 3
    
    my_var = 3
    if true do
        set my_var = 4
    end
    assert my_var == 4
    
  • Bindings can use any (irrefutable) pattern, while assignments require an lvalue.

    {var1, var2} = {1, "super"}
    assert var1 == 1
    assert var2 == "super"
    
    {var1, var2} = {1, "super"}
    #set {var1, var2} = {2, "duper"}; # COMPILATION FAIL
    
  • Variable bindings may be of different types, even if they have the same name. In languages with a static type system, assignment cannot change a variable’s type:

    x = 1
    x = "awesome!"
    assert x == "awesome!"
    
    x = 1
    #set x = "awesome!" # COMPILATION FAIL
    

#15

That’s a good way to think of it, like OCaml has 3 syntax’s for this:

(* This creates a new immutable binding *)
let a = 42
(* `a` cannot be changed, only re-bound like how Elixir does it *)

(* This creates a new mutable binding *)
let b = ref 42
(* It can be changed within its scope *)
let () = b := 21
(* The `:=` operator means `to change where the binding points to` in a sense,
    It returns empty tuple, so you can pattern match that as above, or ignore it.
    It is indeed made purposefully cumbersome to minimize its use. *)

#16

get_new_value(get_new_value(123, :something), :something2)

that’s it.


#17

Maybe this is an intuitive way to think about the difference:

  • with assignment, you copy/move the value while the name (the memory location the name points to) stay the same.
  • with binding, you move the name (the memory location the name points to) while the value stays the same.

:slight_smile: