How difficult would it be to add the infix notation à la Haskell?

How difficult would it be to add the infix notation à la Haskell?

I think it could improve readability on several occasions

For example

rem(5, 3)

could be written as

5 `rem` 3

or

 Enum.into(list, %{})
 list |> Enum.into(%{})

could become

list `Enum.into` %{}

The question is: why? You kind of get it with pipes (see the example you’ve shown).

That’s an entirely subjective judgement.

In my personal view Haskell got a bit sidetracked from “lambda calculus” by trying to mimic “mathematical notation”.

What’s so special about functions of arity 2 that they deserve some special place in the universe to be “promoted to operators”? Functions are simple, operators merely confuse the issue (then there is the ternary operator - but there can’t be an infix notation for that).

And I’m fully aware that my opinion is informed, coloured, biased from Clojure (i.e. Lisp) - what can be more simple than (fn a b c d)?

(+ a b) may not look very mathematical but it conforms in a world built on functions …

12 Likes

Not really, IMHO

pipes are great for pipelines

a 
|> b
|> c

infix notation is useful when it makes more sense, it is particularly evident when the function works as an operator

if 5 `rem` 3 == 0 do
  ...

compare it too

if rem(5, 3) == 0 do

if 5 |> rem(3) == 0 do

but also

state = Enum.into([a: 1, b: 2], %{})
# or
state = [a: 1, b: 2] |>  Enum.into(%{})

VS

state = [a: 1, b: 2] `Enum.into` %{}

there are a lot of cases when the infix notation is just more natural

it removes a lot of the clutter

Elixir separate operators from functions as well, operators are functions with arity 1 or 2

from: elixir/lib/elixir/lib/kernel.ex at v1.2 · elixir-lang/elixir · GitHub

@doc """
 Arithmetic addition.
 Allowed in guard tests. Inlined by the compiler.
 ## Examples
     iex> 1 + 2
     3
 """
 @spec (number + number) :: number
 def left + right do
   :erlang.+(left, right)
 end

(+ a b) may not look very mathematical but it conforms in a world built on functions …

It is harder for many people to understand

We are not RPN calculators or Yoda :slight_smile:

Even if it more readable in some cases, we need very strong reasons to add yet another way of doing things to the language, and I am afraid we don’t have enough pros here to justify it, especially because “more natural” and “readability” mostly boils down to personal preference.

Such feature will complicate the grammar and understanding of the language. If we add it, now every team will ask themselves: when do we use the infix notation? Should we always use it? Never use it? And this is time better spent elsewhere.

28 Likes

and I am afraid we don’t have enough pros here to justify it

I understand the motivation behind the why not, I really do.

My question was how hard would it be, considering that Elixir can already extend operators, but only some of them.

It shouldn’t be the Haskell way (<backtick>function<backtick>) I was wondering if it there could be a way to support it through meta programming in the future.

1 Like

Parentheses rule! :grinning: And it is simple and extremely consistent as everything has the same format.

8 Likes

That is simply pandering to familiarity - in the hopes of gaining popularity.

It is harder for many people to understand

Have you actually pondered why that is? My hypothesis is that we are taught operators before functions - so we get used to operators before functions. But once you understand functions (something applied to a list of data) the notion of operators becomes superfluous (obsolete?). Yet we hang on to it even though it may be an idea who’s time has passed.

We are not RPN calculators

  • That’s postfix
  • It makes sense in the context of a stack based architecture
  • Ultimately our brain is a pattern matcher
  • When patterns change we have to adapt
  • The root of the problem: we hate different, we hate change

state = [a: 1, b: 2] |> Enum.into(%{})

As per style guide that is a no-no. And even pipes aren’t a universal panacea anyway.

4 Likes

I completely agree with the consistency argument, maybe I should have explained where my question comes from first.

I also want to say that my question was genuinely how difficult would it be.
I wasn’t asking to vote for the addition to the language anytime soon.

