Tuple Calls

BEAM is a register-based virtual machine, not a stack-based one. There are X and Y registers (1024 of both). X registers are where regular operations happen. Y registers are slots on the BEAM stack. Arguments to a function are passed in X0-X(n-1) registers. The return value is always in the X0 register. The X registers are caller-saved.

EDIT: registers are 0-indexed, not 1-indexed.

3 Likes

Given the following code:

defmodule Test do
  def test(x, y) do
    x + y
  end

  def test_first do
    x1 = test(1, 1)
    x2 = test(x1, 1)
    test(x2, 1)
  end

  def test_last do
    x1 = test(1, 1)
    x2 = test(1, x1)
    test(1, x2)
  end
end

We get the following assembly (stripped down for brevity)

{function, test_first, 0, 9}.
  {label,8}.
    {func_info,{atom,'Elixir.Test'},{atom,test_first},0}.
  {label,9}.
    {allocate,0,0}.
    {move,{integer,1},{x,1}}.
    {move,{integer,1},{x,0}}.
    {call,2,{f,7}}.
    {move,{integer,1},{x,1}}.
    {call,2,{f,7}}.
    {move,{integer,1},{x,1}}.
    {call_last,2,{f,7},0}.

{function, test_last, 0, 11}.
  {label,10}.
    {func_info,{atom,'Elixir.Test'},{atom,test_last},0}.
  {label,11}.
    {allocate,0,0}.
    {move,{integer,1},{x,1}}.
    {move,{integer,1},{x,0}}.
    {call,2,{f,7}}.
    {move,{x,0},{x,1}}.
    {move,{integer,1},{x,0}}.
    {call,2,{f,7}}.
    {move,{x,0},{x,1}}.
    {move,{integer,1},{x,0}}.
    {call_last,2,{f,7},0}.

The last-argument-chaining version has the extra move instruction to move from the return position in X0 to the last argument position in X1. But as I said before, this level of difference, probably won’t matter in real-life programs, since there are additional instruction-fusions and optimisations going on in the loader.

2 Likes

Exactly my point. :slight_smile:

It only really matters on a tight looping/recursive function, where it did provide a sizable performance difference when I tested it long ago.

(Sent too soon, so edit:)
And I know the BEAM is register based, but when it executes it then it ran it as a stack (or the JIT did, one or the other…).

2 Likes

The main drawback of piping with the first argument is that it conflicts with partial function application.

Partial function application is a very powerful tool, and should be more well-known within the Elixir community:

iex> [1,2,3] |> FunLand.map(Currying.curry(&+/2)) |> FunLand.apply_with([10, 11, 12])
[14, 15, 16, 15, 16, 17, 16, 17, 18, -6, -7, -8, -5, -6, -7, -4, -5, -6, 40, 44,
 48, 50, 55, 60, 60, 66, 72]
iex> import Currying
iex> [curry(&+/2), curry(&-/2), curry(&*/2)] |> FunLand.apply_with([4,5,6]) |> FunLand.apply_with([10,11,12])
[14, 15, 16, 15, 16, 17, 16, 17, 18, -6, -7, -8, -5, -6, -7, -4, -5, -6, 40, 44,
 48, 50, 55, 60, 60, 66, 72]
iex> maybe_num1 = FunLandic.Maybe.just(10)
iex> maybe_num2 = FunLandic.Maybe.just(20)
iex> FunLand.map(maybe_num1, Currying.curry(&+/2)) |> FunLand.apply_with(maybe_num2)
FunLandic.Maybe.just(30)
iex> maybe_num2 = FunLandic.Maybe.nothing()
iex> FunLand.map(maybe_num1, Currying.curry(&+/2)) |> FunLand.apply_with(maybe_num2)
FunLandic.Maybe.nothing()

Partial function application (and its flip-side, currying) are very powerful functional programming techniques, but they have been unsupported by Erlang (probably because of its basis in Prolog, which is also where it obtained its ‘functions with different arities are different functions’ notion that is definitely related to this, from), the only possibility being to build a library-level wrapper around it that will not really be able to be optimized by the compiler.

(I know of two approaches:

  1. Create a macro that defines function clauses with less parameters given than required, that returns clauses to the higher-level function. This is what the curry library does. Main drawback: Impossible to use for functions with multiple clauses.
  2. Check the actual arity of the called function, and if the amount of supplied arguments is less, create a new anonymous function where the rest of the parameters could be passed in. This is what the Currying library does. (disclaimer: I wrote Currying)

)
Neither approach is probably very performant, as they are library-level constructs, rather than language-level.

That being said, Elixir’s & shorthand function syntax is a good alternative for most situations. (I don’t remember where, but José mentioned it as “Elixir’s best feature” somewhere. If he was jesting or serious, I do not know though :stuck_out_tongue_winking_eye:).

5 Likes

I don’t remember saying that but probably jesting as I don’t consider it the best feature. :slight_smile:

In any case, currying was thrown out of the window when we decided to be fully compatible with Erlang (i.e. name/arity) and also due to its performance costs. Dynamic loading and lack of a static type system means it is hard to avoid creating intermediate lambdas - which makes currying expensive. For better or worse, it is an idiom that is unlikely to ever be first-class in Elixir.

5 Likes

I still think there could be an Elixir-with-typing for that. ^.^

2 Likes

I’ve kind of wondered, wouldn’t it be possible to have another operator, maybe ‘<|’ that pipes in from the right? Just sayin’…

2 Likes

That is common in other languages, especially ML/Haskell’y languages, like:

42
|> someFunc 6.28 <| anotherFunc "bloop"
|> moreFunc

Would be this in elixir’ise:

42
|> someFunc(6.28, anotherFunc("bloop"))
|> moreFunc()

Although in the ML’y/Haskell’y languages the <| expands to basically this:

42
|> someFunc 6.28 (anotherFunc "bloop")
|> moreFunc

In this short example it doesn’t really show it off, but it is most used in basically preventing having to use (usually a lot more than what is shown here) of parenthesis.

But yes, you could make something like that in Elixir too, but eh.

3 Likes

When tuple calls are that useful for some and accidents for others, is it potentially senseful to default for errors and make it optionally possible via a compiler flag or so.

I think this thread is worth closing. A lot of good discussion happened initially, but it’s been resurrected twice now months after the fact.

Substantive efforts to develop the conversation around tuple calls can, if desired, happen in a new thread dedicated to that direction.

3 Likes

Yes. Tuple calls are already disabled by default on Erlang/OTP 21 and they explicitly require a @compile option on every module that needs them.

Therefore even more reason to close this thread. :slight_smile:

5 Likes