About variables scope

Hello everybody,

I just noticed something that I admit I should have been notice way more earlier TBH…
Variables are scoped in ifs, conds, cases (and I bet it’s applicable to any block).

An example with a simplified snippet:

def doing_something do
  a = 1
  ...
  if some_true_condition do
    a = 2
    ...
  end
  IO.inspect(a)  # > Still 1 even if some_true_condition is true
end

Now I’m even surprised of how I didn’t got any bugs so far related to this behavior I wasn’t aware.
Maybe I’ve embraced FP well more than I think (so i’m glad).

But until now I didn’t come across (or maybe I didn’t notice) any resource explaining this behavior.
And it’s a really important behavior!

I searched on hexdocs.pm and looked on almost all the guides pages on elixir-lang and this is the only result I found, a changelog for version 1.3 where the following is stated:

Elixir will now warn if constructs like if , case and friends assign to a variable that is accessed in an outer scope.

And contrary to that there isn’t any warning anymore (on v1.10 btw).

I thought replacing the if construct by assigning from the whole block like so:

a =
  if some_true_condition do
    ...
    2
  end

But in this case we are still changing the value with nil if some_true_condition is actually false instead of doing nothing.

If you happen to have to do something equivalent (changing a value in a block) how do you handle it?
Also if you have any more information about this behavior, I’m interested to learn more.

Thank you!

It is more about scoping than FP…

https://elixir-lang.readthedocs.io/en/latest/technical/scoping.html

I would do it like this if needed

a = if ..., do: 2, else: other_value
3 Likes

Remember that you are not changing the value of a. What you are really doing is creating a new variable with the same name (and deleting the old variable).

6 Likes

It is more about scoping than FP…

I meant, FP drastically reduced the usage of ifs.

I would do it like this if needed

a = if ..., do: 2, else: other_value

I was more talking about changing a value or not.
Here with using else you’ll still change the value…

Or do you?
Indeed you can simply affect the variable to itself again:

a = if ..., do: 2, else: a

So, yes this can do the trick.

Thank you for the link too BTW!

Indeed! My bad!
I should have say re-binding the variable…

1 Like

True, and with that in mind you clearly see that you cannot conditionally rebind it or not.

In the end, instead of

a = if ..., do: 2, else: other_value

You will write the following

a = if ..., do: 2, else: a

But I’ve found this to be a very rare construct, generally hidden by a more complex one.

FWIW, I find myself using this with Ecto.Multi when operations should only be done sometimes:

multi =
  Ecto.Multi.new()
  |> Ecto.Multi.insert(:a, some_changeset)
  |> Ecto.Multi.update(:b, some_other_changeset)

multi =
  if control_variable do
    Ecto.Multi.insert(multi, :c, optional_changeset)
  else
    multi
  end
4 Likes

I my case it was also related to a use case with Ecto for a custom validation of a changeset.

Anyway, does anyone know why there is not anymore any warning as it was the case in Elixir 1.3?

Because in 1.7 or 1.8 the “imperative assignment”, which previously issued the warning and changed the value, got removed.

I do consider this a breaking change in elixir, others say the imperative assignment before was a bug that has been fixed with more than enough time of warning.

3 Likes

TIL that there was the “imperative assignment” in Elixir…

So if I correctly understood, now we have what’s a pattern matching, and a variable binding when a variable is on the left hand side (without the pin ^)… But back then there was an other behavior called “imperative assignment”?

There’s some nuance to Elixir scoping because what would be “reassignment” in other languages is really more of a “rebinding”:

iex(1)> foo = 123
123
iex(2)> thing = fn -> foo end
#Function<21.126501267/0 in :erl_eval.expr/5>
iex(3)> thing.()
123
iex(4)> foo = 456
456
iex(5)> thing.()
123

The anonymous function in thing captures the value of foo when it’s initially evaluated in the second expression. Subsequent “reassignment” of foo is really rebinding the name, so the anonymous function retains its original reference.

This behavior is intended to be more ergonomic than Erlang, which uses the pattern-matching approach and allows exactly one assignment / binding for a name. In that style, the example above is:

iex(1)> foo1 = 123
123
iex(2)> thing = fn -> foo1 end
#Function<21.126501267/0 in :erl_eval.expr/5>
iex(3)> thing.()
123
iex(4)> foo2 = 456
456
iex(5)> thing.()
123

Written in this style, it’s much more apparent that the second assignment shouldn’t affect the first binding.

3 Likes

Yes, I just rebind in that case…

1 Like

Your first example is interesting because it’s the opposite of a closure.
And for a FP language it makes actually total sense (because of side effects free immutability etc.)

But now I’m here is there any closure mechanism in Elixir?

Regarding for example Clojure which is also FP, do they have closures? (I always assumed that the name had something to do with closures…)

Anonymous functions are closures:

iex(5)> x = 10
10
iex(6)> fun = fn y ->
...(6)>   y + x
...(6)> end
#Function<7.91303403/1 in :erl_eval.expr/5>
iex(7)> fun.(4)
14
iex(8)> x = 2
2
iex(9)> fun.(4)
14

Note how rebinding the value of x on line iex(8) didn’t change the output of fun. fun closed over the variables it referred to.

3 Likes

Well I was talking about the mechanism provided in closures where the parent scope is still available even the scope happening after the closure definition.

In Elixir the parent scope is available but only the scope until the closure, not after, which is not really a closure then. Or not the one I’ve learned…

For instance in your example x in the parent scope is available in the function but it’s somehow frozen to that moment (with the value of 10). So when x is modified after the function definition (it becomes 2), it’s still available (hence being a closure) but not with its current value (2), only the old one at that moment (10), so the function is not really a closure.

I mean at least closures in JavaScript work like this (but I guess it’s the same for all the other languages when talking about closures).

You can easily try the following in the developer console of you browser (here an example output in Chrome).

>  x=10
<· 10
>  fun = (y) => {return x+y}
<· (y) => {return x+y}
>  fun(4)
<· 14
>  x=2
<· 2
>  fun(4)
<· 6

But I might be wrong since I’m not really a seasoned JS dev and even less a seasoned FP dev…

What you’re proving is precisely that rebinding isn’t mutability. The function closes over the value of x. That value isn’t changed when you rebind x. If it changed in the function then x is in fact not immutable. Javascript does not have immutability, so it behaves the way you observe. Closures in immutable languages behave the way you observed in Elixir.

5 Likes

Well I missed that!

You’re totally right and it even makes total sense!

Thank you for this precision!

1 Like

Just my 2 cents:

def parsed(string) do
    # some other code here
    #...

    # Problem: I need to parse xml document with multiple broken datetime formats
    #    to_date and to_date_time returns either value or nil
    #
    #    string - source value
    #    value - parsed one

    value = value || to_date_time(string)

    value = value || if to_date(string), do:
      to_date_time("#{string}T00:00:00+03:00")

    value = value || if String.contains?(string, "+"), do:
      String.replace(string, "+", "T00:00:00.000+") |> to_date_time()
    
   value || string
end

I dont like this solution but atm Im ok with it

cond do
  to_date_time(string) = datatime -> datetime
  to_date(string) = date ->  to_date_time("#{string}T00:00:00+03:00")
  String.contains?(string, "+") -> to_date_time(String.replace(string, "+", "T00:00:00.000+"))
  true -> string
end
2 Likes