I’m working right now in a large company where a lot of the new hires are data scientist, with mainly a physics or statistics background, not a computer science one (so… no Lisp, no S-expressions).

They use Python a lot, in a very bad way.
One of the problems of Python is exactly inconsistency, everything can be done in at least two ways and both of them are equally correct.
Of course everyone tend to mix the two ways as they please.

I’m writing Elixir and Erlang code for them, I sold them the BEAM with the fault tolerant and easy concurrency arguments, but now I need to teach other people to interact with the code, and I’m having some difficulties (more than I anticipated) getting the new guys to grasp all the concepts, not just the language(s).

They also know quite well R, which is a kinda complex language, they are smart, but too much is different from what they already know.

I’ve started looking for a way to write Python differently, just for the purpose of getting them used to the functional paradigm in Python, so I showed them Coconut lang.

They immediately thought this is better for pattern matching, not so much for the |> operator.

Coming from their background f(g(x)) (and also of course (f (g x))) makes sense, x |> g |> f not really, while they had no problem with the infix notation and that’s when it clicked, there are many occasions where the two are swappable (and many others where it would simply be wrong).

So i started wondering if there was a way to make the transition smoother.

That is simply pandering to familiarity - in the hopes of gaining popularity.

Which is good and it worked great.

You can still write
Kernel./(Kernel.+(some_num, other_num), some_other_hopefully_non_zero_num)
or
some_num |> Kernel.+(other_num) |> Kernel./(some_other_hopefully_non_zero_num)

if you want.
But it’s scary too look at.
And it also starts a wave of new questions:
why Kernel.+(10, 5) but not +(10, 5)?
And why the unary version works both ways Kernel.+(10) == +(10)?

The root of the problem: we hate different, we hate change

That’s exactly what I was trying to avoid: change everything at once.

As per style guide that is a no-no.

Believe me, if I asked the question is because, and I’m quoting you,

Ultimately our brain is a pattern matcher

once people start to see a pattern emerge, they’ll use that pattern everywhere, even when they shouldn’t.
And that’s how the bad ones are born.
I noticed that often a good looking pattern works better than forcing people to read a style guide or use a linter.

In the end, I’m sure eventually they’ll get there, even without the infix notation :slight_smile:

Thanks for the discussion.

6 Likes

Ah, now we’re getting beyond the XY Problem.

You seem to be faced with transitioning people away from a monoglot mindset, towards a polyglot mindset.

once people start to see a pattern emerge, they’ll use that pattern everywhere, even when they shouldn’t.

It’s also easier to simply dump variables into a global namespace but as a community we have recognized that that is a bad idea. Similarly patterns have to be compartmentalized by context. R wasn’t designed to solve the same problems as Elixir (Erlang) have.

Pandering to familiarity is tantamount to “the Mountain coming to Mohammed” and while it may work in a popularity contest, it can go only so far before it becomes ineffective.

So I think you need to be looking for advice on how to “help them understand” that it can be helpful to shift one’s mindset in order to solve certain problems more effectively. For example supervisors are completely foreign to people only used to exceptions.

I’m fond of Syntax is utterly irrelevant but Semantics is King. Syntax is irrelevant, but no it isn’t is actually closer to the truth. Actually watch @rvirding’s entire talk. It reinforces the notion of Beyond Functional Programming with Elixir and Erlang that Erlang (and by extension Elixir) is functional for pragmatic reasons, not to be cool.

