Dotty: Possible syntax for infix operators

Created on 17 Feb 2019  Â·  20Comments  Â·  Source: lampepfl/dotty

Most helpful comment

Recap'ing my opinion on the matter from the offline meeting:

I think the proposal stated above tries to address the initial problem with solutions that are too complicated. And in doing so, it will pose non-trivial compatibility and migration issues. Here is a different proposal that addresses the core problems with a different solution with minimum impact.

Let me start with the problem of variations in style, which is also presented as choice paralysis. As was mentioned by @propensive, we can make sure the choice is enforced to the library author, rather than every call site. For that, I propose an annotation @infix that can be used on any binary def as follows:

@infix
def +=(that: A): Unit = ...

The presence or absence of the annotation dictates the style to use at call site. If I call += with . syntax, I get a warning (eventually an error). If I call a non-infix method as infix, I similarly get a warning. The annotation does not alter type checking in any way. As such, an annotation is fine. Warnings/errors would also be emitted for overrides that do not agree on whether or not they are @infix. Overloads are not affected.

Note that @infix is independent of whether method names are symbolic or not. I can annotate to or eq with @infix, for example.

About the overuse of symbolic names, the proposal suggests that we use alphanumeric aliases (whether "by hand" or with dedicated syntax). I think this is a mistake---and I had already said that about aliases in the 2.13 collections---because that very solution leads to choice paralysis itself: am I supposed to use += or append in my code that uses the collections? I don't know. Choice paralysis.

If we come back to the initial problem that aliases wanted to address, they were twofold:

  • How do I systematically document a good name for symbolic operators, for understanding and searching purposes?
  • How do I give better interoperability with other JVM languages such as Java?

The two problems can be solved in a way that does not affect typechecking in Scala and does not lead to choice paralysis either: use another annotation, whose name I am yet unsure of but I'll use @name for the moment:

@infix @name("append")
def +=(elem: A): Unit = ...

The string in the annotation is used by IDEs and other tools as help text. It could even come up in auto-completion. But the only valid name in Scala source code remains +=, so there is no choice paralysis. The name is further used as the JVM name of that method, which gives it a good name for interoperability purposes.

A warning/error could also be emitted if a method with a symbolic name does not have @name.

For compatibility with Scala 2, we can very easily define the annotations in the 2.14 library. They might not be checked by Scala 2, but they will be good enough for cross-compilation purposes. The checks are also simple enough that they could be backported to Scala 2 without too much effort, I believe.

All 20 comments

When it comes to operators, current Scala has two simple rules:

  • An identifier can be alphanumeric or symbolic
  • A method call may be written with a "." or as an infix operator

Simple as these rules are, they give a lot of flexibility. The problem is that syntactic flexibility like that can lead to choice paralysis and disagreement what style to use. Concretely, there are two problems:

  1. Overuse of symbolic operators.
  2. Variations in style whether a method is written infix or with a ".".

A rule to combat (1) is that every symbolic operator should be an alias of an alphanumeric method, which can be googled more easily. It's a good rule, but it is tedious to write two methods instead of one, so one might be tempted to cut corners.

As to (2), the problem is that it's hard to come up with a hard rule that works for everyone.
Many people would write

a eq b
a max b

Some would also write

xs map f
xs flatMap f

I have even seen

List range (1, 10)

I admit this made my eyes pop. The problem is that there's no clear guidance what to use. I have recently started to _never_ write alphanumeric methods directly as infix operators. Mostly I use method syntax, and if that feels too unnatural I resort to put the operator in backticks. I.e.

a `eq` n

instead of

a eq b
a.eq(b)

But that's also just a convention, which is enforced by nothing.

The choice paralysis problem probably has to be solved by delegating the choice to someone else, typically (but not necessarily) the library author.

During the discussions on extension methods, I postulated (somewhere) a slightly radical idea that it should only be possible to define infix and/or symbolic methods as extension methods, something like:

def (m1: Matrix) + (m2: Matrix) = m.add(m2)

If we were to force method definitions to be alphanumeric (non-symbolic) only, then using symbolic operators (those not in Predef) would require an explicit import to indicate where they come from.

So if you really wanted to use methods like flatMap and map in infix style, then you still could, but you would have to want to do it enough to define them as extension methods. And methods like eq could be infix extension methods defined in Predef.

A rule to combat (1) is that every symbolic operator should be an alias
of an alphanumeric method, which can be googled more easily. It's a good
rule, but it is tedious to write two methods instead of one, so one might
be tempted to cut corners.

If it were an enforced rule, how could one cut corners (other than by
omitting the symbolic alias)?

On Sun, Feb 17, 2019 at 1:43 PM Jon Pretty notifications@github.com wrote:

The choice paralysis problem probably has to be solved by delegating the
choice to someone else, typically (but not necessarily) the library author.

During the discussions on extension methods, I postulated (somewhere) a
slightly radical idea that it should only be possible to define infix
and/or symbolic methods as extension methods, something like:

