Why B >: A in Option.orElse?

I’m new to Scala.

In Scala 3, shouldn’t the signature be

def orElse[B](alternative: => Option[B]): Option[A] | Option[B]

instead of

def orElse[B >: A](alternative: => Option[B]): Option[B]

?

Option is from Scala 2, where there weren’t type unions.

But even if there were, you’d probably want Option[A | B], because you’re sure you have an Option. You don’t want to wonder, “Hm, do I have an option? Or an option?”.

But…if your other option is actually of type C, then if B is A | C it is >: A!

So Scala 3 can already infer Option[A | B] in those cases where it decides a type union is a better idea than a supertype.

And it does:

scala> Option(3).orElse(Option("eel"))
val res0: Option[Int | String] = Some(3)
4 Likes

That’s not the reason, though:

  1. Since Option is covariant, then A | B >: A implies Option[A | B] >: Option[A].
  2. Since Option only wraps a single value, Option[A | B] and Option[A] | Option[B] are equivalent.

Neither hold for mutable lists, for example.

The second property is not expressible at the type system level, AFAICT, so the compiler can’t simplify type expressions such as Option[A | B] & !Option[B], which come up during narrowing. The “split form” is superior, in this regard, but I guess it doesn’t matter too much in practice.

Yeah, that was an oversight of mine. I’m glad to see Scala 3 does the right thing!

I guess it’s because Scala 3 still uses Scala 2.13 standard library, and the standard library has been frozen for some years now (before Scala 3 even came out). There are talks of un-freezing it this year or next year: Scala Center Roadmap for 2024 | The Scala Programming Language

Yes, that is correct. But, not sure what is the point you are trying to make.

Conceptually, probably yes (I am not an expert in type theory and sometimes what seems obvious has deep small details).
However, in practice, they are different because of the way the language specs define the use of union types.

If you have the type Option[A] | Option[B] the usage would need to be:

data match
  case optA: Option[A] =>
    optA match
      case Some(a) => ???
      case None => ???
  case optB: Option[B] =>
    optB match
      case Some(b) => ???
      case None => ???

Which is uglier and more verbose than if the type is Option[A | B]:

data match
  case Some(a: A) => ???
  case Some(b: B) => ???
  case None => ???

But, more importantly, the first version is broken due to type erasure.

I was elaborating on why the “divided form” is better than the “fused one”, and why we can still use the fused one with little loss in this particular case.

You’re forgetting that tagged unions are still unions:

enum MyOpt1[+A]:
  case Some(a: A)
  case None

enum MyOpt2[+A]:
  case Some(a: A)
  case None

def f[A, B](x: MyOpt1[A] | MyOpt2[B]) = x match
  case MyOpt1.Some(a) => 1
  case MyOpt2.Some(b) => 2
  case _ => 0

assert(f(MyOpt1.Some("whatever")) == 1)
assert(f(MyOpt2.Some("whatever")) == 2)
assert(f(MyOpt1.None) == 0)

def g(x: MyOpt1[Int] | MyOpt1[String]): String = x match
  case MyOpt1.Some(_: Int) => "int"
  case MyOpt1.Some(_: String) => "str"
  case MyOpt1.None => "none"

assert(g(MyOpt1.Some(1)) == "int")
assert(g(MyOpt1.Some("s")) == "str")
assert(g(MyOpt1.None) == "none")

I guess the answer is because they are equivalent:
(Rewritten to use Option[A | B2] as it makes the algebra simpler, and you claim they are equivalent)

def orElse1[B1](alternative: => Option[B1]): Option[A | B1]
def orElse2[B2 >: A](alternative: => Option[B2]): Option[B2]

// (A | B1) >: A, Option[B1] <: Option[A | B1]
orElse2[A | B1](alternative: Option[B1])
// has type Option[A | B1]

// B2 >: A  ==>  (A | B2) =:= B2
orElse1[B2](alternative: Option[B2])
// has type Option[A | B2] which is equivalent to Option[B2] 

Furthermore only the second version was expressible in Scala 2, it makes therefore sense that it is in the standard library.
I also find it clearer conceptually, but that might be a habit thing.

1 Like

Yes, you formalized what @Ichoran said.

One difference between the two forms is that the type of alternative is narrower in the union version so, in general, we have better types inside the method implementation (not that it matters in this simple case).

BTW, it’s enough to add an argument to break the equivalence:

def f1[B1](a: => B1, f: B1 => B1): Int | B1 = ???


def f2[B2 >: Int](a: => B2, f: B2 => B2): B2 = ???

f1("a", (x: String) => x)
f2("a", (x: String) => x)            // type error

It’s not really about the number of arguments, rather about their variance, in the original example, all occurences of Bs are in covariant spots, this is no longer the case in your examples.

This can be fixed by adding a contra-variant type parameter:

def f3[B31 >: A, B32 <: A](a: => B31, f: B31 => B32): B32 = ???

Or of course by making B invariant, but it is then just an alias of A:

def f4[B4 >: A <: A](a: => B4, f: B4 => B4): B4 = ???
// equivalent to
def f4(a: => A, f: A => A): A = ???

Aren’t you forgetting something?

def f1[B1](a: => B1, f: B1 => B1): Int | B1 =
  7

I should have been more clear, I was showing how to make f2 not throw a type error

I thought this would bring back equivalence but I was incorrect, this is how to make it work:

Instead do

f2("a", (x: String | Int) => x)
// expands to
f2[String | Int]("a" /* widened to String | Int */, (x: String | Int) => x)

You’ll note this was present in my original reply as well:

I should also clarify that when I said equivalent, I mean that neither can do something the other can’t, this is not to say they are both equally practical ^^’

Sorry for nitpicking, but you’re supposed to make the type error go away by changing the definition of f2, not by modifying my counterexample. The fact that f1 and f2 behave differently while being passed the same exact args was meant to prove that they’re not equivalent.

My claim was/is that there’s no way, at least in Scala, to express

def f1[B1](a: => B1, f: B1 => B1): Int | B1 = ???

without using unions.

That I don’t dispute !
I think we have different concepts of equivalence, hence the confusion

I meant by equivalence that two things are capable of the same thing, but might require some additional work

For example the following are equivalent in this regard:

def foo1[A, B](x: A): B => A = (y: B) => x
def foo2[A, B](x: B): A => B = (y: A) => x

// Any call
foo1[X, Y](bar)
// can be replaced by
foo2[Y, X](bar)

But they are of course not equivalent in your definition
(And both definitions are useful)

Therefore, I agree with the following:

I hope this clarification was useful ^^

Why not? Whether the type vars are inferred or explicitly given, what matters is whether there is or there isn’t an assignment that makes things equal.

For all bar, A, B, there exist X, Y such that foo2[X, Y](bar) is equivalent to foo1[A, B](bar), and vice versa, so foo1 and foo2 are equivalent.

Now, back to our case:

def f1[B1](a: => B1, f: B1 => B1): Int | B1 = ???

def f2[B2 >: Int](a: => B2, f: B2 => B2): B2 = ???

f1("a", (x: String) => x)
f2("a", (x: String) => x)            // type error

Is there any valid assignment to B2 that makes f2[B2]("a", (x: String) => x) behave like f1("a", (x: String) => x)? No, there isn’t, so f1 and f2 are not equivalent, and replacing (x: String) => x with (x: String | Int) => x won’t change that.