Tuple Calls

What do you mean by that?

3 Likes

Tuple calls are slated to be turned into an error in Elixir 2 Jose keeps saying he wants to do.

Technically it is for a good reason, the BEAM error messages when someone tries a tuple call accidentally on a module that is not tuple-callable is a bit ugly, but the feature is just so fantastically useful, it is basically first-class modules on the BEAM and that opens up so many possibilities that would be so very wordy otherwise!

Someone can accidentally do a tuple call, say, like this:

iex(12)> m = %{a: %{b: 42}}
%{a: %{b: 42}}
iex(13)> blah = Map.fetch(m, :a)
{:ok, %{b: 42}}
iex(14)> blah.b
** (UndefinedFunctionError) function :ok.b/1 is undefined (module :ok is not available)
    :ok.b({:ok, %{b: 42}})

What the user probably meant to do was this for the fetch line instead:

iex(13)> {:ok, blah} = Map.fetch(m, :a)

And this is a common bug caused by the lack of a typing system in Elixir (thus the compiler cannot catch it, although Dialyzer can, but how many people, especially newbies, use Dialyzer? I really think Dialyzer should run ‘with’ the compiler, and be optimized/PLT’d as much as possible with auto-updating of the PLT as necessary).

So yes, tuple calls are amazingly wonderful and I love them (I’m a long time erlang fan of tuple-calls), but they do make for some… interesting error messages when someone did not intend to use them (Newbie says: “Wtf? :ok.b/1? Wtf?”).

7 Likes

Well, what I meant was “What do you mean by ‘tuple-calls’”

I’ve never heard that term before and neither used in Erlang nor Elixir worlds.

But I think I roughly understand from your example what it does, or should do.

I do hope though, that m = IO; m.puts "foo" will continue to work?

2 Likes

Oooh, I forget that not many Elixir people know what a Tuple Call is. :slight_smile:

Basically when you ‘call’ a variable in erlang it can be a few different types, here are some of them (in Elixir syntax, these all work as it is, normal Elixir/BEAM stuff):

iex(15)> defmodule Bloop do
...(15)>   def id(x), do: x
...(15)> end
{:module, Bloop,
 <<70, 79, 82, 49, 0, 0, 4, 144, 66, 69, 65, 77, 69, 120, 68, 99, 0, 0, 0, 143,
   131, 104, 2, 100, 0, 14, 101, 108, 105, 120, 105, 114, 95, 100, 111, 99, 115,
   95, 118, 49, 108, 0, 0, 0, 4, 104, 2, ...>>, {:id, 1}}
iex(16)> a = fn x -> x end
#Function<6.52032458/1 in :erl_eval.expr/5>
iex(17)> b = &Bloop.id/1
&Bloop.id/1
iex(18)> c = {Bloop, 1, 2, 3}
{Bloop, 1, 2, 3}
iex(19)> d = %{a: 42}
%{a: 42}
iex(20)> e = IO
IO
iex(21)> a.(42)
42
iex(22)> b.(42)
42
iex(23)> c.id() # The tuple call is added to the end of the argument list
{Bloop, 1, 2, 3}
iex(24)> d.a
42
iex(25)> e.inspect(42)
42
42

Basically when you ‘call’ a variable in Erlang it can be an Atom (Module), or a tuple (Tuple Call).

A Tuple Call is just a Tuple where the first element is a Module atom, can have any number of elements, and the entire tuple is passed in as the last argument. An example:

iex(26)> defmodule Blah do
...(26)>   def id(x), do: x
...(26)>   def new(val), do: {__MODULE__, val}
...(26)>   def add(v, {__MODULE__, val}), do: new(val+v)
...(26)>   def get({__MODULE__, val}), do: val
...(26)>   def inspect(opts\\[], {__MODULE__, val}), do: IO.inspect(val, opts)
...(26)> end
{:module, Blah,
 <<70, 79, 82, 49, 0, 0, 7, 144, 66, 69, 65, 77, 69, 120, 68, 99, 0, 0, 1, 132,
   131, 104, 2, 100, 0, 14, 101, 108, 105, 120, 105, 114, 95, 100, 111, 99, 115,
   95, 118, 49, 108, 0, 0, 0, 4, 104, 2, ...>>, {:inspect, 2}}
