Reputation: 7204
I'm very new to Elixir and this simple problem is driving me nuts.
a = 0
if true do
a = 1 + 1
end
a = a + 1
IO.puts (a)
Interestingly this gives the correct value but also gives a warning:
warning: the variable "a" is unsafe as it has been set inside a case/cond/receive/if/&&/||. Please explicitly return the variable value instead. For example:
case int do
1 -> atom = :one
2 -> atom = :two
end
should be written as
atom =
case int do
1 -> :one
2 -> :two
end
Unsafe variable found at:
Untitled:5
3
I don't understand the warning message. What is the best way of doing this in Elixir?
Update: What about this condition too?
a = 0
b = 0
if true do
a = 1 + 1
b = 2 + 2
end
a = a + 1
b = b + 2
IO.puts (a)
IO.puts (b)
Upvotes: 23
Views: 18624
Reputation: 9791
I'm pretty new to Elixir but also pretty familiar with functional programming thru both languages in that family and more generally as a style of programming. Based on the blog post announcing the deprecation of this behavior [see below], the intent seems to be encourage more-idiomatic programming (among other things).
One advantage of avoiding this behavior is that extracting these blocks of code to a separate function becomes more trivial. A functional programming style encourages thinking of the behavior of programs as a series or sequence of transformations, not modifications, of data. That typical functional programming languages also provide immutable data, at least by default, while also, internally, sharing common or shared values among the data your program generates, encourages you to implement the transformations of data as pure functions, i.e. blocks of code that don't modify the state of existing data.
In Elixir, and other functional programming languages, the combination of pattern matching and powerful standard 'collection' types provides a clean and simple means of returning multiple values from a single function or code block. In contrast, in object oriented programming languages for example, one would typically return an object with multiple values accessible as members of that object's class. That's a perfectly valid pattern in Elixir or other functional programming languages too – see structs in Elixir – but it's unnecessary, and almost always less clear, when returning a relatively small number of values.
So your first example could be rewritten as:
a = 0
a = if true, do: 1 + 1, else: a
a = a + 1
IO.puts (a)
Your example is too contrived for there to be any obvious advantage. Your question implies a criticism, and I think it's valid, in that the else
, i.e. needing to explicitly 'no-op' the update of a
, is redundant. It's a real, albeit minor, 'fixed cost' of this style of programming. But you could easily encapsulate the idea of a 'maybe transform' behavior as a function or macro:
def maybe_transform(x, cond, f) do
if cond, do: f.(x), else: x
end
The real benefit of this style can be better seen with multiple possible transformations:
a = 0
a
|> maybe_transform(cond1, &transform_function_1/1)
|> maybe_transform(cond2, &transform_function_2/1)
|> maybe_transform(cond3, &transform_function_3/1)
where the functions transform_function_1
, transform_function_2
, transform_function_3
would be possibly called, depending on the relevant condition, on the (possibly) and successively transformed value of a
. Note that the |>
operator is passing the (possibly) transformed value of a
as the first argument to each call of maybe_transform
.
Your second example could be rewritten as:
a = 0
b = 0
{a, b} = if true, do: { 1 + 1, 2 + 2 }, else: {a, b}
a = a + 1
b = b + 2
IO.puts (a)
IO.puts (b)
Again, that example is so maximally contrived as to make the benefits of deprecating the imperative assignment behavior unclear.
In a comment on the currently accepted answer, you wrote:
If I have a complex maths problem to solve that needs to alter 40 variables with several nested ifs then I have to define {a,..,a40} for every nested if statement?
I can't think of any example off the top of my head that would involve 40 'return' variables where the calculations or transformations would all depend on the same complex conditions and not be structured in some way such that an imperative style would be clearer or obviously better in some way. A detailed, specific example would be helpful. Something that resulted in data like a vector of 40 values would often be 'structured' such that the map
or reduce
functions in the Elixir standard Enum
module would be typically clearer, in a functional programming style anyways, than the equivalent code involving imperative assignments, and you wouldn't typically need or want to maintain 40 separate variables for all of the values contained in a single vector either.
The problem I was working on when I ran into this involved building a list from two different possible sets of data; my first draft of a function to do so:
def build_list(x) do
new_list = []
if cond1 do
something = f1(x)
if cond2 do
new_list = [ f2(something) | new_list ]
end
end
if cond3 do
something_else = f3(x)
if cond4 do
something_completely_different = f4(something_else)
if test(something_completely_different) do
new_list = [ f5(something_completely_different) | new_list ]
end
end
end
new_list
end
There were a variety of ways I could have rewritten it but I settled on something like this:
def build_list(x) do
list_1 =
case cond1 do
false -> []
true ->
something = f1(x)
if cond2, do: [f2(something], else: []
end
list_2 =
if cond3 do
something_else = f3(x)
if cond4 do
something_completely_different = f4(something_else)
if test(something_completely_different) do
something_completely_different
else
[]
end
else
[]
end
else
[]
end
list_1 ++ list_2
end
Note that the new version behaves differently as [x | list]
returns a new list with x
prepended to the contents of list
whereas list_1 ++ list_2
returns a new list with list_2
effectively appended to list_1
. In my case that didn't matter.
And, because cond4
and test
were in actuality testing whether something_else
or something_completely_different
, which were themselves lists, were empty, and list ++ [] == list
, I ended up with something more like this eventually:
def build_list(x) do
list_1 =
case cond1 do
false -> []
true ->
something = f1(x)
if cond2, do: [f2(something], else: []
end
list_2 =
if cond3 do
f4(f3(x))
else
[]
end
list_1 ++ list_2
end
Part of what ended up helping me was that the standard functions and operators I used in the newer version handled 'degenerate' data, e.g. an empty list []
value, sensibly. My cond4
was checking that f3(x)
was not an empty list but f4
itself worked just fine given an empty list parameter, itself returning an empty list in that case. When using the syntax [x | list]
to generate a new list with x
prepended at the head, I had to check whether x
was itself an empty list, as otherwise it would append an empty list as the head element of the new list, but both list ++ x
and x ++ list
are both the same as just list
when x
is empty.
From the official blog post announcing the release of Elixir version 1.3 (the version in which the warning you observed was introduced to mark the relevant behavior as deprecated):
Deprecation of imperative assignment
Elixir will now warn if constructs like
if
,case
and friends assign to a variable that is accessed in an outer scope. As an example, imagine a function called format that receives a message and some options and it must return a path alongside the message:def format(message, opts) do path = if (file = opts[:file]) && (line = opts[:line]) do relative = Path.relative_to_cwd(file) message = Exception.format_file_line(relative, line) <> " " <> message relative end {path, message} end
The
if
block above is implicitly changing the value inmessage
. Now imagine we want to move theif
block to its own function to clean up the implementation:def format(message, opts) do path = with_file_and_line(message, opts) {path, message} end defp with_file_and_line(message, opts) do if (file = opts[:file]) && (line = opts[:line]) do relative = Path.relative_to_cwd(file) message = Exception.format_file_line(relative, line) <> " " <> message relative end end
The refactored version is broken because the
if
block was actually returning two values, the relative path and the new message. Elixir v1.3 will warn on such cases, forcing both variables to be explicitly returned fromif
,case
and other constructs. Furthermore, this change gives us the opportunity to unify the language scoping rules in future releases.
Upvotes: 8
Reputation: 9639
The warning is correct trying to prevent you from doing _ possibly dangerous_ thing. It is very well explained in the Elixir's 1.3 changelog.
Take a look at Deprecation of imperative assignment section, where it is explained (with example) here:
http://elixir-lang.org/blog/2016/06/21/elixir-v1-3-0-released/
Hope that helps!
Upvotes: 10
Reputation: 5812
In Elixir every statement returns the value. Instead of assigning variable in if
you can assign whole if
statement value into variable.
a = 0
a = if true do
1 + 1
else
a + 1
end
Upvotes: 42