Proposing backticks infix syntax for arity-2 functions

115 views
Skip to first unread message

Wiebe-Marten Wijnja

unread,
Aug 10, 2016, 6:17:36 PM8/10/16
to elixir-lang-core
One of the more longstanding problems in Elixir is the fact that there is a difference between how code is executed inside clauses and outside.

The choice was made to only define infix operators for the kinds of operations that are guard-safe, so it is not confusing as to when you are and when you are not allowed to use these infix operators.

There is another problem that operators have: They are very cryptic. The only way to know what an operator does, is if you've read its definition, and still remember it. (Could you guess what `<|>`, `|^|` and `~?=` do in Haskell?)

Names of functions, on the other hand, are self-describing (as long as they are named well, of course), so you instantly see what a piece of code does.


However, there are many operations that take two equally-important arguments, which are much more natural to write in an infix-style than in a prefix-style, as this is also the direction in which we 'think' them in.

Some common examples include:
- Arithmetic operations like `+`, `-`, `*`, `/`, `div`, `mod`, `pow`.
- Comparison operations like `>`, `<=`, `!=`, `match?`, `MapSet.subset?`, `lt?`, `gte?`, `neq?`.
- Access-based operations like the Access Protocol's `arr[x]`, `elem`, `put_in`, `List.delete`.
- Operations that combine two structures, like `|`, `Map.merge`, `MapSet.intersection`.


Because it is discouraged to override the infix operators for operations that are not (and often cannot be) guard-safe, it feels a little 'clunky' to use custom data structures, as we're forced to do things like:
Decimal.add(decimal_a, Decimal.div(decimal_b, Decimal.new(2))

Timex
.before?(Timex.shift(Timex.today, days: 1), Timex.today)



As Guy Steele said in his marvelous talk 'Growing a Language': "When faced with this, programmers that are used to performing addition using a plus sign, quetch". (It is one of the most amazing talks I've ever seen, by the way. I totally recommend tha you watch it right now.)

If there were a way to use an infix notation for non-operators, users could instead improve on the language in a "smooth and clean" way.


Taking inspiration from Haskell's syntax, I realized that there is a way to circumvent this problem:

My proposal: Introduce backtick-syntax to use arity-2 functions inline.

- Functions (and macros) with arity 2, can be written as
a `div` b
This is rewritten during compilation into
div(a, b)


Some more examples:
users[1][:name] `put_in` "José"
{x, _} `match?` foo
{1, 2, 3} `elem` 2


- Both local and remote functions can be called this way. The following is thus also valid:
%{a: 1, b: 2} `Map.merge` %{c: 3, d: 4}
["foo", "bar", "baz"] `List.delete` "bar"


- This rewriting happens from left-to-right,(left-associative) so:
import Map
a
`merge` b `merge` c
is rewritten into:
merge(merge(a, b), c)


As far as I know, this is completely backwards-compatible: Backticks are not allowed inside Elixir syntax right now.
The only place where they are 'used' is inside documentation strings, to delimit in-line markdown code snippets. 
This is not a problem, however; To create an in-line code snippet that allows backticks to be used internally, one can simply delimit it with two backticks. This is already used inside the documentation of Elixir itself, such as on the page about writing documentation. 

-------


Adding infix backtick-syntax is better than the current situation, because:
- It allows a more natural syntax for binary operations, making the language more readable.
- It makes custom data structures feel more integrated into the language.

This solution is better than 'just adding more possible operators' because:
- It keeps it very clear what is allowed inside guard-clauses and what isn't.
- It is explicit what an operation does, as names are self-describing while operator symbols are not.


--------


Please, tell me what you think. :-)


~Wiebe-Marten/Qqwy

Allen Madsen

unread,
Aug 10, 2016, 7:48:39 PM8/10/16
to elixir-l...@googlegroups.com
In my opinion, the pipeline operator already solves this problem. I
would rewrite some of your examples as follows:

decimal_b |> Decimal.div(Decimal.new(2)) |> Decimal.add(decimal_a)
Timex.today |> Timex.shift(days: 1) |> Timex.before?(Timex.today)
a |> div(b)
Allen Madsen
http://www.allenmadsen.com
> --
> 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/2d7507ef-9ea3-4d0e-809b-8c1c674eb951%40googlegroups.com.
> For more options, visit https://groups.google.com/d/optout.

Ben Wilson

unread,
Aug 10, 2016, 8:12:07 PM8/10/16
to elixir-lang-core, allen.c...@gmail.com
I agree, this suggestion reads like the |> does not exist.

I'm also not clear on what is meant by "One of the more longstanding problems in Elixir is the fact that there is a difference between how code is executed inside clauses and outside." What is a clause?

Paul Schoenfelder

unread,
Aug 10, 2016, 10:35:29 PM8/10/16
to elixir-l...@googlegroups.com, allen.c...@gmail.com
I agree that the pipe operator solves this already. I think what was meant by "difference between how code is executed inside clauses and outside" is the fact that guards have different rules than function bodies (i.e. you can only use a subset of "blessed" functions in guards). At least that's my understanding.

Paul

To unsubscribe from this group and stop receiving emails from it, send an email to elixir-lang-core+unsubscribe@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/elixir-lang-core/0d6666ed-106f-410a-9f1d-d12dd76df102%40googlegroups.com.

Wiebe-Marten Wijnja

unread,
Aug 11, 2016, 2:51:47 AM8/11/16
to elixir-lang-core, allen.c...@gmail.com
Thank you for your replies!

Indeed, the word 'guard' is missing from that sentence: The fact that Erlang only allows certain BIFs inside guard clauses, and therefore Elixir discouraging the overriding of operators that will not work in them. 

This suggestion is not at all meant as a replacement for the pipeline operator |>.
The pipeline operator is built to accomodate the need to pass a single, most important, subject through a sequence of functions.
These functions that work on a single, most important subject, can be described as 'Take the left-hand side, and perform the action outlined by the rest of the parameters'.


However, the functions in the cases outlined above (arithmetic, comparisons, access, combinatorial, and there are probably more), it feels very weird, very unnatural to use the pipeline operator. 
These functions do not work on a single most important subject. Both the left-hand side and the right-hand side are equally important.
These can be described as 'Take the left-hand side and the right-hand side, and perform the action ontlined by the name of the operation.'

Using these binary (arity-2) functions with the pipeline operator feels strange and distracts from what is going on: 
a |> div(b)
x
|> lt?(y)
{1,2,3,4} |> elem(2)
%{foo: 1} |> Map.merge(%{bar: 2}) |> Map.merge(%{baz:3})

The pipeline operator also stops to be useful in the case that we want to expand on the second argument of something:

Observe that
Decimal.add(decimal_a, Decimal.div(decimal_b, Decimal.new(2)))
which the new syntax would let you write as
decimal_a `Decimal.add` (decimal_b `Decimal.div` Decimal.new(2))
was rewritten by Allen Madsen with the pipeline operator to
decimal_b |> Decimal.div(Decimal.new(2)) |> Decimal.add(decimal_a)
which swaps the order of parameters passed into `Decimal.add`. For addition this is not a problem, but take a non-reflexive operation like subtraction:

Decimal.sub(decimal_a,Decimal.div(decimal_b, Decimal.new(2)))
When using the pipeline operator, this would mean creating a call structure in this way:
Decimal.sub(decimal_a, decimal_b |> Decimal.div(Decimal.new(2)))
which would definitely be less readable than:
decimal_a `Decimal.sub` (decimal_b `Decimal.div` Decimal.new(2))


What I am trying to get at, is that the new syntax lets you write binary functions in the same location as the guard-safe operators, which will make user-defined structs feel more integrated with the language.
If you can do 
a > b
when a and b are built-in types, but are forced to use 
lg?(a, b)
 or 
a |> lg?(b)
 when having custom data types, I am not very happy.
But if I can use 
a `lg?` b
, I am. The semantics of the comparison are kept intact.


~Wiebe-Marten/Qqwy

Aleksei Magusev

unread,
Aug 11, 2016, 4:23:16 AM8/11/16
to elixir-lang-core, allen.c...@gmail.com
I think backticks syntax makes sense for the Haskell since it has function application via space, but for Elixir it hurts readability with no (visible to me) benefits.
Calling non-guard-safe function as the guard-safe operators doesn't feel like a good thing: we remove valuable hint about function property in our code.

– Aleksei

Ben Wilson

unread,
Aug 11, 2016, 10:43:17 AM8/11/16
to elixir-lang-core, allen.c...@gmail.com
I'm confused what you mean by "What I am trying to get at, is that the new syntax lets you write binary functions in the same location as the guard-safe operators, which will make user-defined structs feel more integrated with the language." Your proposal still won't let `lt` be used in guard clauses, so I don't see how the guard clause discussion is relevant here.

At the end of the day the `` is exactly identical to |>, it just looks a bit different. I don't see the value in extra syntax to do something we can already do. That isn't to say that aesthetics never matters, but the difference here is merely that it doesn't emphasize the left hand side, whereas the |> operator does a little bit. That seems like entirely too little of a distinction to warrant extra syntax.

Wiebe-Marten Wijnja

unread,
Aug 12, 2016, 2:29:42 AM8/12/16
to elixir-lang-core, allen.c...@gmail.com
Thank you for your reply, Ben :-)