Coming from their background f(g(x)) (and also of course (f (g x)) makes sense, x |> g |> f not really,

Lots of people rationalize pipelines as an FP replacement for method chaining/fluent interface from OO, personally I view it as using forward function application as a poor man’s replacement for actual function composition.

So I think the real challenge here is to convince your audience that the functional mindset has something to offer them. That may require you to more deeply get involved in what they are working on and how they do things, so that you can more effectively propose that doing some things “functionally” may make it simpler to solve particular problems.

Now that you have opened the dialog, hopefully there will be lots of suggestions of how to make that happen.

2 Likes

If they know R, they may know the tidyverse set of libraries and it’s pipe operator %>% so there may be a way “in” there for at least some of this.

https://cran.r-project.org/web/packages/magrittr/vignettes/magrittr.html

Ah, now we’re getting beyond the XY Problem .

Sorry, but no.

Don’t mean to be rude, but as I’ve said few times already, I’ve only asked how difficult would it be.
I really wanted to know how hard would it be.

That’s all.

Thanks.

1 Like

Thanks to the great Elixir’s meta programming capabilities I was able to hack together the code below.
It’s just a quick experiment I did for my own amusement, nobody in his right mind should use it :slight_smile:

I believe that giving programmers the freedom to extend the language can work in the same way custom keybindings and plugins in modern editors make them “right for your fingers”.

Kudos to the Elixir team for making it so easy and fun to do complex stuff and experiment on new things.

defmodule A do
  def a <~ b do
    {a, b}
  end

  def {a, [m, f]} ~> b do
    apply(m, f, [a, b])
  end

  def {a, f} ~> b when is_binary(f) do
    case f |> String.split(".") |> Enum.reverse() do
      [f] ->
        {a, String.to_atom(f)} ~> b

      [h | t] ->
        f = String.to_atom(h)
        m = String.to_atom("Elixir.#{Enum.reverse(t) |> Enum.join(".")}")
        {a, [m,f]} ~> b
    end
  end

  def {a, f} ~> b when is_function(f) do
    apply(f, [a, b])
  end

  def {a, f} ~> b do
    apply(Kernel, f, [a, b])
  end
end

# it allows you to write
5 <~"Integer.to_string"~> 2
5 <~"rem"~> 3
5 <~:rem~> 3
[a: 1] <~(&Enum.into/2)~> %{} # parenthesis are necessary due to op precedence 
[a: 1] <~[Enum,:into]~> %{}

# or
enum_into = &Enum.into/2
[a: 1] <~enum_into~> %{}

# or, if you fancy ugly stuff
5 <~fn(a,b)-> rem(a,b) end~> 3
1 Like

Infix operators are great if you have 3-5 of them, but then you end up with a line of code like

a = b <=|=> c

and you think what in the world does that mean.

1 Like

I prefer f(x, y, z) over x f y z, thank you.
Pipe is there so you won’t have to write f(g(h(i(x), y), z), aa)

1 Like

nobody’s trying to steal that from you :slight_smile:
also operators works with two operands max, I’m sure you would prefer f(x,y) over f(x,y,z,x,w,k,j,t,o,flags_array)

I prefer the f(x) syntax as well, still there are situations where i think it could be of some help

row_id = ordinal_name(div(index, cols)) <> "_row"
row_id = (cols |> div(index) |> ordinal_name) <> "_row"
row_id = ordinal_name(index `div` cols) <> "_row"

I’m not trying to start a war on which way is the better one

let’s try to give functions a meaningful name

line_to(transpose_to(move_to(coords_for(x), y), z), aa)

could be written as

coords_for(x) |> move_to(y) |> transpose_to(z) |> line_to(aa)
coords_for(x) `move_to` y  `transpose_to` z `line_to` aa

The greatest limitation of the infix notation is that it works when the function acts like an operator.
Pipes look better when stacked and are more useful in the general case.

I don’t see myself writing

record `insert_into` database

Have you considered this:

id_fragment = div(index, cols) |> ordinal_name()
row_id = "#{id_fragment}_row"

This is by far the most clear way to write that, plus/minus piping vs. calling in the id_fragment = … line.

When building strings one should try to replace function calls by variables that already have been bound to. Most of the time this makes the effort much more clear.

In most cases, infixing function names and also pre-fixing operators by using `` and () in haskell is often disturbing and requires additional brainpower to re-write the expression in the head to be as it would have been naturally written.

4 Likes

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!

1 Like

Won’t that seriously complicate the understanding of other peoples code? And how should/would the formatter handle these things?

1 Like