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.
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 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).
| 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.
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.
| 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.
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 |