Dear all,
we’re maintaining witchcraft
family of libraries in the wake of deprecations of some pre-defined infix operators.
Right now we’ve already removed some infix notation from the Control.Arrow modules, which makes the UX worse. It’s a sad concession to make, but we’re ready to make it.
However, <|>
is now deprecated too, making us consider <~>
for Alternative
. The problem, however, is that we can’t use this new <~>
from Alternative
in modules that use <~>
from other modules because qualified calls to infix functions don’t work:
iex(3)> alias Kernel, as: K
Kernel
iex(4)> "foo" <> "bar"
"foobar"
iex(5)> "foo" K.<> "bar"
** (SyntaxError) iex:5:7: syntax error before: 'K'
It would be very sweet if we got facilities to define custom arrows in Elixir. All the code to support it is there already, and it is absolutely needed even for ergonomic Alternative and Applicative.
Consider the following model example:
Given
def maybeLeft, do: {:left, {}}
def maybeRight, do: {:right, {}}
applicative-style function that branches depending on eihter:
def nimble(x) do
(maybeLeft *> pure("fail")) <|> (maybeRight *> pure("success"))
end
versus
def nimble(x) do
or_else(ap_right(maybeLeft, pure("fail"), ap_right(maybeRight, pure("success")))
end
The first snippet is clean, the second snippet is messy.
In general, when the operators encode branching or multiplying computation, pipe is insufficient.
At least one industrially validated usecase for this functionality is applicative-style combinatorial parsing. There are more, of course, but I think it is a sufficient argument for custom arrows.
Please note that it’s not to say that C+±style operator overloading is a good feature! The difference is that operators “apply right”, “apply left”, “apply” and “or else” are fundamental. Anything that is Applicative has “ap”-operators, and anything that is Alternative has “or else” operator. So it’s not about overloading operators, but rather about showing that some data structure has properties that allow us to use fundamental operators.
I’d like to also add that the argument for “adding ways to do things”, with all due respect, doesn’t hold water. We have macros in Elixir, which means that many peculiar behaviours can be implemented. A classical example of one is “conn” variable appearing out of thin air when we use get
or post
macro in Plug
. Is it good UX? Yes. Is it confusing when one sees it for the first time? Also yes. I had junior devs ask me question “where does conn
come from?”.
In concolusion, I think that there are many options in the solution space to enable this, including having a limited amount of infix operators, but allowing to call them in infix notation in a qualified mode (make Kernel.<>
callable in the infix way). But I think that it’s rather natural to simply allow Elixir programmers to define their own operators using the following grammar:
opS = '<' | '>'
opC = '+' | '*' | '&' | '^' | '-' | '~' | '|'
operator = (opS (opS | opC)*) | ((opS | opC)* opS) | (opC opC opC+)
P.S.
To reiterate the point, compare once again:
opS = either(string('<'), string('>'))
opC =
either(string('+'), string('*'))
|> either(string('&'))
|> either(string('^'))
|> either(string('-'))
|> either(string('~'))
|> either(string('|'))
operator =
opS
|> followed_by(many(either(opS, opC)))
|> either(many(either(opS, opC)) |> followed_by(opS))
|> either(opC |> followed_by(opC, some(opC)))
with
opS = string('<') <|> string('>')
opC =
string('+') <|> string('-') <|> string('&') <|> string('^') <|>
string('-') <|> string('~') <|> string('|')
operator =
(opS <*> many(opS <|> opC)) <|>
(many(opS <|> opC) <*> opS) <|>
(opC <*> opC <*> some(opC))
P.P.S.
If this proposal is accepted, we’re more than happy to submit a PR!