def (m1: Matrix) + (m2: Matrix) = m.add(m2)

If we were to force method definitions to be alphanumeric (non-symbolic)
only, then using symbolic operators (those not in Predef) would require
an explicit import to indicate where they come from.

So if you really wanted to use methods like flatMap and map in infix
style, then you still could, but you would have to want to do it enough to
define them as extension methods. And methods like eq could be infix
extension methods defined in Predef.

—
You are receiving this because you are subscribed to this thread.
Reply to this email directly, view it on GitHub
https://github.com/lampepfl/dotty/issues/5937#issuecomment-464493331,
or mute the thread
https://github.com/notifications/unsubscribe-auth/AAGAUFYBNAKcjZgGH_grQ4MJTAjAhQrFks5vOaLBgaJpZM4a_sWC
.

One possibility to solve (1) and (2) together would be to introduce an infix modifier or annotation. It could exist in two forms. Simple:

infix def to (limit: Int): Range

and with argument:

infix "+=" def append(x: T): this.type

The simple form can be understood to be an abbreviation of the parameterized form where the method name is repeated. I.e. the to definition above would be a shorthand for

infix "to" def to (limit: Int): Range

Some possible rules would be:

  1. A method with an infix definition infix "op" def methodName ... can be referred to by its operator name op.
  2. Infix declarations must agree: If two infix methods are members with the same name and matching types in the same class, then their operators must be the same.
  3. Method names must agree: If two infix methods with matching types in the same class define the same operator then their method names must be the same.
  4. Infix operation syntax a op b can be used _only_ if op is the operator of an infix method such as infix "op" def m .... It translates in that case to a.m(b).
  5. Symbolic names are allowed only as infix operators. Names of other vals or defs cannot be symbolic.

Rules (1-3) introduce a convenient syntax to define an operator name with a normal method. Rules (4-5) are normative; they rule out existing possibilities. Adopting rule (5) has the advantage that then all fields and methods could be represented in alphanumeric form. Since the operator name is used purely internally, it needs not be translated to bytecode. This is also great for interoperability with other languages.

Similar rules could be introduced for types. I.e. all type definitions must be alphanumeric but they can have infix modifiers just like methods can.

Another question is what is the best syntax for infix declarations. I have used

infix "+=" def append(x: T): this.type

Other possibilities would be:

@infix("+=") def append(x: T): this.type

infix += def append(x: T): this.type

infix `+=` def append(x: T): this.type

Not sure what's best. Annotations look probably most familiar, but this might be considered to overstretch annotation usage to influence typing and name resolution like this.

Two more rules, which are left out so far, could be:

  1. Methods declared infix must take exactly one leading parameter list (generalized accordingly for extension methods).
  2. Every reference to an infix operator must be in an infix operation.

Without (6) and (7), it is still possible to define symbolic nouns. E.g. use * as a standalone value, not an operator:

infix * def root: Root = ...

It's admittedly a mis-use of notation since there is nothing infix about root. With (6) and (7), that would be no longer possible.

It looks very strange to put the infix operator inside quotes, especially if the decision is taken to introduce a new infix keyword. Quoting it like a literal string make it look decidedly non-firstclass. I don't think there's a precedent elsewhere in the compiler for quoting identifiers.

5. Symbolic names are allowed only as infix operators. Names of other vals or defs cannot be symbolic.

