Lack of a conformity check of lower to upper bound of a type parameter

I have just noticed that this compiles both in Scala 2 and Scala 3:

class Ops[+A, +O[_]] {
  def orElse[X, B[x] >: O[x] <: Opt[x]](alt: => B[X]) :B[X] = alt
}
class Opt[+A] extends Ops[A, Opt]

In the example, O[x] does not conform (necessarily) to Opt[x]. OTH, a declaration of an abstract type with a lower bound not statically conforming to its upper bound is an error.

Is this on purpose and may we rely it will remain so in the forseable future?

Hello @max.

What I see here is the declaration of a formal type parameter B on the method orElse. That is what the constraints apply to - O and Opt don’t have any constraints in themselves.

Whether you can supply a B satisfying those constraints is indeed limited depending on what O is wrt Opt , so yes, possibly there are no choices at all leading to a clean compile. However, that would be checked at each call site for orElse on a case-by-case basis.

Did you try actually using this construct and what happened at compile / run time? Where you able to compile a call-site that was unsound from the point of view of the substituted types?

Yes, I tried it both in Scala 2 and Scala 3 and it works at expected: the code compiles, but, naturally, extending Ops[A, O] for O not conforming to Opt makes orElse method impossible to call. So, it’s not a bug or anything, I was just surprised because of inconsistency with an analogous problem I ran into with type aliases:

trait Test:
  type A
  type B
  type C >: A <: B //compile error

I am actually very happy that it works for type parameters, as I need it for my use case. However, very many hacks/undocumented features I have used in the past have been patched, so I wanted to reach out and ask if this is a conscious choice to gauge the forward compatibility of such code with future Scala versions.

Also, if someone knows why different choices have been made in these two use cases, it would be enlightning. I do not however feel entitled to explanations from the language designers of every minor feature :slight_smile:

It’s not that abstruse - simply a case of degrees of freedom in where parametric types are frozen by substitution - you might have a class / trait with type parameters - they are frozen across the type usages as a whole, whereas a method with type parameters, like a function with type parameters has them frozen at call-site usage, even for partial application (I think).

Somebody can chime in about the difference between parametric function definitions and truly polymorphic functions, but I’ll gloss over that for now.

The second example you gave has no type parameters at all, and it compiles for me, at least under Scala 3.3.7 LTS.

Aaaah, that may be Scala 3 feature then. In Scala 2 it doesn’t compile.

1 Like

(That does still leave me curious why Scala 2 had the restriction, though.)