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.
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.
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.
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
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”