神刀安全网

Elixir's (Almost) Insane Flexibility: “Mutable” Iteration via Macros

Comparing iteration in JavaScript and Elixir

In JavaScript (and languages like it), it is common to see code like this:

// JavaScript  let array = [-1, 2, -3, 4] let sum = 0  for (let i = 0; i < array.length; i++) {   if (array[i] < 0)     continue    sum = sum + array[i] }  assert(sum == 6) 

This code is doing a pretty simple task. It calculates the sum of all elements in a which are positive. However, it’s difficult to read at a glance, and this style of iteration is error prone.

An idiomatic translation of this code to Elixir would be:

# Elixir  sum =   [-1, 2, -3, 4]   |> Enum.reject(fn elem -> elem < 0 end)   |> Enum.sum()  assert sum == 6 

The Elixir version is more declarative, and certainly easier to read. (To be fair, JavaScript now includes functions like Array.prototype.filter and Array.prototype.reduce that allow you to use a similar style.)

However, a new user might initially try something familiar from other languages and run into some problems:

# Elixir  sum = 0  Enum.each [-1, 2, -3, 4], fn elem ->   unless elem < 0 do     sum = sum + elem   end end  assert sum == 6 # Failure: sum == 0 

Why doesn’t this work?

In languages like JavaScript, variables which are captured by a closure (or from enclosing scopes, like in the first example above) are captured by reference:

// JavaScript  let a = 1  let get = () => a let set = (value) => a = value  set(2)  assert(get() == 2) 

However, in Elixir, variables are captured by value. When we refer to sum in the anonymous function we are passing to Enum.each/2 , we are referring to the value that sum had at the time we created the function:

# Elixir  a = 1  get = fn -> a end set = fn value -> a = value end  set.(2)  assert get.() == 1  a = 2  assert get.() == 1 

Additionally, in Elixir, even when we “reassign” a variable, we are not actually changing the original variable. Code like this:

# Elixir  a = 1 a = 2  assert a == 2 

is actually equivalent to the following code:

% Erlang  A1 = 1, A2 = 2, assert(A2 =:= 2). 

So, in our example before, set is not actually assigning to the original a . It’s assigning to a “new” a , and the original a has not changed.

To each their own…

Given everything we just discussed, this code may be somewhat shocking:

# Elixir  use MutableEach  sum = 0  each elem <- [-1, 2, -3, 4], mutable: {sum} do   if elem < 0,     do: continue    sum = sum + elem end  assert sum == 6 

“How is this possible in Elixir? I thought we couldn’t refer to variables by reference!”, you might say. Well, thanks to the power of macros, almost anything is possible in Elixir.

Introducing: MutableEach , a library which implements “mutable” iteration in Elixir.

Ultimately, the example above expands to something that looks somewhat like:

sum = 0  {sum} =   Enum.reduce_while [-1, 2, -3, 4], {sum}, fn elem, {sum} ->     try do       if elem < 0,         do: throw {:mutable_each_continue, {sum}}        sum = sum + elem       {:cont, {sum}}     catch       {:mutable_each_continue, mutable} -> {:cont, mutable}       {:mutable_each_break, mutable} -> {:halt, mutable}     end   end  assert sum == 6 

MutableEach mostly relies on Enum.reduce_while/3 and throw . Under the hood, there is still no actual mutability. The variables that are declared as “mutable” are simply provided as an accumulator within reduce_while , automatically returned at the end of each iteration, and then exported back into the original vars after the reduce is complete.

continue and break are implemented as macros which throw a tuple containing an atom representing the type of interrupt ( continue or break ) and the “mutable” variables. The function generated and passed to reduce_while contains a catch clause that returns {:cont, values} or {:halt, values} depending on the type of interrupt that was thrown.

Just another great example of how powerful and flexible Elixir is, thanks to macros.

Don’t try this at home

I probably shouldn’t have to say this, but this was simply an experiment, and MutableEach should not be used in your Elixir code. Simply using Enum.reduce_while/3 (no throw needed, probably!) directly in your own code is more explicit, and explicit is better than implicit.

If you enjoyed this blog, follow me on Twitter here !

转载本站任何文章请注明:转载至神刀安全网,谢谢神刀安全网 » Elixir's (Almost) Insane Flexibility: “Mutable” Iteration via Macros

分享到:更多 ()

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址