iex(27)> b = Blah.new(21)
{Blah, 21}
iex(28)> b = b.add(21)
{Blah, 42}
iex(29)> b.get()
42
iex(30)> b.inspect()
42
42
iex(31)> b.inspect(label: "Vwoop")
Vwoop: 42
42
iex(32)> Blah.get(b) # Can use it normally too, you do not need to call it with tuple-calls
42
iex(33)> b = Blah.add(21, b)
{Blah, 63}
iex(34)> Blah.get(b)
63

Basically where a normal atom call like:

iex(35)> a = IO
IO
iex(36)> a.inspect(42)
42
42

Let’s you call a module, a tuple-call let’s you associate data with and pass in data along with that module name without needing to carry it in multiple bindings as you’d have to do otherwise. Like say you had an Enum module that could work on any ‘type’, even user-defined, without the need for protocols while still being fast, tuple-calls could allow that without any protocol-magic, and anything that fulfills the interface @behaviour could be used with that module, thus replacing the entire Access type Elixir has as well, if only each container was wrapped in such a tuple-call (could special case primitives like [] and %{} of course), and a user-container could fulfill the same interface by just returning something like {MySpecialModule, mydata} instead of just mydata, which is perfect if it is supposed to be opaque anyway as most special containers are.

But tuple-calls fill a very nice niche that is just not as easy to do otherwise and I do wish Elixir took advantage of them instead of ignoring them. :slight_smile:

As well as it does already. Tuple calls work fine as it is.

5 Likes

I would like tot add to @OvermindDL1’s explanation that Tuple Modules were an implementation mechanism of a feature of Erlang that was removed in OTP R16B. The only reason Tuple Modules themselves are still around is backwards compatibility. This is also the reason why no official Erlang documentation for them exists. It is possible they are themselves removed at some point, because they have never been part of the official Erlang standard.

And I think this is the reason that the Elixir Core team is not really enthusiastic about promoting their usage.

A second reason (and possibly the reason why they never moved into the Erlang standard) is that it is possible to write OOP-style code, dispatching on the module that happens to be inside the result of the previous function (such as the q.push(1).push(2).push(3) in @OvermindDL1’s post).

6 Likes

I thought they were pretty clear that they intended to keep Tuple Calls permanently? What they removed were parameterized modules (which meh, I’m for removing them anyway) and that used tuple calls. I remember talk of them fully legitimizing tuple calls in Erlang at one point (just not parameterized modules), but never removing them regardless.

That is not OOP style, not even remotely…
That is just the tuple-call version of Elixir’s piping |>, which of course puts it on the end of the argument list where piped values belong. :wink:

2 Likes

We both agree on it making more sense for piping to work on the last argument because of currying (and this would make quite a few functional patterns, such as applying applicative functors, a lot easier to express in Elixir!). However, this is a choice that cannot be altered anymore as it is deeply ingrained into the language.

The difference between tuple calls and piping, and why I call this ‘OOP-style’ is that you are dispatching functions based on the data type you happen to have at hand:

my_data_type
|> MyOneModule.bar(extra_argument)
|> MyOtherModule.baz()

Here, it is clear what code is called.

my_data_type
.bar(extra_argument)
.baz()

Here, which bar/2 is called depends on what my_data_type actually is bound to. Furthermore, which baz/1 is called depends on what this bar/2 returns. This is the kind of implicitness inherent in OOP programs because in objects, code and data lives together, and exactly what we try to avoid in functional programming.

6 Likes

It is identical to dispatching based on a module ID as well:

defmodule Blah do
  def send_message(msg) do
    send(NamedProcess, msg)
    Blah
  end
end

a = Blah
a.send_message(1).send_message(42).send_message(6.28).send_message("Hello world")

