[Notions] extend function capture syntax

67 views
Skip to first unread message

Rich Morin

unread,
Jul 2, 2019, 12:52:51 PM7/2/19
to elixir-lang-core
I enjoyed reading "[elixir-core:8881] [Proposal] identity function" and the
ensuing discussion. It strikes me that some simple extensions to Elixir's
function capture syntax might:

- clean up the appearance of function captures
- make existing functions more generally useful
- reduce the need for special-purpose lambdas

When a captured function is being used as an argument, it may be possible
to infer its arity. So, maybe we can leave this out:

Enum.map(&id)
Enum.sort_by(&String.downcase)

Many named functions take multiple arguments, so they can't be used in
function captures. Allowing arguments could extend their reach:

Enum.sort_by(&elem(1)) # sort by the 2nd element
Enum.map(&Tuple.append(42)) # append 42 to each tuple

On a loosely related note, Clojure gets a lot of mileage out of some
higher-order abstractions (eg, seqs). There may be opportunities for
Elixir to follow their lead. For example, some Enum and List functions
might be applicable to Tuples. Similarly, String functions such as
downcase/1 could be extended to accept atoms, etc.

(ducks)

-r

Louis Pilfold

unread,
Jul 2, 2019, 1:19:04 PM7/2/19
to elixir-lang-core
Hiya

Given Elixir has very limited ability to infer information at compile time how will the compiler know which function to capture? It is not possible to create a multiple-arity anonymous function on the BEAM.

Cheers,
Louis


--
You received this message because you are subscribed to the Google Groups "elixir-lang-core" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elixir-lang-co...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/elixir-lang-core/3A20E4BD-9648-4872-852A-C5FFDD0A31D5%40gmail.com.
For more options, visit https://groups.google.com/d/optout.

José Valim

unread,
Jul 2, 2019, 1:57:28 PM7/2/19
to elixir-l...@googlegroups.com
> When a captured function is being used as an argument, it may be possible
> to infer its arity.  So, maybe we can leave this out:
> Enum.sort_by(&String.downcase)

How would Elixir know if you meant downcase/1 or downcase/2
on functions like Enum.group_by which accept multiple arities?
We would either need a type system or currying (too expensive at
runtime on the BEAM) or closures with multiple arities (not available
today) or a combination of those for it to work reliably and without
surprises.

For example, if I write this:

fun = get_me_some_function()
Enum.sort_by(fun)

If get_me_some_function() returns &String.downcase, how would
we know which one to invoke?

Don't get me wrong, I would love this feature. I love that in Clojure
I can do this: (reduce + coll) and they call +/0 for the initial accumulator
and +/2 on the reducing. But making this actually work in Elixir is way
beyond "simple extensions".

> Many named functions take multiple arguments, so they can't be used in
> function captures.  Allowing arguments could extend their reach:
> Enum.sort_by(&elem(1))

And how would I know if you meant &elem(&1, 1) or &elem(1, &1)?
Maybe you could say "the first element has to be a tuple and we are
passing an integer". But what if we have this:

x = result_from_another_function()
Enum.sort_by(&elem(x))

Now we don't know either the value of x nor the value being sorted on.

You could also say "we will always pass it as first argument".
But if you have both fun/1 and fun/2, how do you know if:

&fun(1)

Means "fn x -> fun(x, 1) end" or "fn -> fun(1) end"?

Those features may introduce a bunch of ambiguity once
you consider them within the whole context of the language.
And we definitely don't want to introduce something that works
on very specific cases to save on typing 2 characters.

> On a loosely related note, Clojure gets a lot of mileage out of some
> higher-order abstractions (eg, seqs).  There may be opportunities for
> Elixir to follow their lead.  For example, some Enum and List functions
> might be applicable to Tuples.  Similarly, String functions such as
> downcase/1 could be extended to accept atoms, etc.

Enumerable is Elixir's "equivalent" to seqs. It works with a huge variety
of data types and it can be extended. But it doesn't work with tuples
on purpose: tuples are not meant to be enumerated. If you are thinking
about enumerating tuples, you should most likely rethink how you are using
them.

And in many cases, yes, we could make the code more generic. We could
automatically cast atoms to strings in the String module. But the more generic
the code is, the harder it becomes to understand what it is doing. Being able to
write assertive code is important for readability. At least for me.

José Valim
Skype: jv.ptec
Founder and Director of R&D


Roman Smirnov

unread,
Jul 2, 2019, 6:40:01 PM7/2/19
to elixir-lang-core
Seems you would like to have currying in Elixir.
It's not possible in general way on BEAM, but I had fun with doing it some years ago:
Enum.map(rcarry(&Tuple.append/2, 42))

вторник, 2 июля 2019 г., 19:52:51 UTC+3 пользователь Rich Morin написал:

Rich Morin

unread,
Jul 2, 2019, 9:27:52 PM7/2/19
to elixir-lang-core
Thanks for all the thoughtful responses. Also, apologies for the ambiguities and
omissions in my original note. As so often happens, some of the things I had in
mind didn't make it into my email. (sigh)

In this note, I'm only considering the case of named functions that are explicitly
handed other named functions as arguments, via function capture. So, for example,
we don't have to worry about dealing with variables which are bound to a function.

# Inferring arity of captured functions

