Skip to content

Proposal: Change @ to as #9829

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 Sep 19, 2020 · 38 comments
Closed

Proposal: Change @ to as #9829

odersky opened this issue Sep 19, 2020 · 38 comments

Comments

@odersky
Copy link
Contributor

odersky commented Sep 19, 2020

I have a late syntax change proposal: Change @ as the pattern matching binder to as.

Example:

  x match 
    case s as Success(x, in1) => ...
    case f as Failure(_, in1) => ...

  e match
    case (xs as hd :: tl, y) => ...

Why @ is not a good choice for pattern matching:

  • It is an obscure symbol with a different meaning. I find the two worst operators to use in programming languages are @ and $ since both have everyday uses that have nothing to do with their role in programming. We have avoided $ so far, let's get rid of @ as well.

  • It comes with the wrong precedence. @ binds more weakly than every other infix operator, yet being a non-standard symbol it would suggest that it binds more strongly.

By contrast, as is readable and it comes with the right precedence. It also reinforces learning in relation to givens. In both cases x as would be an optional label.

An objection could be that in patterns, as would be followed by a pattern whereas in givens it's followed by a type. But there's precedence for that: | plays a similar dual role: it connects patterns in patterns and types in types.

Migration

For 3.0, allow both as and @. For 3.1. either deprecate @ or disallow it altogether and offer an automatic rewrite.

@som-snytt
Copy link
Contributor

More precedence for precedence: lampepfl/dotty-feature-requests#120

Changing the symbol instead of the precedence feels quite normal.

Maybe allow case (xs @$ hd :: tl, y) => under -Xbad-@$s.

@sjrd
Copy link
Member

sjrd commented Sep 19, 2020

TBH I don't like that we are still introducing completely new changes this late in the process. We're already swamped in changes that we cannot keep track of.

Independently of that, this seems to be a very cosmetic change that is not justified by anything else than "it does not look nice; let's use something nicer".

If we really have to change how binding works in pattern matching, I'd like something deeper. I would like a real analogy with |. I would like to express the example as:

  x match 
    case s & Success(x, in1) => ...
    case f & Failure(_, in1) => ...

  e match
    case (xs & hd :: tl, y) => ...

This is very semantic: I want the scrutinee to match both the pattern on the left (which is a capture, so a binding) and (&) the pattern on the right (which is destructuring). The captures/bindings on both sides of & would be visible in the guard and the body.

There would be no restriction on what we can put on the left and on the right. We could actually put a pattern on the left (I have occasionally wanted to match on something that had to satisfy two different patters/extractors). The very concept of a binding would cease to exist. All we would have would be captures. That would actually simplify the language.

@odersky
Copy link
Contributor Author

odersky commented Sep 19, 2020

@sjrd Rolling this into conjunction is indeed very elegant. But I believe it is also a lot more obscure for the reader who is not familiar with the idiom. For the cases with as it's immediately clear what they mean. For the cases with &, not so much.

@odersky
Copy link
Contributor Author

odersky commented Sep 19, 2020

Independently of that, this seems to be a very cosmetic change that is not justified by anything else than "it does not look nice; let's use something nicer".

It's more like: I find it awkward teaching this. Can we avoid that? It's not a matter of nicety but of promoting the right intuitions for students.

@julienrf
Copy link
Contributor

I find @sjrd’s suggestion very elegant.

To add to Martin’s introductory post, here is what the proposal would look like in the other situations where we currently use @:

// val someX @ Some(x) = Some(42)
val someX as Some(x) = Some(42)

// for (xs @ hd :: tl <- eventuallyInts) ()
for (xs as hd :: tl <- eventuallyInts) ()

@odersky
Copy link
Contributor Author

odersky commented Sep 19, 2020

Note that as/@ is actually not completely subsumed by a hypothetical &. For instance, we use this pattern in Flags.scala:

val (_, Captured @ _, NoInits @ _) = newFlags(32, "<captured>", "<noinits>")

This makes an uppercase identifier a bound variable. I believe there's no other way to do this, except for writing separate val definitions.

@LPTK
Copy link
Contributor

LPTK commented Sep 19, 2020

It's worth noting that both SML and OCaml use as for pattern binding, which I've always found very nice to read, especially as opposed to Haskell and Scala's cryptic @.


There was also the idea of allowing as inside expressions too, where it could play the role of an "inline" val binder, which can be useful when debugging, and in situations like:

val keys = Set(a as newKey("a"), b as newKey("b"), c as newKey("c"))

instead of the verbose:

val a = newKey("a")
val b = newKey("b")
val c = newKey("c")
val keys = Set(a, b, c)

@He-Pin
Copy link
Contributor

He-Pin commented Sep 19, 2020

Rust is currently using @, I think as is easier for beginners.

@mpilquist
Copy link
Contributor

Would as be a soft keyword and hence still usable in type and term names?

@odersky
Copy link
Contributor Author

odersky commented Sep 20, 2020

Yes, as can stay a soft keyword. It would be interpreted as a binder only when used as an infix operator in patterns.

@djspiewak
Copy link

djspiewak commented Sep 20, 2020

So I'm a fan of the proposed syntax, just not a fan of making more waves so late in the game. Most usage of Dotty right now is cross compiling with Scala 2, so it's not a huge impact, but I feel like the bar should be pretty high for such changes.

One immediate problem that this creates, for example, is the fact that "as" is actually a commonly used function name across the Cats ecosystem. I believe it's also quite common in a number of other libraries (with varying meaning). All of those would need to be stuffed into backticks during migration (this is a problem that "then" really doesn't have for some reason), and that's more pain on the ecosystem and more pain on users.

(Edit: the soft keyword proposal above resolves this concern)

If I could rewind a decade and make the change you're suggesting, I would be all in favor of it. But at this stage, I feel it's a little too much in a process which is already fraught with variables.

@som-snytt
Copy link
Contributor

Worth noting that pattern alternation doesn't currently parse as infix:

scala> 42 match { case n @ 42 | 17 => n }
1 |42 match { case n @ 42 | 17 => n }
  |                ^^^^^^
  |                Illegal variable n in pattern alternative

Scala 2.12 allows an extractor named $bar:

scala> (42, 17) match { case n @ 42 `|` 17 => n }
val res3: (Int, Int) = (42,17)

@odersky
Copy link
Contributor Author

odersky commented Sep 20, 2020

@djspiewak One possibility would be to try it out now, and if anything major breaks backtrack. The only thing that would break is if some code uses as as an infix operator in patterns.

@djspiewak
Copy link

djspiewak commented Sep 20, 2020

Lower case "as" can't be used infix in patterns without quoting (I believe) so I think that case is alright.

I'm cool with "try it and see", so long as we watch carefully as we do. 🙂

@odersky
Copy link
Contributor Author

odersky commented Sep 20, 2020

Lower case "as" can't be used infix in patterns without quoting (I believe) so I think that case is alright.

It can be used, actually:

case class as(x: Int, y: Int)
val x = as(1, 2) match
  case 1 as 2 => ???

But maybe nobody did use it that way.

@Sciss
Copy link
Contributor

Sciss commented Sep 20, 2020

does that mean no Scala 2 source can be cross-compiled without resorting to 3.0-migration ? Because I'm now compiling many projects successfully, and they are all ok without 3.0-migration...

@som-snytt
Copy link
Contributor

som-snytt commented Sep 20, 2020

scala> val as = "(a*)(.*)".r
val as: scala.util.matching.Regex = (a*)(.*)

scala> "aaabbb" match { case as as bs => s"$as .. $bs" }
val res0: String = aaa .. bbb

but I'm willing to forgo this in future.

Edit: I just got the pun from earlier, "a process fraught with variables."

@nafg
Copy link

nafg commented Sep 21, 2020 via email

@NthPortal
Copy link
Contributor

does that not mean that as could be implemented the same way in userland? (roughly, excluding the nuisance about capitalization)

@LPTK
Copy link
Contributor

LPTK commented Sep 21, 2020

@NthPortal it actually can:

scala> object as {
     |   def unapply[A](a: A) = (a, a)
     | }
// defined object as

scala> Some(1) match { case a as Some(x) => (a, x) }
val res2: (Some[Int], Int) = (Some(1),1)

Recommending the use of this operator (which would be added to Predef) and deprecating @ would be a nice way of simplifying the language.

@tabdulradi
Copy link

@NthPortal it actually can:

scala> object as {
     |   def unapply[A](a: A) = (a, a)
     | }
// defined object as

scala> Some(1) match { case a as Some(x) => (a, x) }
val res2: (Some[Int], Int) = (Some(1),1)

Recommending the use of this operator (which would be added to Predef) and deprecating @ would be a nice way of simplifying the language.

As much as I like implementing @/as in the userland, I am worried about the cost of tuple instantiation.

@nafg
Copy link

nafg commented Sep 21, 2020 via email

@odersky
Copy link
Contributor Author

odersky commented Sep 21, 2020

@LPTK It's interesting that one could define as as an extractor in that way. But it still does not solve the problem how to get capitalized names as binders.

@sjrd
Copy link
Member

sjrd commented Sep 21, 2020

TBH, I find the capitalized names as binders a fringe use case, bordering on abuse of the feature. Not as bad as abstract case classes, but still.

But anyway, it doesn't matter. There's one thing that as-in-user-land or even my initial & proposal does not cover, and which is much more common: they don't refine the type of the binder. So in something like:

val tree: Tree = ???
tree match {
  case Apply(sel & Select(_, _)) =>
    sel.qual // does not compile: sel is a Tree, not a Select
}

and I don't think there is any way to either a) implement that in user-space nor b) correctly specify with a language-defined commutative &. To have this behavior, the binder must be non-commutative.

So I'm afraid we do need the concept of binder as defined in the language.

@LPTK
Copy link
Contributor

LPTK commented Sep 21, 2020

@sjrd good point. You can always write Apply((sel: Select) as ...), but it is more verbose than with @.

I don't think there is any way to [...] b) correctly specify with a language-defined commutative &

Given a pattern A(...as) & B(...bs), couldn't you look at the input types T_A and T_B of the respective extractors A and B (for a pattern variable A = v, it would be T_A = Any) and then type check the ...as and ...bs subpatterns based on applying each extractors to a scrutinee of type T_A & T_B?

@lihaoyi
Copy link
Contributor

lihaoyi commented Sep 21, 2020

I personally don't really like the keyword as, due to the somewhat unclear left-right order between the pattern thing being bound. Perhaps it's my personal experience speaking, but I'm very used to xxx as yyy binding the name yyy from Python, where this is front-and-center in the language's import/try-except/with-block syntax, and here it's the other way around. Python's PEP622 uses the := walrus operator for this same thing, which makes it very clear that the LHS is the name being assigned. I suppose Scala's equivalent would be using the = or <- operators which currently bind names to the left.

@djspiewak
Copy link

I am worried about the cost of tuple instantiation.

Pattern matching is already prohibitively slow for any code which is sensitive to allocations, so I'm not sure that we need to worry too much about adding a single layer of boxing in this case.

I'm worried more about the interaction between this feature and tableswitch, which currently works just fine using @, but would break the optimization if it were not a primitive.

@bilal-fazlani
Copy link

I like this proposal. From perspective of explaining scala pattern matching to new people, 'as' makes more sense than '@'

@odersky
Copy link
Contributor Author

odersky commented Sep 21, 2020

There's already a discussion in #9837 about the order of the operands of as. In a nutshell: there's precedent for both orders. Since one of the main reasons of the proposal is to reinforce learning in conjunction with given instances, the order is determined to be what it is in this proposal.

@prolativ
Copy link
Contributor

Actually couldn't we simply have syntax like

case foo: Foo(_) => ...

by analogy to

case foo: Foo => ...

?

@tabdulradi
Copy link

tabdulradi commented Sep 21, 2020 via email

@som-snytt
Copy link
Contributor

Because of the mixed precedents for order, there is a danger that a beginner might write their patterns as-backward.

@prolativ
Copy link
Contributor

If we can't reuse :, what about =?
That would quite nicely go with the syntax for value definitions.

case class Foo(i: Int)
object Foo

val fooInstance: Foo = new Foo(1)
val fooObject = Foo

x match {
    case fooInstance: Foo => ...
    case fooObject = Foo => ...
}

val someString = Some(s) = Some("xyz")

for {
    foo = Foo(i) <- Some(Foo(10))
} println(foo.toString + i)

for {
    case someInt = Some(_) <- Seq(Some(10), None, Some(5))
} yield someInt

The only problem I can see for now would be with patterns like

list match {
    head = hd :: tail => ...
}

if we decide to change precedence of bindings in patterns.

@sjrd
Copy link
Member

sjrd commented Sep 22, 2020

I don't think there is any way to [...] b) correctly specify with a language-defined commutative &

Given a pattern A(...as) & B(...bs), couldn't you look at the input types T_A and T_B of the respective extractors A and B (for a pattern variable A = v, it would be T_A = Any) and then type check the ...as and ...bs subpatterns based on applying each extractors to a scrutinee of type T_A & T_B?

Hum, perhaps, yes. I can't convince myself either that it would indeed work or that it wouldn't. If it works, that would be very nice!

@vasily-kirichenko
Copy link

I find F#'s syntax, where the order of pattern and as is reverted is more natural and easier to read. In Scala it would look like this:

x match 
    case Success(x, in1) as s => ...
    case Failure(_, in1) as f => ...

  e match
    case (hd :: tl as xs, y) => ...

Why it's better? Because patterns are placed immediately after case which makes reading the whole match a lot easier than when the first thing you see (and have to skip) is a local binding, which does not bring any information in understanding the whole match, which is somewhat similar to the function definition in C-family languages, where return type obfuscates signature: public MySuperUsefulType foo() {}, you have to skip the return type to find the function name, it's hard.

@jdegoes
Copy link

jdegoes commented Sep 22, 2020

@odersky Big 👍 from me given it's a soft keyword, and the pedagogical benefits of aligning this with as in givens are undeniable and significant.

odersky added a commit that referenced this issue Sep 24, 2020
Fix #9829: Allow `as` in place of `@` for pattern bindings
@noresttherein
Copy link

Can I ask if as is becoming a keyword only in the context of pattern matching, or a general reserved word? I am very worried about new scala's claiming quite a bit of new keywords, especially that no effort is made to use less common terms. This is a serious issue for libraries - the new keywords are likely widely used because they are so natural in DSL contexts.

@tabdulradi
Copy link

Can I ask if as is becoming a keyword only in the context of pattern matching, or a general reserved word? I am very worried about new scala's claiming quite a bit of new keywords, especially that no effort is made to use less common terms. This is a serious issue for libraries - the new keywords are likely widely used because they are so natural in DSL contexts.

Check #9829 (comment)

Yes, as can stay a soft keyword. It would be interpreted as a binder only when used as an infix operator in patterns.

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