tompave
tompave

Reputation: 12412

Elixir anonymous functions, captures, and partial applications

I'm learning Elixir and I'm currently dealing with the semantics of captures and partial application of functions.
I'm already familiar with the fn(x)-> x + 1 end syntax.

I've noticed what I consider a surprising behavior. For example, I've observed that the last expression in this snippet returns false:

f = &(&1 <> "-X")
# #Function<6.50752066/1 in :erl_eval.expr/5>

is_function f
# true

is_function &(&1 <> "-X")
# true

f == &(&1 <> "-X")
# false

Which I would interpret as meaning that "recreating" the anonymous function &(&1 <> "-X") on the fly returns a value that does not equal the value stored in f (according to the == semantics).

That would be fine, and this other snippet would confirm my theory:

g = &(String.upcase(&1))
# &String.upcase/1

is_function g
# true

is_function &(String.upcase(&1))
# true

g == &(String.upcase(&1))
# true

There, g would equal the recreated anonymous function because – maybe – capturing an existing named function is optimized by the compiler, and the same value is returned each time. The return value of the first line in the second snippet seems to confirm the idea that the two cases are treated differently.

I then tried with a "known" function, meaning that it is already in existence:

f = &(&1 <> "-X")
z = &(f.(&1))
#Function<6.50752066/1 in :erl_eval.expr/5>

z == &(f.(&1))
# false

The last statement is false again.
I would have expected &(f.(&1)) to be treated in a similar way to &(String.upcase(&1)), since both already exist.

What are the semantics of function captures, then?

Upvotes: 3

Views: 1162

Answers (1)

michalmuskala
michalmuskala

Reputation: 11278

You are right about the fact that capturing a named function is optimized, and thus returns the same value each time.

Further down I'll often refer to anonymous functions by "funs" and to module, function and arity of a function by "mfa".

The fact why there is no equality when capturing a well-known anonymous function is related to how that optimization works. When creating a fun from a named function instead of storing the whole anonymous function, the compiler stores the module name, the function name and the arity of the named function. This is not possible with "recapturing" a well-known anonymous function - you have to create a new fun each time.

Disclaimer: this analysis is not based on the deep knowledge of the compiler, but familiarity with the External Term Format (result of calling :erlang.term_to_binary/1), which is described in http://erlang.org/doc/apps/erts/erl_ext_dist.html.

There are two distinct types for anonymous functions in ETF:

  • FUN_EXT/NEW_FUN_EXT - encoding general anonymous functions. This encodes basically a reference to a particular module's (the module the fun is defined in) fun table.
  • EXPORT_EXT - encoding anonymous functions from named functions in the form of module name, function name and arity.

This gives also an insight into how anonymous functions are implemented: each module has a table of all the anonymous functions that occur inside. I'd suspect the compiler is doing some sort of lambda lifting in order to construct such a table. The funs created from named functions are encoded differently - as the mfa and are probably not stored in the fun table.

Upvotes: 6

Related Questions