iphaaw
iphaaw

Reputation: 7204

Elixir: Set variable in if statement

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

Answers (3)

Kenny Evitt
Kenny Evitt

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 in message. Now imagine we want to move the if 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 from if, case and other constructs. Furthermore, this change gives us the opportunity to unify the language scoping rules in future releases.

Upvotes: 8

Paweł Dawczak
Paweł Dawczak

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

PatNowak
PatNowak

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

Related Questions