Just happens to be a function that returns the right thing in the right format, though in this case it is more obscure that a tuple call as it is obvious that push should be returning the updated structure, which it indeed does. Really though, if someone did that with different things that returned different tuples to different modules, that would be quite obscure and not recommended, but I do not even recommending doing that with pipes (when the type changes in pipes then I add intermediate bindings unless it is a trailing Enum.into/2 or something obvious like that).

It is not different then just calling a binding that has a module name bound to it, like a=IO; a.inspect(42). :wink:

Yep, just like a=IO; a.inspect(42) does.

First class modules in OCaml are certainly not even remotely OOP, and yet this is exactly how it is implemented in OCaml as well (internally it is also a tuple of data, though it does not need to carry the first element ‘type’ along like Erlang’s tuple calls do since it knows the type already) and is a common pattern (standard way is to name it SomeName.S where S is the recursive module signature of that module).

3 Likes

Criticism of tuple dispatch for the curious:

[erlang-questions] Proposal to remove tuple dispatches from Erlang (from José)

One major project using them is Webmachine, which used to be based on parameterized modules:

https://github.com/webmachine/webmachine/pull/93

1 Like

Yes, dynamic module dispatch is bad enough. Adding additional parameters makes it so much worse. This kind of dynamic module calling is generally very rare in Elixir and Erlang (outside of behaviours) and if it’s used, it’s usually wrapped in libraries.

Because dynamic calls are so rare, the dynamism of Elixir is a much smaller issue than one might think initially. That’s probably also why I don’t miss type system most of the time. The biggest source of “dynamism problems”, in my experience, stems from the fact that we have no idea what code foo.bar.baz is going to execute. It’s entirely based on runtime values. With fully qualified module calls, that’s not an issue - you always know what code will be executed.

8 Likes

And as I’ve mentioned back in the ol’ Erlang days (long long time ago!), essentially all of the Tuple Calls ‘cons’ go away if you type things properly (even dialyzer, though in-source typing would be more clear). It is still immutable, there is no mutation, not even any hint of a mutation (unless you are sending messages off to another process or so, but same issue with functions there), and the only real issue that I consider an issue is that there are then two ways to call the same function, which I do agree is annoying, but could be resolved if a function declaration that took a tuple call as its final argument could not be called ‘normally’ anyway (meaning you could also clean up error messages related to its arity and so forth).

I am quite a fan of removing parameterized modules, they added too many out-of-place bindings and made the implicit things about tuple-calls gone so only the explicit ways were left (I like explicit).

4 Likes

That’s awesome! I may have to start using that. Concerns over implicitness would be largely alleviated with better IDE-esque tooling.

Not too thrilled on the tuple syntax. It would be nicer as a struct for protocols.

2 Likes

Honestly, the variable ambiguity argument seems to be overdone in Elixir. Proper variable names and typespecs go a long way towards communicating your code. As long as you only use a single type to represent a concept, there shouldn’t be an issue with creating descriptive/unambiguous variables.

2 Likes

Structs don’t exist on the BEAM. ^.^

True true yeah, but I still like typed systems enforcing it. Way WAY too many bugs leak through dynamic typing everywhere.

3 Likes

No, it is not. :slight_smile: One implicitly passes arguments, the other does not. To see the issue with this approach and how it couples behaviour and data, just consider what would happen if you have queue.push(42) and then someone wants to add a new queue.new_function(bar) that you haven’t defined. The coupling make it impossible to extend it without introducing a series of extension mechanisms, such as monkey patching or inheritance, all of them unnecessary if you don’t couple them in the first place. None of those issues happen with behaviours or pipes, where all arguments are still explicitly given.

And as a consequence they are even no supported fully by tools. For example, Hipe will not perform tuple dispatches. For all purposes, it is an undocumented feature and it may be removed from Erlang anytime.

4 Likes

Elixir could just pattern match at the dot.

2 Likes

I can’t keep quiet here. :grinning: Yes, they kept tuple calls for backwards compatibility after they removed parametrized modules but IMAO they should have removed them as well and stuck BC. They were always an implementation hack. :rage: It is a pity that some of the libraries like dict work with them but my only defence there is that they came later.

