Custom Infix Functions in Elixir
Published Sat Feb 13 2016 16:00:00 GMT-0800 (PST) by Rodney Folz
(…sort of)
I’m writing a Math module/shim for Elixir, which means I need to write tests that deal with floats. In particular, I need to know when two floats are really, really close to being equal, even though they might not be 100% equivalent.
My tests look like this:
test "add" do
assert sum(0.1, 0.2) <~> 0.3
end
test "asin" do
assert asin(0) <~> 0
assert asin(1) <~> pi/2
end
and they pass!
➜ mix test
........
Finished in 0.1 seconds (0.1s on load, 0.00s on tests)
8 tests, 0 failures
This might look like magic, especially if you’ve tried and failed to write custom infix functions in the past (like I have). Here’s how I implemented my nearly-equal test:
@doc """
Equality-ish test for floats that are nearly equal.
"""
@spec number <~> number :: boolean
def x <~> y do
nearly_equal? x, y
end
defp nearly_equal?(x, y) do
# ...
end
That’s right: it’s just a regular function definition!
This isn’t the only infix operator you can use. Here’s all the infix operators you can define in this way (as of Elixir 1.2.0):
\\
, <-
, |
, ~>>
, <<~
, ~>
, <~
, <~>
, <|>
, <<<
, >>>
, |||
, &&&
, and ^^^
.
Sadly, you can’t use any other custom symbols or names as infix. But hopefully those fourteen infix operators will be enough to satisfy :-)
Now for the deep dive into why this is the case.
Let’s look at the source code for another infix operator like the built-in +
addition operator. First we need to find where +
is defined – the Getting Started guide gives us a hint:
The
Kernel
module is also where operators like+/2
and functions likeis_function/2
are defined, all automatically imported and available in your code by default.
Ok, that’s promising. Let’s check out the Kernel module docs for +/2
.
Hmm, not really helpful for us. However, there’s a link to the source code for +/2
. Maybe that will be what we’re looking for?
@spec (number + number) :: number
def left + right do
:erlang.+(left, right)
end
This isn’t the answer, but it’s a start. Notice that when the Kernel
module implements +/2
, it’s already using the left-op-right form. So does this mean we can just define our own infix functions like the ones in Kernel
are defined?
@spec (number inf number) :: String.t
def left inf right do
IO.puts "#{left} INFIX #{right}"
end
No, we can’t:
== Compilation error on file lib/ops.ex ==
** (SyntaxError) lib/ops.ex:34: syntax error before: ')'
(elixir) lib/kernel/parallel_compiler.ex:100: anonymous fn/4 in Kernel.ParallelCompiler.spawn_compilers/8
Darn, it would have been nice if custom infix functions Just Worked.
But what about trying it the other way? Can we define an infix-by-default operator as a prefix function?
def <~>(left, right) do
IO.puts "#{left} OP #{right}"
end
This doesn’t work. In fact, it’s a compile error to try to define <~>
in the regular way.
➜ iex -S mix
== Compilation error on file lib/ops.ex ==
** (SyntaxError) lib/ops.ex:30: syntax error before: ')'
(elixir) lib/kernel/parallel_compiler.ex:100: anonymous fn/4 in Kernel.ParallelCompiler.spawn_compilers/8
From this, we can guess that the Elixir parser does not generally support infix notation, but instead has the infix operators in Kernel
special-cased. Let’s validate our hunch by finding how Elixir is parsed.
This information, however, is not to be found in the Getting Started Guide. Instead, a brief Google search for “elixir parser” led me to this very interesting article on parsing source code with Elixir. Near the end of article, the author mentions that Elixir itself is parsed using the same methods discussed in the article:
Want another real-world™ example? Wait, I think I have one: ever heard of the Elixir programming language? It’s a nice language built atop the Erlang virtual matching, focused on concurrency, fault to… Well, it’s parsed by yecc :).
Jackpot! Let’s check the source for elixir_parser.yrl, the file linked in that quote, and see what we get.
Left 5 do.
Right 10 stab_op_eol. %% ->
Left 20 ','.
Nonassoc 30 capture_op_eol. %% &
Left 40 in_match_op_eol. %% <-, \\ (allowed in matches along =)
Right 50 when_op_eol. %% when
Right 60 type_op_eol. %% ::
Right 70 pipe_op_eol. %% |
Right 80 assoc_op_eol. %% =>
Right 90 match_op_eol. %% =
Left 130 or_op_eol. %% ||, |||, or
Left 140 and_op_eol. %% &&, &&&, and
Left 150 comp_op_eol. %% ==, !=, =~, ===, !==
Left 160 rel_op_eol. %% <, >, <=, >=
Left 170 arrow_op_eol. %% |>, <<<, >>>, ~>>, <<~, ~>, <~, <~>, <|>
Left 180 in_op_eol. %% in
Left 190 three_op_eol. %% ^^^
Right 200 two_op_eol. %% ++, --, .., <>
Left 210 add_op_eol. %% +, -
Left 220 mult_op_eol. %% *, /
Nonassoc 300 unary_op_eol. %% +, -, !, ^, not, ~~~
Left 310 dot_call_op.
Left 310 dot_op. %% .
Nonassoc 320 at_op_eol. %% @
Nonassoc 330 dot_identifier.
There we have it. In Elixir, operators are predefined in the parser. Since the parse step happens before Elixir is bootstrapped, there’s no way for us to define our own operators in our code. We would either have to modify the Elixir parser directly (and recompile Elixir) or just be satisfied with one of the fourteen unused operators:
\\
, <-
, |
, ~>>
, <<~
, ~>
, <~
, <~>
, <|>
, <<<
, >>>
, |||
, &&&
, and ^^^
.
After I posted an excited tweet about my "discovery", José Valim reached out to me and confirmed that the unused operators were deliberately included for exactly this use case.
did you know: Elixir has unused infix ops? that u can bind to yr own custom functions?! w0w so cool! #myelixirstatus pic.twitter.com/MYTh1HCKNk
— Rodney Folz (@rodneyfolz) February 14, 2016
@rodneyfolz we provide a handful of them as they may be useful in some domains. You can't create your own. :)
— José Valim (@josevalim) February 14, 2016
In summary:
- Elixir’s infix operators are special-cased in the parser.
- The Kernel module just implements them, but doesn’t do anything special, syntax-wise, itself.
- You can implement behavior for unused operators that the parser knows about.
- You can’t create new infix operators without recompiling Elixir from source.
But wait, there’s more!
While I was searching around for how infix operators worked, I came across this #elixir-lang-talk thread which discussed overriding the built-in Kernel
module’s operators.
One special thing about Kernel is that it defines the original implementations for each operator. You can redefine any of the operators if you first unimport it inside your module.
Once you have a new implementation for an operator (say, in module called MyOp), you can call it as a function
MyOp.>(a, b)
or you can import it and use it as an operator
import Kernel, except: [>: 2]
import MyOp
a > b
This trick is used by the pipespect library to automatically IO.inspect
every stage in a |>
pipe
import Kernel, except: [{:|>, 2}]
defmacro first |> rest do
# code and edge cases to wrap the pipeline in IO.inspect
end
Since this is monkey-patching the language, redefining Kernel’s built-in operators may break expectations from other Elixir code and should be used with care, if it’s used at all. But it’s a fascinating example of how predictable and regular Elixir can be.
You can also find me @rodneyfolz on Twitter.