The @infix Annotation

That is a good point. But yes, I still would. Because I favor consistency over choice (in style, not in actual problem solving abilities). The big difference however is that non-infix style always works, but infix still doesn’t with more than one parameter. So, it is not really a viable choice to always use.

1 Like

What if we give override power to the method author and maintain scala 2 rules for scala 2 code?

  1. No annotation. Can be called as both normal of infix method.
    Scala 2 behavior and behavior for libraries defined in another language.
  2. @infix annotation. Emits deprecation warning in 3.0 when called as normal method.
    Symbolic methods get this by default when compiled with 3.0+.
  3. @no-infix annotation. Emits deprecation warning in 3.0 when called as an infix method.
    (This could be the default for alphanumeric methods in 3.0+)
  4. @infix(alsoDotCall = true). Can be called as both normal and infix method.

Rules could become more strict in scala 3.x or through a linter.

Possible defition of the annotations:

final class infix(alsoDotcall: Boolean = true) extends StaticAnnotation with MethodCallStyle(alsoDotCall, true)
final class no-infix extends StaticAnnotation with MethodCallStyle(true, false)

final trait MethodCallStyle(dotCall: Boolean, infix: Boolean)

This can achieve most goals:

  • It is fully backwards compatible with Scala 2
    symbolic operators are the exception (deprecation warning), but I think the concensus is that this is a good thing.
  • It allows library authors to be opinionated about how their code should be called, without hindering users if the author is not ¹
  • It is a step towards more stricter rules for infix calls
  • It allows for a manual override if the general rule is not the correct one.
    I always think this is a good thing, but especially here it makes deprecation warnings - or errors with -strict or -X-fatal-warnings - more acceptable

¹ Authors of non-Scala libraries will never be opionated, and a lot of developers would still prefer number plus 1 if number is a Java type.

1 Like

This example isn’t relevant, as @infix is only relevant to alphanumeric method names, not symbolic ones:

Infix operations involving symbolic operators are always allowed, so @infix is redundant for methods with symbolic names.

Source

I can’t see how your proposal would allow for manual override, without sacrificing the ability to use -strict or -X-fatal-warnings (which is a non-starter). Can you elaborate?

Can you please stop using this example, it’s not equivalent, and the way it’s being used is a strawman.

Inserting arbitrary underscores in method names is deeply hostile to standard tooling and is incompatible with the primary compilation target, while infix notation is just syntactic sugar.

These are not equivalent, and presenting an argument based on the presumption of their equivalence is a misreprentation of the argument for call-site control of infix notation use.

Once could be a mistake, twice could be a misunderstanding, but three times becomes bad-faith.

1 Like

I’m sorry, but I disagree. They are both language choices about standardization, and how much flexibility is ideal overall for producing clear and comprehensible code. From the thousand-foot view of “if we were making these choices in a tabula rasa environment”, without the existing assumptions based on Scala 2, I think they’re quite comparable.

It’s unfair to claim that @lihaoyi is discussing in bad faith – I think he’s just proceeding from a different viewpoint than you are…

2 Likes

This is a bit harsh. Please focus on desired effects and undesired effects as opposed to personal attacks.

I think what I am hearing both Odersky and Haoyi say is that there is a trade-off with allowing both ways. One is more flexible for humans reading and writing the code at the expense of complexity and blocked possible incremental evolution pathways for other future features due to unanticipated interactions. The other is the opposite, simplification of parsing models reducing complexity and/or unanticipated interactions blocking possible incremental evolution of future features. Clearly attempting to dive into both sides of the trade-off is valueable. And being conservative and tending towards a reduction in some level of redundancy is part of what has made Scala’s evolution possible, valuable, and very rich in the first place.

1 Like

First off, I’ve accused @lihaoyi of nothing more serious than a misunderstanding, yet.

Second, this discussion will go much smoother without the infix notation being treated as equivalent to a language feature that:

  1. Is fundamentally incompatible with standard tooling: everything from grep to vim to emacs to IntelliJ extensively use text search, which breaks badly when underscores can be arbitrarily inserted in method names at the callsite. Infix notation does not have this problem.
  2. Is only present in one language, with negligible mindshare.
  3. Nobody is asking for.