It is true that when compiled, `a foo b` and ` a |> foo(b)`, both become `foo(a, b)`. The argument that you gave against adding `` could just as well be applied against the pipeline operator itself:
"At the end of the day, |> is exactly identical to normal function application, it just looks a bit different. I don't see the value in extra syntax to do something we can already do."
Aesthetics do matter. And in contrast to |>, `` will not raise a warning when the right hand side is not enclosed in brackets.

___

With my statement about 'writing binary functions in the same location as the guard-safe operators' I meant that you could use infix notation on them, rather than prefix. I totally agree that my statement was confusing, sorry. 

Maybe this fake infomercial, geared towards new Elixir users explains it better:

Hey, new Elixir user! Want your homebuilt data structure behave just like the built-in Elixir types? Do not fret!
While it is true that you cannot define new operators, and should not redefine operators like >, + and * because they would stop working in guard clauses, you can do the next best thing:
create implementations for functions like gt?, plus and pow instead, and use them just like you would normally use the operators:
import YourModule
a
`gt?` b
a
`plus` b
if (ten `pow` (five `plus` three)) `div` two `gt?` fourty_two, do: "So long, and thanks for all the fish!"

Look at this stuff!
Isn't it neat?
Wouldn't you think your struct now feels complete?

Thank you for your replies, everyone!

~Wiebe-Marten

José Valim

unread,
Aug 12, 2016, 4:25:18 AM8/12/16
to elixir-l...@googlegroups.com
It is true that when compiled, `a foo b` and ` a |> foo(b)`, both become `foo(a, b)`. The argument that you gave against adding `` could just as well be applied against the pipeline operator itself

As you said though, "aesthetics do matter" and |> provides a huge gain in readability we are all aware of. The question, however, is: does `fun` provides substantial gains compared to what we have today? I would say the answer is no.

There is a cost in adding features and language syntax and I don't think `fun` offsets those costs given we have others, similar ways, of solving the raised problems.

Reply all
Reply to author
Forward
0 new messages