Capture containing at least one argument - CompileError

core
language-implementation
#1

Hello.

I’m relatively new to Elixir, but not to programming generally.

In the learning process I’ve been recently struck with following inconsistency, at “capturing” an anonymous function with & operator:

iex> &(:just_return_this_atom)
** (CompileError) iex:350: invalid args for &, expected an expression in the format of &Mod.fun/arity, &local/arity or a capture containing at least one argument as &1, got: :just_return_this_atom

I’ve read the docs, where the limitation is stated:

The only restrictions when creating anonymous functions is that at least one placeholder must be present, i.e. it must contain at least &1 , and that block expressions are not supported:

What is the rationale behind this limitation ? I’ve already searched some mailing lists but so far no satisfactory explanation has been found.

I’m aware by choosing unary ampersand operator for converting both - normal expression and function name with arity - to Function instance, already did introduced ambiguity itself. In addition, by choosing slash operator both - for arithmetic division and separator between function name and its arity, only made it worse.

As far as I know, function_name/arity_num is not even valid token itself and does exist only for the sole purpose of capturing with &. Just in this single special case.

Are there some plans, work in core dev to fix above described inconsistency in some major release, like 2.0 ? Or find it as unsolvable problem without breaking backward compatibility and accepting damage has been already done ?

I’d probably need dive deeper into Elixir’s parser, but so far I’d imagine it could be handled with fixing operators precedence incl. parens and capturing function_name/arity_num would always require explicit enclosing in parens.

#2

Hi @torimus, welcome to the forum!

I don’t know why it’s like this, if there’s any technical reason, but I suspect it’s intentional, a language design choice.

For some time I also liked the idea of the capture operator working without &1, but now I see it potentially bringing confusion, this ends up helping to bring clarity and consistency to elixir code IMO.

Some meta examples where I see it becoming cryptic or misleading:

# this "lispy wrapping" could become very confusing, specially for beginners
[&(42), &(nil), &([]), &(:foo), &(@foo), &(foo), ...]

# & easily unnoticeable
[&%{foo: :bar}, &"wut", &[1, 2, 3]]

&fun(1) # easy to mistake with fun(1) or &fun(&1)
&Mod.fun() # easy to confuse with Mod.fun(), &Mod.fun/0 wins IMO
&Mod.fun(a, b) # easy to confuse with Mod.fun(a, b)
1 Like
#3

name/num is perfectly valid and token and can be used for division. Which mean that in situation like:

f = &(a/2)

The compilation need to “deduce” whether a is a function or not.

Another reason can be the fact that &foo(1) and &foo(&1) can be easily confused one with another and could lead to strange bugs.

1 Like
#4

name/num is perfectly valid and token and can be used for division. Which mean that in situation like:

Yeah, I’ve meant standalone expression in function_name/arity meaning. I was not too clear, my bad.

f = &(a/2)
The compilation need to “deduce” whether a is a function or not.

Another reason can be the fact that &foo(1) and &foo(&1) can be easily confused one with another and could lead to strange bugs.

Right. Abuse of function argument reference &n for “deduction” purposes seems the true reason. Forcing existence of at least one reference signalizes function body else expression is considered as function/arity specification.

defmodule MyMod do
  def zero_arity_fn(), do: 42
end

# myfn = fn -> zero_arity_fn() / 2 end
# anon. function capture attempts:
myfn = &zero_arity_fn/2    # BadArityError
myfn = &(zero_arity_fn() / 2)    # BadArityError
myfn = &((zero_arity_fn()) / 2)    # BadArityError
# can't mix function specifier with body 
myfn = &((&zero_arity_fn/0) / 2)    # CompileError - nested captures not allowed

# Forced explicit declaration of argument reference allows parser cheat ...
myfn = &zero_arity_fn/&1    # "deduction"

#IO.inspect myfn.()
IO.inspect myfn.(2)    # 21.0

I don’t know guts of Elixir parser, not sure what solution would be possible while not introducing another inconsistency at the same time.

&<expression> or &(expression) - capture anonymous function body by default
(&expression/expression) - edge case of capturing function_name/arity
or
&:function_name/arity: - atom-like specifier to capture existing function, inspired a bit by Ruby’s Symbol#to_proc

#5

https://groups.google.com/d/msg/elixir-lang-core/3HVYGqimS9o/8x9L6PNeHewJ


2 Likes
#6

Thanks for the link to related discussion.
I can live with that, but it only leaves a bitter taste.

  • named functions can have zero or more arguments
  • anonymous functions can also have zero or more arguments
  • operator & does convert(capture) named function to anonymous Function instance, that function still can have zero arguments
  • operator & does also convert(capture) single expression to anonymous Function instance, that function now illogically can not have zero arguments
  • by mixing apples with oranges, & operator obviously introduced exception to the rule, but hey having cool-looking syntax is worth the hassle. Or not ? You won’t need zero-arity that much anyway. Or would ? Use full syntax for anon. functions then. But there is & macro … :zipper_mouth_face:
1 Like
#7

& operator when used as a sugar for generating lambda has more limitations, for example this is illegal:

fun = &(&2)

As you need to use all arguments within such lambda body. So no, there is no exception to the rule as &function/arity is something completely different from & &1 lambda as these will behave differently. First one will cause call to function/arity while second will expand to fn a -> a end which will expand even further to generate function with generated name, for example :'-lambda-line-1-arity-1'/0 which will be internally used by the compiler. So yes, there is huge difference between these two.