7
$\begingroup$

Haskell has support for declaring custom infix operators, including their precedence and associativity. In addition, any identifier can be used as binary infix operator by placing it between backquotes. Have other languages picked this up or has the downside of creating too many custom infix operators made language designers stay clear of this?

-- Operator precedence table: -----------------------------------------------

infixl 9 !!
infixr 9 .
infixr 8 ^
infixl 7 *
infix  7 /, `div`, `quot`, `rem`, `mod`
infixl 6 +, -
infix  5 \\
infixr 5 ++, :
infix  4 ==, /=, <, <=, >=, >
infix  4 `elem`, `notElem`
infixr 3 &&
infixr 2 ||
infixr 0 $

I am more familiar with OCaml which can define and re-define infix operators but precedence and associativity is determined by the prefix of the operator. There is no support for using regular functions in infix positions. I like Haskell's more general and systematic approach but wonder how much it has spread beyond Haskell.

Addendum: I’ve asked moderators to turn this into a Community Wiki because there is no single perfect answer. The question collects how languages support the declaration of infix operators and each is interesting in its own right.

$\endgroup$
0

5 Answers 5

5
$\begingroup$

tl;dr

This is legal, runnable Scala code:

import language.postfixOps

object Foo:
  def +(other: this.type) = "binary infix"
  def +                   = "unary postfix"
  def unary_+             = "unary prefix"

Foo + Foo //=> "binary infix"

Foo +     //=> "unary postfix"

+ Foo     //=> "unary prefix"

Scala allows calling arbitrary methods with one argument using infix syntax and allows arbitrary operator characters in method names, therefore allowing arbitrary operators to be defined. Associativity and precedence, however, cannot be freely defined, they are based on the method name.

Scala 2 (Legacy)

Scala mostly doesn't deal with operators at all. There are three rules for method calls which allow Scala to support what "feels like" operators and operator overloading without actually having to deal with special support for operators and operator overloading:

  1. The . for method calls can be replaced by whitespace.
  2. When using the method calling syntax from #1, parentheses for a single argument list with a single argument are optional.
  3. A method whose name ends with : is right-associative.

So, rule #1 means that I can replace

foo.bar(baz)

with

foo bar(baz)

and rule #2 means I can replace that with

foo bar baz

There is nothing special about bar. Any method call expression with a single argument list with a single argument can be written this way.

Consequently, there is nothing special about

a + b

It is simply the same as

a.+(b)

applying rules #1 and #2 from above. + is just a legal name for a method like any other. Scala's rules for identifiers are roughly the same as for other popular languages (alphanumeric, can't start with a number, etc.), except that identifiers can also contain (and be completely composed of) operator characters. When mixing operators and alphanumerics, they need to be separated by an underscore. So, foo is legal ++--::::--++!!!**** is legal, foo_+ is legal.

The third rule means that

a +: b

is actually equivalent to

b.+:(a)

or, more precisely (observing left-to-right evaluation):

{ val __unspeakable_name__ = a; b.+:(__unspeakable_name__) }

This is primarily used to support the standard :: operator for prepending an element to a CONS list. Naturally, in a single-dispatch OO language, that must be a method of the list object, but we still want to write the elements in the order they appear in the list:

val list = 1 :: 2 :: 3 :: Nil
// is equivalent to
val list = Nil.::(3).::(2).::(1)

But what about precedence? And what about unary prefix operators? Okay, that's where my qualification from above comes in, where I wrote Scala "mostly" doesn't deal with operators.

For precedence, the rule is that the precedence is determined by the first character of the operator. I.e. all operators starting with + have the same precedence, all starting with - have the same precedence, and so on. The relative precedence between the starting characters is roughly the "standard" precedence from languages like C or Java, but with some modifications.

For unary prefix operators, Scala does not allow defining new ones. It only allows +, -, ~, and !. Unary prefix operator expressions are translated into method calls by prefixing unary_ to them, e.g.

+foo
-foo
~foo
!foo

is equivalent to

foo.unary_+
foo.unary_-
foo.unary_~
foo.unary_!

Note how the rule that identifiers mixing alphanumeric and operator characters must be separated by an underscore is observed, so these are legal identifiers and can be defined using normal method definition syntax.

Unary postfix operators used to be allowed by simply observing rule #1 from above:

foo bar

is equivalent to