Third: because of these differences, equating the two as a difference in degree, rather than kind, is either a strawman or reductio ad absurdum argument. Either way, it’s not helping this discussion.

Lastly, as I said before, this can only be pointed out so many times before the assumption of good faith no longer holds:

I see several arguing for “consistency over choice”.

If you want consistency, that is trivially simple: be consistent.

Oh…you want consistency in other people’s code?

Well, fair. (At least if interpreted as “I think a standard coding style would be good” and not “I want to control how people write their code”.)

Except that the proposed mechanism won’t help with that, because you will always have author A thinking that map on Foo should be infix and author B thinking that map on Bar should not, ending up with user code like:

foo map f
bar.map(g)

which gives you no more consistency - only less choice.

The only scenario in which the mechanism would work as intended is if 100% of library producers agreed 100% on which kind of method should be allowed as infix and which should not. I’ll leave it as an exercise for the reader to determine the likelihood of that happening.

7 Likes

Then maybe we need a @whitespaced annotation on methods that enforces whitespace inside curly brackets. Why not allow libraries to choose this also?

Why should library writers be empowered at the expense of users? This violates the fundamental principle of APIs: generate things strictly according to spec, but accept things with tolerance for variations by default.

The user of the library is asking the library to accept stuff, so it should be tolerant by default.

Unfortunately, org.foo and org.bar are Java projects. Now what?

(This is not an idle example. We have java.time._, java.lang.StringBuilder, java.math.{BigDecimal, BigInteger}, etc with exactly these sorts of methods.)

My sense is the opposite: whether to use infix syntax depends very little on the library being called, and far more on the code style for the project and on my personal preference.

I wonder why we have a different take on this? There are a few cases where a library has an API for which the usage is really clear one way or the other, but I generally find a pile of methods that could go either way depending details of usage site. Take better-files for example. The API docs say

file.copyTo(target)

But why should I have to do it that way instead of

file copyTo target

in a project that otherwise favors operator style.

2 Likes

Infix notation for alphabetic method names is generally confusing. file copyTo target is not English looking at all. If I would wrote DSL then it would look like: copy file to target. ScalaTest has plenty of DSL that produce intuitive infix syntax, but that library is thoroughly designed to support such notation and also ScalaTest DSL is very frequently used so I can get used to it. Applying infix syntax to random methods reduces legibility IMO. In non-trivial examples it’s not immediately clear what is an argument and what is a method. In ScalaTest DSL it’s generally not a problem because that DSL is mostly for inspecting one object (so not for side effects) and despite infix syntax it’s still clear what is inspected in almost all cases.

2 Likes

I meant override at definition site

@infix(allowDotCall = true) def contains(t: T) allows a method author to explicitly state “it’s okay to call this as either a contains b or a.contains(b)”. @no-infix def /(t: T) allows the author to state that even though it a symbol method, it should still be called as a normal method.

Override at use site would need a way to supress warnings, which has been discussed separately iirc.

Neither is file.copyTo(target)? As long as it’s clearly recognizable as what it is, once learned, and not too hard to learn, why is there a problem?

So don’t do it in non-trivial cases. Most cases are trivial, though, so it covers a lot of ground.

Personally I find ScalaTest confusing and almost every other (modern) usage I see in the wild not-confusing because ScalaTest tries so hard to make it sorta English-like (though not quite, because it doesn’t work syntactically in every case).

Anyway, @infix doesn’t get rid of confusing use cases, though maybe if it diminishes them you will be happier regardless? This seems like the best argument for @infix: “infix notation is generally bad, so things that reduce it are a plus. Annotations are a hassle, and they let me prevent anyone from using my libraries infix, so :+1:

I don’t think this is a great way to do language design in a language that admits a diversity of approaches, but at least it seems self-consistent and non-arbitrary.

3 Likes

I think it’s clear that where we draw the line is arbitrary. Every argument made so far applies equally to callsite case-insensitivity, callsite paren-count choice, as well as strict whitespace layouting

The line is in one place now, and this proposal moves it in the direction of less freedom.