And I’m whispering here, but I am not really a fan of pipes either. Shh. :hushed:

12 Likes

Heh, I view it that the atom-call is implicitly passing nothing, since there is no data to pass in. ^.^
Looks the same from that viewpoint. :slight_smile:

Exactly why there should be a @behaviour that could be checked by Dialyzer. :slight_smile:

Yeah, OCaml gets around that via it’s include keyword, can do something kind of like this (pseudo-code but the basic gist):

module MyExpandedList = struct
  include List (* Standard lib's List! *)

  let my_special_function a b c = a+b+c
end

(* Then you can use it straight and intercompatible with normal List function calls,
   or you can 'overwrite' the normal List in the current scope via: *)
module List = MyExpandedList (* Only for the current scope *)

Discourse highlights OCaml very poorly… ((*/*) are comments).

Oh I would never ever suggest tuple calls to replace piping, ever, I consider them as just a refinement on Behaviours while carrying data along without needing to carry two bindings (just one).

I know, still makes me sad. :wink:

Adds overhead, but yeah it could, that is how elixir does [indexing] currently, transforms it to a case, same with if's and more too.

Lol, I remember how vocal you were on the mailing lists back then about that. :wink:

I really think they were originally added to mimic OCaml’s First Class Modules, though you could actually mimic those a lot better nowadays via Maps (and it gives a single point of call too!:

defmodule Vwoop do
  def new(v) do
    %{
      get: fn -> v end,
      add: fn x -> Vwoop.new(x+v) end,
      new: &new/1,
    }
  end
end

Used as:

iex(41)> v = Vwoop.new(21)
iex> v.get.()
21
iex> v = v.add.(21)
iex> v.get.()
42

How’s that for more proper use of immutable closures? :wink:
But yeah, that is much more ‘normalish’ of OCaml’s First Class Modules (hmm, immutable prototype system?).
Could macro it for simplicity too (though you are always left with that dot before the ().

I only think the pipes pipe into the wrong position, even BEAM puts the ‘implicit tuple call’ at the end (plus it optimized the recursive stack to have the ‘changing arguments’ at the end and the unchanging arguments at the start last I checked the interpreter), but piping itself is awesome, consider that it is OCaml’s only one of 2 default non-math/list operator functions (the other is @@, which if OCaml’s operator precedence allowed for then it really should have been <|). :slight_smile:

3 Likes

That is not true. You get the least instructions when the result of the last function call is the first argument of the next function call. Not that it would matter much in real code, though.

3 Likes

It’s just tradition for me. Every language I’ve seen that has piping via main code or library’s from C++ to Haskell to OCaml to many many others all put it in the last position.

I do agree that the first position would make the most ‘logical’ sense, but about everyone followed the ML way of a function being a single argument to a single return, so even the non-ML languages followed it, so they had a programmatic reason that was required since they did not have the concept of Macro’s (at the time, now they have bigger). :slight_smile:

Oh wait, I mis-read that, not talking about pipes. ^.^;
I recall in the BEAM (at least when I last read it, I’ve no reason to think this has changed though?) that when arguments can be reused between function calls (like say a loop function calling itself, or a function calling another function that takes some of the same arguments) that it only popped off the stack up to the ‘removed’ ones and only replaced what was necessary, so in essence it did this:

def blah(a, b, c, d), do: vwoop(a, b, c, d+1)

That would only pop d off the stack, calculate d+1 onto the stack, then call vwoop.

def blah(a, b, c, d), do: vwoop(a+1, b, c, d)

This one would pop all the arguments off the stack, calculate a+1, put it on the stack, then put b, c, and d back on, then call vwoop.

As for pipes in some code it does not really matter as many calls are like:

val
|> blah0()
|> blah1(a)
|> blah2(b)
|> blah3(c)

But in the case you have stuff like:

val
|> blah0(a)
|> blah1(a, b)
|> blah2(a, b)
|> blah3(a, whatever)

Then piping into the last position would always be more efficient unless the piped value never changed (and even then I doubt the BEAM could optimize it knowing that it is the same value since the same binding is not used again).

2 Likes