Named tuples are not considered to be disjoint from any type

Due to the implementation of NamedTuple as opaque type with an upper-bound of Any, they are not considered to be disjoint from any type. This significantly limits their usage in match types:
scastie

sealed trait Foo

type Bar[T] = T match
  case Foo => (a: Int, b: Int)
  case _ => NamedTuple.From[T]

val x: Bar[Foo] = ???
val xCheck = x.a //ok

val y: Bar[(1, 2)] = ???
val yCheck = y._1 //ok

val z: Bar[(a: Int, b: Int)] = ???
val zCheck = z.a //error value a is not a member of Bar[(a : Int, b : Int)]

I think we should special-case NamedTuple to be considered as Tuple in match types, so that its usability won’t be hindered compared to Tuple.

cc @sjrd

Related ticket

Named tuples are not implemented as opaque type aliases. They are specced as opaque type aliases. Under that constraint, they must behave the way they do with match types.

If you want something else, you must argue that named tuples should be specced as (magic) classes, not opaque type aliases.

Why can’t the match type spec special-case an opaque type of NamedTuple?

Because match types are hard and subtle enough as it is. It’s not the match types’ responsibility to fix up things that could have been specced differently elsewhere.

how would you change that spec - could the library stay the same?

When a library relies on the implementation of named tuples (and most will probably will) you will need to break source backwards compatibility.

No it would totally and utterly break compatibility, in all categories except perhaps binary.

inline match however is able to make the distinction - but you would have to resort to context parameters and type members

sealed trait Foo

trait Bar0[+T] { type Res; def res: Res }

object Bar0:
  type Aux[T, R] = Bar0[T] { type Res = R }
  transparent inline given mkBar[T]: Bar0[T] =
    inline compiletime.erasedValue[T] match
      case _: Foo =>
        new Bar0[Foo] { type Res = (a: Int, b: Int); def res: Res = ??? }
          .asInstanceOf[Bar0.Aux[T, (a: Int, b: Int)]]
      case _ =>
        new Bar0[T] { type Res = NamedTuple.From[T]; def res: Res = ??? }
          .asInstanceOf[Bar0.Aux[T, NamedTuple.From[T]]]

val x = summon[Bar0[Foo]]
val xCheck = x.res.a

val y = summon[Bar0[(1, 2)]]
val yCheck = y.res._1

val z = summon[Bar0[(a: Int, b: Int)]]
val zCheck = z.res.a

if you want a DSL:

type Bar = TypeFun[(Foo ->> (a: Int, b: Int), TypeFun.Wild[[T] =>> NamedTuple.From[T]])]

val x0 = summon[Bar[Foo]]
val x0Check: x0.Out = (a = 1, b = 2)

val y0 = summon[Bar[(1, 2)]]
val y0Check: y0.Out = (1, 2)

val z0 = summon[Bar[(a: Int, b: Int)]]
val z0Check: z0.Out = (a = 1, b = 2)

based on:

type TypeFun[Cases <: Tuple] = [T] =>> TypeFun.Resolved[T, Cases]
type ->>[Case, R] = (Case, R)

object TypeFun:
  sealed trait Wild[F[_]]
  final class Resolved[T, Cases <: Tuple]:
    type Out

  object Resolved:
    type Aux[T, Cases <: Tuple, R] = Resolved[T, Cases] { type Out = R }
    transparent inline given resolved[T, Cases <: Tuple]: Resolved[T, Cases] =
      resolveCases[T, Cases, Cases]

    transparent inline def resolveCases[T, Cases <: Tuple, C <: Tuple]: Resolved[T, Cases] =
      inline compiletime.erasedValue[C] match
        case _: ((T, r) *: _)  => Resolved[T, Cases].asInstanceOf[Resolved.Aux[T, Cases, r]]
        case _: (Wild[f] *: _) => Resolved[T, Cases].asInstanceOf[Resolved.Aux[T, Cases, f[T]]]
        case _: (_ *: rest)    => resolveCases[T, Cases, rest]

maybe someone could make it work with more kinds of “patterns”