I support less freedom in all these things:

  • I think Go did the right thing with its strict treatment of whitespace, and while it’s a tough to get there, the end state is definitely attractive.

  • I think having nine ways to define a method in Scala is a bad thing, and am happy it’s coming down to seven.

  • I am happy we do not have callsite case insensitivity.

  • I would be happy to remove the operator/dot-notation choice from users, and relegate it to library authors.

  • I would be happy for calls to Java methods look, exactly like Java method calls. (haven’t quite accomplished that due to optional parens still remaining)

Why do I support removing freedoms in all these cases?

  • I think a stricter language with fewer arbitrary options in all ways makes it easier for newbies, who very much want to be told what to do rather than be given arbitrary choices. Newbies have no preference, no conventions to follow, and easily make a mess unless they install a linter (which most won’t know to do so, because they’re Newbies)

  • I think making the language better for newbies is a top priority: the expert users, despite how vocal they are, generally can use Scala competently. All experts start off as newbies, as the above linked SO question demonstrates, and I would like to have more experts in the community as well.

  • I think Scala’s overwhelming flexibility of meaningless choices is a top contributor to its reputation as a “weird” or “too powerful” or “too complex” language, and that this is a fair judgement. What else do you call a language where newbies who try it easily make a mess? Where newbies can’t read each other’s code due to meaningless-but-different choices? Where a newbie has to install a linter he doesn’t know about and pick a convention he doesn’t care about just to achieve the low bar of mutually-comprehensible code?

  • I think limiting that flexibility would harm expert users very little after the transition period, and help people (especially newbies) see past the intimidating mass of meaningless choices to the relatively regular & clean OO/FP paradigm at the language’s core

5 Likes

Okay, that was my impression also. Glad we at least agree on this!

I support more freedom in almost all these things.

Go looks nice mostly because of gofmt, not the language.

This isn’t a helpful characterization of that question. Even the poster can tell that they’re not all the same, despite apparently (or at least for the sake of argument) not understanding the difference between expressions and code blocks, or the point of 0-ary functions.

Me too, because caps look really different from non-caps. Some freedoms make it really hard to figure out what’s going on. For example, allowing normal and pig-latin variants of all method names would be bad too. xs aketay 4 for the…not…win.

I wouldn’t.

So build the linter and formatter into the standard tools, like Rust and Go do.

If newbies find something difficult, it absolutely is valuable to address it, but tooling and education are perfectly valid alternatives to removal of features.

Expert users can end up being served by Scala less well though. I don’t write operator notation for giggles. I write it because when I come back to some code I can understand it faster. It makes me more productive. I avoided it for a long time for consistency, and eventually realized that actually, it doesn’t matter that I have thirty years of experience reading function/method calls with parens; they’re still visual clutter in many contexts and slow down comprehension. They don’t slow me down very much except in complex trees, which I try to avoid anyway if at all possible, but it’s enough to notice.

It could be. I’m not sure how to measure this. If I did know how to measure this, and measure which things present the most-troubling meaningless flexibility, I’d be inclined to go after those.

People do complain about operator notation, but generally about long chains of such things like

  xs map transform take lookAhead filterNot select groupBy keys mapValues length

Let’s not advocate for that.

Absent studies on this, I would ask people who like and broadly use the syntax, and be inclined to believe what they say (taking into account that people tend to overestimate how hard it is to change, but still generally believe them).

If newbies find it hard, the first question to ask is whether there’s a good way to get them up to speed before asking experts to unlearn it. (Unlearning is harder than learning.) I don’t see much attention here explaining why newbie-friendly measures like a code formatter/linter aren’t both (1) needed anyway for other things and (2) a superior solution to the problem.

1 Like

Haha as the original poster of the question, back in 2011 when I was a newbie learning Scala, everything you said is accurate, but my characterization of my own confusion back in 2011 is entirely on point!

That was with ~10 years experience programming in multiple languages, so just new to Scala, not new to programming in general

3 Likes

I really like @alpha. I’ve seen that developers new to Scala continuously push back against symbolic names like those in the Scala parser combinators library, Scala collections library (:/, \:), and Scalaz / Cats.