When a captured function (&bar) is being used as an argument to another function
(foo), it may be possible to infer bar's arity. In the case of library functions,
this information should be available from the function's typespec. For example,
https://hexdocs.pm/elixir/Enum.html#group_by/3 tells us that key_fun and value_fun
both have arity 1:

group_by(enumerable, key_fun, value_fun \\ fn x -> x end)

group_by(t(), (element() -> any()), (element() -> any())) :: map()

So, we should be able to write something like this:

list = ~w{ant buffalo cat dingo}

list |> Enum.group_by(&String.length)
# %{3 => ["ant", "cat"], 5 => ["dingo"], 7 => ["buffalo"]}

list |> Enum.group_by(&String.length, &String.first)
# %{3 => ["a", "c"], 5 => ["d"], 7 => ["b"]}

To clarify my motivation, I'm not trying to save the effort of typing the arity
information. Rather, I'm trying to cut down on the amount of clutter on the page
and (perhaps) the effort of reading it. I also want to get the "/1" syntax out
of the way to allow for the following notion.

# Adding arguments to captured functions

Many named functions take multiple arguments, so they can't be used in function
captures. Allowing arguments could extend their reach and reduce the need for
special-purpose lambdas. Here is some proposed syntax:

list = [
{ :status, 2, "This is a minor problem." },
{ :status, 1, "This is a major problem." }
]

list |> Enum.sort_by(&elem(1))

which could replace complected horrors such as:

list |> Enum.sort_by(fn {_, x, _} -> x end)
list |> Enum.sort_by(fn x -> elem(x, 1) end)

https://hexdocs.pm/elixir/Enum.html#sort_by/3 tells us that its mapper function
needs to have arity 1: "(element() -> mapped_element)". Although we're using
elem/2, we're also handing it an argument, so the arity math comes out even...

-r




Alexis Brodeur

unread,
Jul 3, 2019, 8:45:35 AM7/3/19
to elixir-lang-core
Let me reformulate that,

If no capture arguments (i.e.: `&1`, `&2`, etc.) are used in a capture function and the capture function is simply a function call (of the form `&my_function(...)` or `&my_function`, `&1` will automatically be inlined as the first argument of the captured function, thereby removing the need to know arity at compile time.

Meaning (pseudocode warning):
&identity == &identity(&1)
&role?(:admin) == &role?(&1, :admin)

&role?(&2) != &role?(&1, &2) # capture argument, so no inlined first argument

If we go in this direction, why not add something like lens, a capture structured like a property access.  `&.my_property` could translate to `&(&1.my_property)` ?

I think this is an interesting feature proposal, and both changes are backward compatible.

Tyson Buzza

unread,
Jul 3, 2019, 8:56:12 AM7/3/19
to elixir-l...@googlegroups.com
Would this mean

&(&1) == &() 

--
You received this message because you are subscribed to the Google Groups "elixir-lang-core" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elixir-lang-co...@googlegroups.com.

Alexis Brodeur

unread,
Jul 3, 2019, 8:58:31 AM7/3/19
to elixir-lang-core
Also, these should also work:
&String.downcase == &String.downcase(&1)
&callback.() == &callback.(&1)
&role?(role) == &role?(&1, role)


Alexis Brodeur

unread,
Jul 3, 2019, 8:59:13 AM7/3/19
to elixir-lang-core
No, since a capture argument is used in `&(&1)`, it would not be affected.
To unsubscribe from this group and stop receiving emails from it, send an email to elixir-l...@googlegroups.com.

Alexis Brodeur

unread,
Jul 3, 2019, 9:01:54 AM7/3/19
to elixir-lang-core
Opps, I read too fast.

No, the form would need to explicitly match `&function` or `&function(...)`, meaning `&(...)` would not be treated differently and current use of the form `&function(...)` must use capture arguments or it will not compile, meaning no breaking changes.

José Valim

unread,
Jul 3, 2019, 10:24:36 AM7/3/19
to elixir-l...@googlegroups.com
As detailed in my previous reply, when discussing such extensions to the language, it is very important to go beyond the trivial examples. The examples above look obvious because we all know how String.downcase/1 behaves. We know it expects one argument. So looking at examples we can "guess" how we want the code to behave

But if I look at this code:

&SomeModule.some_function

It actually has two possible interpretations:

fn -> SomeModule.some_function end
fn x -> SomeModule.some_function(x) end

None of them are possible in the language today, but once we do allow the proposed syntax, we are introducing this ambiguity. Sure, the compiler will know which one to pick, but for every person reading the code, your brain now has to parse it, interpret it, and resolve it accordingly.

I have given more examples in an earlier reply so I won 't repeat those, but in a nutshell we are not adding more ambiguity to the language. The cost it has on reading the code is not worth saving 2-3 characters.


José Valim
Skype: jv.ptec
Founder and Director of R&D

To unsubscribe from this group and stop receiving emails from it, send an email to elixir-lang-co...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/elixir-lang-core/ca1cc996-414c-473e-ad75-6d799ccb4d82%40googlegroups.com.

Alexis Brodeur

unread,
Jul 3, 2019, 10:33:07 AM7/3/19
to elixir-lang-core
Thanks for the feedback.

I have to agree that it will indeed break the motto: explicit, not implicit.

It is a nice to have feature, but in retrospect, I agree that sacrificing code readability will counterbalance the meager improvement to productivity it will bring (if any).
Reply all
Reply to author
Forward
0 new messages