foo.bar

They are not technically deprecated (yet!) but their use is heavily discouraged. In current versions of Scala, they require an explicit import (or compiler flag), and will generate a compiler warning even if explicitly enabled.

There are a couple of other rules in order to support common use cases for operator overloading:

  • Function application syntax: if there is no method named foo in scope, then foo(bar) translates to foo.apply(bar). This is used extensively for Factory methods, e.g. Scala has no literal syntax for collections, instead, all collections have companion objects with an apply method, which allows you to construct e.g. a List with List(1, 2, 3, 4). This is also used for array and map indexing, treating arrays as functions of their indices and maps as functions of their keys (in fact, arrays and maps literally inherit from Function).
  • Update syntax: foo(bar) = baz translates to foo.update(bar, baz). Not used extensively, as the Scala community eschews mutability.
  • Property syntax: If there is no mutable field (var) named bar, then foo.bar = baz translates to foo.bar_=(baz).
  • Assignment operators: +=, -=, etc. are just normal method names, so they can be implemented, i.e. a += b translates to a.+=(b). However, if there is no method named += but the expression a = a + b type checks, then that translation is chosen. This means += can be overloaded, but var n = 0; n += 1 still works as expected even though Int doesn't define +=.

There are some other "magic methods", but they are not related to operators, specifically:

  • for comprehensions de-sugar into calls to foreach, map, flatMap, and withFilter.
  • Pattern Matching de-sugars into calls to unapply and unapplySeq.

An interesting tidbit is that infix notation is also valid for type constructors. Application of a binary type constructor C[A, B] can be written as A C B. This is rarely used but can be useful in the context of type-level programming. For example, the type-constructor =:=[A, B] compiles IFF A and B are equal, and it should be obvious why being able to write it as A =:= B is desirable.

Scala 3 (Current)

Some of the rules above have been tightened up in Scala 3:

  • It is always possible to call a method using infix syntax by enclosing it in backticks, i.e. foo `bar` baz and foo `+` baz are always allowed and are equivalent to foo.bar(baz) and foo.+(baz).
  • It is still always possible to call a method whose name consists exclusively of operator characters using infix notation, i.e. a + b is still allowed.
  • Methods whose name contains alphanumeric characters can only be called using infix notation if they are explicitly marked infix at the definition site: infix def foo(bar: SomeParameterType): SomeResultType. Operator methods don't need an infix modifier.
  • For operator methods, it is encouraged to use the @targetName annotation which allows you to choose a human-readable name for the method in the actual compiled code. This is especially useful if the name is illegal in the underlying target platform (e.g. the JVM). For example, @targetName("prepend") def ++:(other: SomeParameterType): SomeResultType will be compiled into a Java method named prepend whereas without the annotation it would be something like $plus$plus$colon, which would be the name you need to use if you want to call this method from Java, Kotlin, Ruby, or any other JVM language.

But the fundamental idea of trying to, as much as possible, treat operators as methods like any other, just with funky names, remains.


Disclosure: this answer is copy&pasted from https://langdev.stackexchange.com/a/1759/854

$\endgroup$
5
$\begingroup$

Swift

Swift allows users to declare their own infix operators, and even custom precedence groups:

// here’s the fun little dance Swift makes you do to import the C stdlib:
#if canImport(Darwin)
import Darwin
#elseif canImport(Glibc)
import Glibc
#elseif canImport(WASILibc)
import WASILibc
#elseif canImport(ucrt)
import ucrt
#else
#error("unsupported platform")
#endif
// All that to get a pow function!

precedencegroup ExponentiationPrecedence {
  higherThan: MultiplicationPrecedence
  associativity: right
}

infix operator ** : ExponentiationPrecedence

extension Double {
  static func ** (lhs: Double, rhs: Double) -> Double {
    pow(lhs, rhs)
  }
}

There are some restrictions on what an operator name can be, as I described in this answer. The short version is that it has to be punctuation and that certain characters are reserved.

