In Scala 3, how to replace General Type Projection that has been dropped?
Asked Answered
S

1

3

This code does not compile in Scala 3 since type projection on an abstract type is now invalid:

trait Entity:
  type Key

type Dictionary[T <: Entity] = Map[T#Key, T]

The compiler complains that T is abstract and type projection is therefore no more available:

T is not a legal path
since it is not a concrete type

How could you define the above Dictionary type in Scala 3?

Super answered 24/1, 2023 at 20:47 Comment(16)
AFAIK, that was essentially equivalent to Map[Any, T] in Scala 2; which is why it was removed. It wasn't doing anything useful at all.Wenonawenonah
Do you mean there was no actual enforcement of the type dependency between Entity type and its Key type? How come there is no way to express such a simple constraint?Castara
What kind of constraint can it even enforce? Key can be anything.Wenonawenonah
In Dictionary, Key depends on actual T type. But I get your point: there is no guarantee that Key is actually defined unless T is a concrete type.Castara
Even if T is a concrete type, Key may be still undefined. - Or, it may be defined as Any. - Or may be defined in a way such that the actual type can change for each instance.Wenonawenonah
I meant "there is no guarantee that Key is actually defined unless T is a concrete class".Castara
Again, it doesn't matter if T is a concrete class: The Key type may not be defined - It may be defined as Any - Or, it may be defined in a way that the actual type will depend on each instance.Wenonawenonah
I don't understand how a concrete class A that would implement Entity trait and therefore define Key type would not put any constraint on Dictionary[A] (type Key = Any would be, of course, a very loose constraint!).Castara
You don't have to define a type member to make a concrete class, not even to instantiate it :) They can be abstract... because they are always abstract. When you do type T = Foo that is actually just sugar syntax for type T >: Foo <: FooWenonawenonah
And when you just do type T it actually means type T >: Nothing <: Any so that is why that projection is mostly equivalent to just AnyWenonawenonah
@LuisMiguelMejíaSuárez "AFAIK, that was essentially equivalent to Map[Any, T] in Scala 2" Although Map[T#Key, T] can be similar to Map[Any, T] at runtime, at compile time they are quite different. You can always put string -> t into Map[Any, T] but apparently you can not always put it into Map[T#Key, T].Nealy
@LuisMiguelMejíaSuárez "which is why it was removed" General type projections were removed (so far) for different reasons github.com/lampepfl/dotty/issues/1050 contributors.scala-lang.org/t/… lptk.github.io/programming/2019/09/13/type-projection.html github.com/lampepfl/dotty-feature-requests/issues/14Nealy
@LuisMiguelMejíaSuárez "It wasn't doing anything useful at all" On contrary, I guess they were quite useful apocalisp.wordpress.com/2010/06/08/… Type projections and type classes were two ways to perform type-level calculations in Scala 2 (like match types and type classes are in Scala 3).Nealy
@LuisMiguelMejíaSuárez " They can be abstract... because they are always abstract." The thing is that figuring out whether a type is "not abstract" can be not easy #54138022 Although there is no standard way to check that L and U are the same in implementations of type T >: L <: U, this can be checked with a macro (at least for simple L and U like in type T >: Foo <: Foo).Nealy
@LuisMiguelMejíaSuárez "When you do type T = Foo that is actually just sugar syntax for type T >: Foo <: Foo" This is a syntax sugar in DOT, but not literally a syntax sugar in implementations for performance reasons (although intended to be the same) scastie.scala-lang.org/Hl3ykwYNRTujc2BpESyy8A contributors.scala-lang.org/t/whats-in-a-type-alias/4001Nealy
@DmytroMitin right I should not have used "sugar syntax" there, rather "equivalent". - Also, I didn't mean type projections were always useless, only in this case (as for my limited understanding of the matter).Wenonawenonah
N
5
trait Entity:
  type Key

object Entity:
  type Aux[K] = Entity { type Key = K }

// match type
type EntityKey[T <: Entity] = T match
  case Entity.Aux[k] => k // k being lower-case is significant

type Dictionary[T <: Entity] = Map[EntityKey[T], T]

I had to introduce Aux-type because match types seem not to work with refined types

type EntityKey[T <: Entity] = T match
  case Entity { type Key = k } => k // Not found: type k

Beware that on value level match types can work not so well as type projections: Scala 3: typed tuple zipping

  • Besides match types, also type classes can be an alternative to general type projections
trait Entity:
  type Key

// type class
trait EntityKey[T <: Entity]:
  type Out

object EntityKey:
  type Aux[T <: Entity, Out0] = EntityKey[T] { type Out = Out0 }

  given [K]: EntityKey.Aux[Entity { type Key = K }, K] = null

// replacing the type with a trait
trait Dictionary[T <: Entity](using val entityKey: EntityKey[T]):
  type Dict = Map[entityKey.Out, T]

What does Dotty offer to replace type projections?

https://users.scala-lang.org/t/converting-code-using-simple-type-projections-to-dotty/6516

Dotty cannot infer result type of generic Scala function taking type parameter trait with abstract type

Nealy answered 25/1, 2023 at 1:56 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.