The @alpha annotation will allow library authors to easily add alphanumeric aliases for operators, which will make it more likely to happen, and at significantly lower cost (no repeated Scaladoc, no repeated method signature, etc.).

As for @infix, I have seen much confusion about the rules for when infix syntax can be used; and too much inconsistency about when to use or to avoid infix. After not using and using the feature on and off over 10 years, I myself decided it makes sense to use infix in precisely the case where a symbolic operator makes sense: i.e. the left and right hand sides of the operator, together with the result of the operator, have the same type (or same type constructor, anyway); and the operator obeys one or more algebraic laws.

This means I believe that a union b is an acceptable use of infix, because a and b and a union b all have the same type, and follow well-defined laws; similarly, a * b is a good use of operators, for similar reasons. Meanwhile, old school Dispatch-style (ab)use of operators and infix notation is unacceptable, no matter how “cute”.

Personally, I think it would be great if Scala 3.1 or 3.2 required @alpha for all symbolic names (to ensure such names can be Googled, pronounced, and used by people new to Scala); and moreover, only allowed infix notation for alphanumeric names so defined; although perhaps that’s a little draconian, and it would be ok to permit @infix, but to have @alpha also imply @infix for the alphanumeric alias.

8 Likes

This is an underappreciated point. Having map sometimes be allowed infix is arguably more confusing than allowing or deprecating it altogether. I personally prefer dot notation and highly value consistency, so I very much appreciate the intent of @infix annotation. That said, there might be a better way to reach the same goal. The @alpha annotation is a lot less controversial; it’s worth examining differences between these two.

@alpha prevents library authors from making bad design decisions (at the very least, it mitigates a bad decision). Importantly, it applies uniformly. If you see an operator, you know you can use it with infix notation, and you know it has an alphanumeric alias you can search. We’ve improved a Potentially Confusing Language Leature. This is an easy win.

@infix, in comparison, gives library authors more room to make bad, or at least inconsistent, design decisions. It also adds more edge cases to a Potentially Confusing Language Feature. Whether or not you can infix the words map, in, contains, dependsOn now depends on where they’re defined (unlike operators). Meanwhile, newbies will still see Scala 2 code all over the internet, which adds to the confusion.

Perhaps rather than discussing whether or not @infix is good, we should discuss alternative solutions that reach the same goal of code consistency across a codebase (which I think we all can agree is good). Is there any way we can make @infix more thorough or consistent? Can we at least figure out how to make sure library author’s agree with each other? Is it really okay to let library authors give their users more “meaningless choices”? I like John’s intuition that the rules should be consistent according type. Why don’t we also make it consistent according to method name? Or why not make this
import scala.language.infixOps? Or enable via a compiler flag? (This discourages newbies from using it without requiring they learn about linters.) Why don’t we just get rid of infix altogether? Or what if we forced infix usage to eliminate all the grey area methods?

5 Likes

Wish granted. It’s actually mandatory for symbolic names :slight_smile:

2 Likes

I’m not happy about backticked definitions requiring @alpha. I have a project where I kind of abuse the feature, basically to have “strings” that I can refer to by their value but statically enumerate.

Some of this is in a library and then in applications I add a lot more on top. https://github.com/nafg/simpleivr/blob/master/core/src/main/scala/simpleivr/Sayables.scala

1 Like

My 2 cents: I’ve never wasted so much of my own time rewriting expressions to completely equivalent expressions as my personal style preferences change, in any other language, as I have in Scala. I love the freedom that Scala gives me but sometimes it can be a burden without benefit. Therefore I think the @infix annotation will be a great addition to the language!

I do have a criticism of it though in that it still permits multiple usage choices and allows inconsistency in a codebase (and churn for devs as opinions evolve). I would suggest it we make it one or the other. No @infix annotation means it cannot be used infix, and presence of an @infix annotation means it can only be used infix. That way there’s only one way to use a method = guaranteed consistency. I’d rather not have to think, decide, argue about, reconsider, rewrite things like this even if it means I write in a style I don’t fully agree with, or I lose a small percentage of readability.

(Apologies if that has already been mentioned above - I’ve only done a quick skim)

5 Likes