Variable Scope

Elixir is lexically scoped. The basic unit of scoping is the function body. Variables defined in a function (including its parameters) are local to that function. In addition, modules define a scope for local variables, but these are accessible only at the top level of that module, and not in functions defined in the module.

Do-block Scope

Most languages let you group together multiple code statements and treat them as a single code block. Often languages use braces for this. Here’s an example in C:

 int​ line_no = 50;
 
 /* ..... */
 
 if​ (line_no == 50) {
  printf(​"new-page​​\f​​"​);
  line_no = 0;
 }

Elixir doesn’t really have blocks such as these, but it does have ways of grouping expressions together. The most common of these is the do block:

 line_no = 50
 
 # ...
 
 if​ (line_no == 50) ​do
  IO.puts ​"​​new-page\f"
  line_no = 0
 end
 
 IO.puts line_no

However, Elixir thinks this is a risky way to write code. In particular, it’s easy to forget to initialize line_no outside the block, but to then rely on it having a value after the block. For that reason, you’ll see a warning:

 $ ​​elixir​​ ​​back_block.ex
 warning: the variable "line_no" is unsafe as it has been set inside one of:
 case, cond, receive, if, and, or, &&, ||. Please explicitly return the
 variable value instead. Here's an example:
 
  case integer do
  1 -> atom = :one
  2 -> atom = :two
  end
 
 should be written as
 
  atom =
  case integer do
  1 -> :one
  2 -> :two
  end
 
 Unsafe variable found at:
  t.ex:10
 
 0

The with Expression

The with expression serves double duty. First, it allows you to define a local scope for variables. If you need a couple of temporary variables when calculating something, and you don’t want those variables to leak out into the wider scope, use with. Second, it gives you some control over pattern-matching failures. For example, the /etc/passwd file contains lines such as

 _installassistant:*:25:25:Install Assistant:/var/empty:/usr/bin/false
 _lp:*:26:26:Printing Services:/var/spool/cups:/usr/bin/false
 _postfix:*:27:27:Postfix Mail Server:/var/spool/postfix:/usr/bin/false

The two numbers are the user and group IDs for the given username.

The following code finds the values for the _lp user (and see the sidebar following for some notes on its layout).

basic-types/with-scope.exs
 content = ​"​​Now is the time"
 
 lp = ​with​ {​:ok​, file} = File.open(​"​​/etc/passwd"​),
  content = IO.read(file, ​:all​), ​# note: same name as above
 :ok​ = File.close(file),
  [_, uid, gid] = Regex.run(​~r/^lp:.*?:(\d+):(\d+)/​m, content)
 do
 "​​Group: ​​#{​gid​}​​, User: ​​#{​uid​}​​"
 end
 
 IO.puts lp ​#=> Group: 26, User: 26
 IO.puts content ​#=> Now is the time

The with expression lets us work with what are effectively temporary variables as we open the file, read its content, close it, and search for the line we want. The value of the with is the value of its do parameter.

The inner variable content is local to the with, and does not affect the variable in the outer scope.

with and Pattern Matching

In the previous example, the head of the with expression used = for basic pattern matches. If any of these had failed, a MatchError exception would be raised. But perhaps we’d want to handle this case in a more elegant way. That’s where the <- operator comes in. If you use <- instead of = in a with expression, it performs a match, but if it fails it returns the value that couldn’t be matched.

 iex>​ ​with​ [a|_] <- [1,2,3], ​do​: a
 1
 iex>​ ​with​ [a|_] <- nil, ​do​: a
 nil

We can use this to let the with in the previous example return nil if the user can’t be found, rather than raising an exception.

basic-types/with-match.exs
 result = ​with​ {​:ok​, file} = File.open(​"​​/etc/passwd"​),
  content = IO.read(file, ​:all​),
 :ok​ = File.close(file),
» [_, uid, gid] <- Regex.run(​~r/^xxx:.*?:(\d+):(\d+)/​, content)
 do
 "​​Group: ​​#{​gid​}​​, User: ​​#{​uid​}​​"
 end
 IO.puts inspect(result) ​#=> nil

When we try to match the user xxx, Regex.run returns nil. This causes the match to fail, and the nil becomes the value of the with.

A Minor Gotcha

Underneath the covers, with is treated by Elixir as if it were a call to a function or macro. This means that you cannot write this:

 mean = ​with​ ​# WRONG!
  count = Enum.count(values),
  sum = Enum.sum(values)
 do
  sum/count
 end

Instead, you can put the first parameter on the same line as the with:

 mean = ​with​ count = Enum.count(values),
  sum = Enum.sum(values)
 do
  sum/count
 end

or use parentheses:

 mean = ​with​(
  count = Enum.count(values),
  sum = Enum.sum(values)
 do
  sum/count
 end​)

As with all other uses of do, you can also use the shortcut:

 mean = ​with​ count = Enum.count(values),
  sum = Enum.sum(values),
 do​: sum/count