$\endgroup$
3
  • $\begingroup$ I knew I had seen a language that made precedence grouping a partial order; I just couldn't put my finger on which one it was. I think this is my favorite approach to order-of-operations. There's no obscure "Remember, + is precedence level 6" or whatever; it's all just relative. $\endgroup$ Aug 19, 2023 at 15:44
  • $\begingroup$ @SilvioMayolo: Fortress has a partial order as well, and pre-dates Swift. Operators which have no defined relative precedence cannot be used together in the same expression without explicit parentheses. You can use whitespace to validate the precedence, but not to change it, i.e. whitespace does not change the meaning of an expression, but it can cause a compile error. E.g., 1+2*3, 1 + 2 * 3, and 1 + 2*3 are all valid, but 1+2 * 3 is a compile error. Raku defines operator precedence relative, just like Fortress and Swift, but I believe the order is total. $\endgroup$ Aug 19, 2023 at 16:31
  • $\begingroup$ Swift is whitespace-sensitive but not to that extent. Like 2 -1 is illegal because it parses as two tokens (2, -1), but both 2 - 1 and 2-1 parse as three tokens (2, -, 1) and are accepted. $\endgroup$
    – Bbrk24
    Aug 19, 2023 at 23:32
5
$\begingroup$

Prolog

Prolog has an operator grammar. There are 51 operators built in, and users can declare their own.

You declare an operator like this:

:- op( 1150,  fx, mode ).
:- op( 1050, xfy, -> ).
:- op(  900,  fy, \+ ).
:- op(  700, xfx, = ).
:- op(  550, xfy, : ).
:- op(  500, yfx, - ).
:- op(  500, yfx, + ).
:- op(  200,  fy, - ).

The three arguments to the op/3 declaration are the precedence (an integer between 0 and 1200), the fixity, and the operator symbol or name.

The fixity is one of xf, yf, xfx, xfy, yfx, yfy, fy or fx. You can interpret f as being the position of the operator, and x and y the positions of its operands. A y operand means that in that position, a term with precedence less than or equal to the precedence of this functor should occur. An x operands means that the precedence of that term must be strictly less than that of the functor. A basic term, or a term in parentheses, has predecedence 0.

So, for example, consider the infix + and - operators. They have the same precedence, and both have fixity yfx. Informally, this means that both operators are left-associative, so this:

a - b + c - d

parses as (and could be equivalently written as):

'-'('+'('-'(a,b),c),d)

Similarly, in this example, the : operator is right-associative, and the = operator is non-associative.

Both prefix and suffix operators are supported and, as you can see with the - operator, the same symbols can be used for both an infix operator and a prefix operator. In Prolog, these are different operators; one is -/1 (i.e. minus with one argument) and the other is -/2 (i.e. minus with two arguments).

Readers who know some things about parsing will note that it's possible to declare combinations of operators that are formally ambiguous. Parsing Prolog is nontrivial, and there are two competing standards (Edinburgh and ISO) which handle potential ambiguities in different ways.

$\endgroup$
4
$\begingroup$

Kotlin has the ability to create infix functions:

infix fun Int.foo(x: Int) = this + x * x

2 foo 3 // 11

Nothing super special like Scala. Just add an infix modifier and you're done. However, all infix functions are at the same precedence and it cannot be modified.

$\endgroup$
2
$\begingroup$

Raku allows not only prefix, suffix, and infix, but also "circumfix" and "postcircumfix" operators. Circumfix means things like %<code>%, and postcircumfix means things like code%<code>%. The tutorial gives these examples:

sub postcircumfix:<⸨ ⸩>( Positional $a, Whatever ) {
    say $a[0], '…', $a[*-1]
}
 
[1,2,3,4]⸨*⸩;      # OUTPUT: «1…4␤» 
 
constant term:<♥> = "♥"; # We don't want to quote "love", do we? 
sub circumfix:<α ω>( $a ) {
    say „$a is the beginning and the end.“
};
 
α♥ω;               # OUTPUT: «♥ is the beginning and the end.␤»

Agda goes even further - it has "mixfix" operators that allow syntactic constructs as complex as if x then y else z to be defined as custom operators. This is done by defining a function with underscores as placeholders, e.g. if_then_else_:

-- Example function name if_then_else_
if_then_else_ : {A : Set} → Bool → A → A → A
if true then x else y = x
if false then x else y = y

I'm not sure whether it also allows custom precedence and associativity rules for such complex operators, though.

Seed7 also has structured syntax definitions which are very similar, including support for precedence levels for operators that aren't purely infix. The documentation gives this example:

$ syntax expr: .loop.().until.().do.().end.loop   is -> 25;
$\endgroup$
1

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .