Pre-SIP: Bind variables for alternative patterns

Please see the link here.


cc @LPTK specifically since you’ve attempted this before and may have some insights on the implementation or problems.

6 Likes

I made a similar post not so long ago:

It looks into a lot of edge cases, like for example:

tree match
  case Wrap(subtree) | Leaf(subtree: Int) =>
    // What should be the type of subtree here ?
1 Like

I don’t think there are. If the actual type is always a union of all the derived unapply types then it’s well defined by the rules of union types.

1 Like

Thank you for the link!

It was very helpful, since I was able to find some bits of commentary that I had not seen before. I will certainly update the draft to include the new information before submitting it to the SIP committee.

Regarding your first concern,

tree match
  case Wrap(subtree) | Leaf(subtree: Int) =>
    // What should be the type of subtree here ?

I would expect subtree to be the lub of Tree and Int which would mean subtree: Tree | Int. The draft currently has an example of this case here.

I may add a restriction to the type as an alternative in the SIP, but while implementing it, I came to the conclusion that the difference between the two implementations is negligible and it adds a restriction which stops a fairly common case which I’ve encountered:

enum Num:
  case Int(i: Int)
  case Str(s: String)

  def intValue = this match
    case Int(x) => x
    case Str(s) => s.toInt

enum Foo:
  case Bar(n: Num.Int)
  case Baz(n: Num.Str)
  case Qux

  def bar = this match {
    case Bar(n) | Baz(n) => // This would fail here, or we need to add a type-ascription!
      val x = n.intValue
      // rest of code
  }

Regarding the second concern, that P1 | P2 is not the same as P2 | P1, I believe that the semantics are already well-defined since the following is already allowed in the specification (wildcard bindings):

enum Foo {
  case Bar(x: Int)
  case Baz(x: Int)

  def test = this match {
    case Bar(_) | Baz(_) => 42
  }
}

Hence, unless we think that this is already problematic, I would not change it.

Stupid question, but since when does the compiler compute union types as LUBs?

Please note this may be indeed a very stupid question, as I have no clue here.

But I thought a lot of issues in Scala stem form the fact that it doesn’t compute union (or intersection) types when not explicitly asked for, and happily looses all structural information every time it needs to infer some types. My current understanding is: Scala is nominal, the structural types are just bolted on…

But please correct me in case I’m talking nonsense! :smile:

1 Like

The compiler computes LUBs as unions. However, when it needs to infer types, it widens so-called “soft” union types to a non-union super type. “Soft” union types are the ones that the compiler made up, as opposed to “hard” union types which were explicitly written in the source code.

It’s entirely conceivable to use a union type in this case.

7 Likes

Thanks for the clarification!

What I meant was:

The current description does work, but can generate surprising bindings.
For example, I would expect that if id: T appears somewhere, then id has type T in that scope, which is not the case with the current spec:

Of course it’s fine to decide one way or another, but it’s important to have thought about it

Note that in that case replacing by Baz(_) | Bar(_) leaves the code equivalent (as long as there are no side-effects)

I don’t think the order mattering is an issue, but it’s good to keep in mind it is somewhat of a change

Ah, thank you for clarifying what you meant. I agree that if you read it as a type ascription for the scope of the case definition then the behaviour would be odd. I will make sure to amend the SIP proposal to discuss this reading of the case definition.

May I ask whether you would expect the following to be x: Int, or is it only in the case of an explicit type ascription that you would naturally read it as constraining the type?

enum Foo[A]:
  case Bar(a: A)
  case Baz(i: Int) extends Foo[Int]
    
  def fun = this match
    case Baz(x) | Bar(x) => // x: ???

Only in the case of an explicit type ascription, so x would have type Int | A in the above

Hello,

I have added the extra parts of the discussion. I hope I have captured your points @Sporarum - specifically under the alternatives section.

I am unfamiliar with the SIP process; would this be in a state to be submitted, or is there more legwork to be done?

The details are here:
https://docs.scala-lang.org/sips/process-specification.html

From my point of view, I would say that community support as been reached, and that it is time to open a PR to the SIP repository !

(But I’m not a SIP committee member)

1 Like