Skip to content

Possible syntax for infix operators #5937

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
odersky opened this issue Feb 17, 2019 · 20 comments
Closed

Possible syntax for infix operators #5937

odersky opened this issue Feb 17, 2019 · 20 comments

Comments

@odersky
Copy link
Contributor

odersky commented Feb 17, 2019

No description provided.

@odersky
Copy link
Contributor Author

odersky commented Feb 17, 2019

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.

@propensive
Copy link
Contributor

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.

@nafg
Copy link

nafg commented Feb 17, 2019 via email

@odersky
Copy link
Contributor Author

odersky commented Feb 18, 2019

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.

@odersky
Copy link
Contributor Author

odersky commented Feb 18, 2019

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.

@propensive
Copy link
Contributor

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.

@jducoeur
Copy link
Contributor

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...

@odersky
Copy link
Contributor Author

odersky commented Feb 18, 2019

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

@sjrd
Copy link
Member

sjrd commented Feb 18, 2019

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.

@odersky
Copy link
Contributor Author

odersky commented Feb 18, 2019

@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.

@nafg
Copy link

nafg commented Feb 18, 2019 via email

@sjrd
Copy link
Member

sjrd commented Feb 19, 2019

@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.

@odersky
Copy link
Contributor Author

odersky commented Feb 19, 2019

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

@sjrd
Copy link
Member

sjrd commented Feb 19, 2019

Yes, @alpha is better!

@Jasper-M
Copy link
Contributor

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.

@bmeesters
Copy link

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.

@Jasper-M
Copy link
Contributor

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.

@odersky
Copy link
Contributor Author

odersky commented Feb 21, 2019

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.

@odersky
Copy link
Contributor Author

odersky commented Feb 23, 2019

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

@AshishPrasad
Copy link

alpha as an annotation looks good.

Should we have infix as a modifier like override?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

9 participants