Elm 0.19 removes the syntax for user-defined operators. So it is no longer possible to define things like
This document presents the reasoning behind this decision.
Theory of Operator Meaning
It is extremely difficult to come up with good operators. I think all of the successful ones have two things in common:
They have some visual relation to their meaning. For example,
<?>from the URL parsing library exactly mimic the
?symbols in URLs. And
|>indicates directionality really clearly. The math operators like
/are directly related to the math operators taught in every school in the world.
They use the visually simple symbols. I have never seen
$used to create an excellent operator. I think it is partly that they are so busy visually, mixing curves and lines haphazardly. It is also partly that they do not have super strong cultural meanings as infix operators in general. E.g.
$4means four dollars, but what is
Between these two things, there are not actually very many viable possibilities, and all of them are taken. Visual bracketing like
|+| expands things a bit, allowing reuse of symbols that already have cultural meaning, but at that point, you are basically constrained to making operators for vector math or something.
Usage in Packages
Elm has had this feature for a while, so I studied how it has been used so far in packages.
As of this writing, there are 1031 packages published for Elm 0.18 and 66 of them have ever defined operators. Core developers who agree with this choice account for about 15 of those, so there are 51 unaccounted for. That means that under 5% of packages are affected.
I want to highlight some of the usage trends with that 5%:
Haskel Operators - Some authors really like the following operators:
>=>, etc. This seems to be one of the more popular uses.
Invented Operators - A fairly small fraction of authors invent very elaborate operators. For example, one package contains
|~>, and many others. Another has
!+>, and many others. This is pretty uncommon.
Math Operators - Some packages are for math. Vector math. Matrix Math. They generally use operators like
|*|that match the cultural norms. This is not as common as I would have hoped actually! More math!
Parser Operators - Parsers in the ML-family of languages can be really lovely. All of the parsing packages I have seen in Elm have special syntax of some sort.
I may be missing some scenarios, but those were the ones that stood out to me.
Root Design Goal
Each of these categories stems from a reasonable design goal. I will try to outline the design goal, and then point a path towards acheiving that goal in a nicer way:
"I want it to be easier to chain tasks." Haskell has this special syntax called
do-notation for sequencing tasks. It is pretty neat once you know it, but it it also a major barrier to entry. I struggled with it for about six months at least. Haskell also has a set of operators like
>>=that is connected to this special syntax, so I suspect folks settle for having that as a personal compromise. Now, I think other languages have actually accomplished the root goal of "chaining tasks is easy" in nicer ways. For example, F# has computation expressions that are a bit more flexible and do not require integration with a type class mechanism. And C# introduced
awaitsyntax that gives the same capabilities, but integrates with the language way more cleanly. And Idris generalized that with bang-notation. So I feel that
>>=is missing the bigger picture here.
I want to write fewer characters. I think this explains the "invented operators" case, but the line between "consise" and "cryptic" is a matter of taste. Is APL concise or cryptic? Is Ruby concise or cryptic? Is Haskell concise or cryptic? Depends who you ask! In Elm, having explicit and readable code is a major design goal.
elm-formathelps with that. Using qualified values like
Set.maphelps with that. So even if you have the
|-~->operator, Elm is not really designed for minimizing character count and will clash with your root goals in other ways.
I want to do math! I really like this goal. I think languages like Julia have done an excellent job at overloading
*in a reasonable way. Their approach is really lovely, but we would have to lose Elm’s type system to match them. Point being, rather than making
|*|as a stopgap, perhaps it is possible to think about the broader question in a comprehensive way. Should there be a way to overload
+for vector and matrix math? How would that work? How would you multiply a vector by a scalar? Perhaps the best design is to restore user-defined operators for bracketed math operations like
|-|with certain types? Or maybe it can just be done in a really nice way with a library. Worth exploring!
I want to parse! Writing parsers can be tricky, and operators are one way to help make things easier. Most parsing packages replicate the Haskell operators, and all of the logic in case (1) applies. Separate from that, it seems like
<?>have been quite successful in
elm-lang/url, and it seems that
|=have been quite successful in
elm-lang/parser. These operators are getting special cased like
-. What is the deal with that?
Well, many languages special case parsers (e.g. regex in Perl, JS, and Ruby) with very specific costs and benefits. The cost is that if regex cannot handle your scenario, it is very annoying. The benefit is that there is specific knowledge that transfers between different codebases and languages. I think Haskell is the best example of a language that does not special case parsers, also providing specific costs and benifits. The cost is that everyone has to pick between
parsecfor okay error messages and
attoparsecfor better perf. Any project I ever created ended up using both
attoparsecthrough transitive dependencies. (That was frustrating when I was trying to get
elmbinaries smaller, but it is a much bigger deal for JS bundles where size is super important!) Even though the API for
attoparsecare pretty much identical at a high-level, code does not transfer between because the details are slightly different. On the other hand, the benefit is that you can get a parser tailored for your exact performance or error message needs, and if there is some new insight, someone can make a new parser library around it.
The design of
elm-lang/parseruses the same performance insights from
attoparsecand improves upon the error message quality of
parsec. It appears to be possible to have both under one API. I also think it makes the most sense for the ecosystem to have one option that distills the best known path. Exploration can still happen (I do my exploration in Haskell because it is great for that) and insights can be brought back without fragmenting the Elm ecosystem.
The broader message here is that we have some fairly specific design problems, and user-defined operators are often stopgap measures. I think it is important to think about languages on a timescale of decades, and by looking at each case directly, I think we can end up with something nicer in the long run.
Usage in Applications
I know some folks also define operators in their application code. Some people really love custom operators. Some people really hate them. We have found with
elm-format that just making a choice is an effective way to help teams minimize these debates and focus more on the application.
This case is a bit borderline for me though, especially if you are working on your own. One thing I learned from discovering The Elm Architecture is that it is really lovely to be able to show up in any codebase and know what is going on. I think custom operators detract from that enough that they are not worth it for the whole ecosystem, even if they are great for specific individuals.
But why was this feature added in the first place? As far as I can remember, this is how I implemented
- while I was working on my thesis. This was a naturally exploratory time, and features did not undergo as much scrutiny as they do now.
When assessing features from that time, I ask myself, "If someone proposed adding this feature today, would it get in?" I cannot see user-defined operators getting in. All of the considerations in this document seem to point to there being more specific problems that would benefit from a more specific designs.
I know not everyone agrees with this choice, but I hope this document clarifies some of the thinking behind it.