This would be quite painful on the browser side, for the same reason Sébastien recently cited: $ is a standard symbol, both as an object and as a function. (It's the sigil for jQuery, the most commonly-used browser library.) This allows many JS documentation examples to be dropped into Scala.js code unchanged, which I use as a significant selling point for SJS...

@jducoeur In fact $ does not count as a symbol; it is treated as alphanumeric.

Recap'ing my opinion on the matter from the offline meeting:

I think the proposal stated above tries to address the initial problem with solutions that are too complicated. And in doing so, it will pose non-trivial compatibility and migration issues. Here is a different proposal that addresses the core problems with a different solution with minimum impact.

Let me start with the problem of variations in style, which is also presented as choice paralysis. As was mentioned by @propensive, we can make sure the choice is enforced to the library author, rather than every call site. For that, I propose an annotation @infix that can be used on any binary def as follows:

@infix
def +=(that: A): Unit = ...

The presence or absence of the annotation dictates the style to use at call site. If I call += with . syntax, I get a warning (eventually an error). If I call a non-infix method as infix, I similarly get a warning. The annotation does not alter type checking in any way. As such, an annotation is fine. Warnings/errors would also be emitted for overrides that do not agree on whether or not they are @infix. Overloads are not affected.

Note that @infix is independent of whether method names are symbolic or not. I can annotate to or eq with @infix, for example.

About the overuse of symbolic names, the proposal suggests that we use alphanumeric aliases (whether "by hand" or with dedicated syntax). I think this is a mistake---and I had already said that about aliases in the 2.13 collections---because that very solution leads to choice paralysis itself: am I supposed to use += or append in my code that uses the collections? I don't know. Choice paralysis.

If we come back to the initial problem that aliases wanted to address, they were twofold:

  • How do I systematically document a good name for symbolic operators, for understanding and searching purposes?
  • How do I give better interoperability with other JVM languages such as Java?

The two problems can be solved in a way that does not affect typechecking in Scala and does not lead to choice paralysis either: use another annotation, whose name I am yet unsure of but I'll use @name for the moment:

@infix @name("append")
def +=(elem: A): Unit = ...

The string in the annotation is used by IDEs and other tools as help text. It could even come up in auto-completion. But the only valid name in Scala source code remains +=, so there is no choice paralysis. The name is further used as the JVM name of that method, which gives it a good name for interoperability purposes.

A warning/error could also be emitted if a method with a symbolic name does not have @name.

For compatibility with Scala 2, we can very easily define the annotations in the 2.14 library. They might not be checked by Scala 2, but they will be good enough for cross-compilation purposes. The checks are also simple enough that they could be backported to Scala 2 without too much effort, I believe.

@sjrd I like that proposal! One potential abbreviation would be to merge @infix and @name. I.e.

@infix("append")
def +=(elem: A): Unit = ...

Being shorter, it will encourage stating alphanumeric aliases. We could retain @name as an annotation that can be used stand-alone, in case we want to have a symbolic thing that is not an infix operator. But maybe it should be @externalName or something like that, then.

If symbolic operators will have an annotation specifying the JVM name of
the method, would the whole $bang$plus encoding scheme become unnecessary?

On Mon, Feb 18, 2019 at 12:14 PM odersky notifications@github.com wrote:

@sjrd https://github.com/sjrd I like that proposal! One potential
abbreviation would be to merge @infix https://github.com/infix and @name
https://github.com/name. I.e.

@infix("append")

def +=(elem: A): Unit = ...

Being shorter, it will encourage stating alphanumeric aliases. We could
retain @name as an annotation that can be used stand-alone, in case we
want to have a symbolic thing that is not an infix operator. But maybe it
should be @externalName or something like that, then.

—
You are receiving this because you commented.

Reply to this email directly, view it on GitHub
https://github.com/lampepfl/dotty/issues/5937#issuecomment-464814901,
or mute the thread
https://github.com/notifications/unsubscribe-auth/AAGAUNdruuj2k2p3E69O-bMwfnWflSaYks5vOt9ngaJpZM4a_sWC
.

@odersky The merge is shorter, but its meaning is very misleading IMO. @infix("append") suggests that the infix notation for += is append, which makes no sense. If we want to merge them we need a name that is much clearer than that.

it will encourage stating alphanumeric aliases

A warning when omitting @name for symbolic method names would have that effect too, and more strongly so.

@nafg In theory, yes. In practice, dropping it right away would expose us to regressions in mixed Scala/Java codebases where Java already calls some $-encoded methods of Scala.

@sjrd I see your argument. Maybe @alpha for @name? I find @name too generic.

Yes, @alpha is better!

I have even seen

List range (1, 10)

I agree that this doesn't make sense, but I thought that this is already handled elsewhere.

I don't really agree that all the other problems stated here are actually problems that require solving. Or that the extra rules and handholding are strictly better than the problems they are solving.

Perhaps an optional @alias or @alpha annotation could solve a small amount of boilerplate. Making it mandatory just seems like _more_ boilerplate. By the way, I think some libraries intentionally define these aliases to give users the choice, and not (only) for a Java-friendly interface.

Perhaps an optional @alias or @alpha annotation could solve a small amount of boilerplate. Making it mandatory just seems like _more_ boilerplate. By the way, I think some libraries intentionally define these aliases to give users the choice, and not (only) for a Java-friendly interface.

Personally I think consistency is worth more than avoiding some boilerplate. I really like working with Scala, but I think one of the biggest problems we face in using it professionally is that there is too much room for personal style. Now you can use linters and formatters, but I think it would be nice if Scala itself became a bit more opinionated.

This proposal goes in that direction and I am favor, regardless if it becomes a keyword or annotation, or something else that achieves the same thing.

Personally I think consistency is worth more than avoiding some boilerplate.

In some sense I couldn't agree more. It's what I like about the way it is now: all operators are just methods, every call is a method call, all methods are treated equally.

In some sense I couldn't agree more. It's what I like about the way it is now: all operators are just methods, every call is a method call, all methods are treated equally.

Consistency, like simplicity, is always relative. Often consistency and simplicity on the level of a _programming language_ is in direct conflict with consistency and simplicity of the _programs written in that language_.

5975 now defines a doc page that gives an elaboration of the proposed rules for @infix and @alpha.

alpha as an annotation looks good.

Should we have infix as a modifier like override?

Was this page helpful?
0 / 5 - 0 ratings

Related issues

julienrf picture julienrf  Â·  3Comments

ohze picture ohze  Â·  3Comments

andreaTP picture andreaTP  Â·  3Comments

m-sp picture m-sp  Â·  3Comments

liufengyun picture liufengyun  Â·  3Comments