8

I have been reading about Dotty, since it looks like it is about to become scala 3, and noticed that type projections are deemed "unsound" and removed from the language ...

This seems like a bummer, as I have seen several use cases where they were really useful. For example:

trait Contents
class Foo extends Contents
class Bar extends Contents

trait Container[T <: Contents] { type ContentType = T }
class FooContainer extends Container[Foo]
class BarContainer extends Container[Bar]

trait Manager[T <: Container[_]] { 
  type ContainerType = T 
  type ContentType = T#ContentType
  def getContents: ContentType 
  def createContainer(contents: ContentType): ContainerType
}

How would one do something like this in Dotty? Add a second type parameter to Manager? But, aside from the fact that it makes it really tedious to create and manipulate instances of the Manager, it also doesn't quite work, as there is no way to enforce the relationship between the two types (Manager[FooContainer, Bar] should not be legal).

Then, there are other uses, like type lambdas, and partially applied types, that are useful for creating biased functors etc ... Or do these (partially applied types) become "first class citizens" in Dotty?

EDIT

To answer the question in the comments, here is a somewhat contrived example of his this may be used. Let's suppose, my Managers are actually Akka Actors:

abstract class BaseManager[T <: Container[_]](
  val storage: ContentStorage[T#ContentType]
) extends Actor with Manager[T] {
    def withContents(container: T, content: ContentType): ContainerType
    def withoutContents: T

    var container: T = withoutContents

    def receive: Receive {
       case ContentsChanged => 
          container = withContents(container, storage.get)
       case ContainerRequester => 
           sender ! container
       // ... other common actions 
    }
}

class FooManager(storage: FooStorage) extends BaseManager[FooContainer](storage) {
   def withContents(container: FooContainer, content: Foo) = 
       container.copy(Some(content))
   def withoutContent = FooContainer(None)

   override def receive: Receive = super.receive orElse { 
    // some additional actions, specific to Foo
   }
}

case class FooContainer(content: Option[Foo]) extends Container[Foo]{
  // some extremely expensive calculations that happen when 
  // content is assigned, so that we can cache the result in container
}
Dmytro Mitin
  • 48,194
  • 3
  • 28
  • 66
Dima
  • 39,570
  • 6
  • 44
  • 70
  • Can you give an example of how you would use Manager? That would make it easier to come up with an alternative design. – Guillaume Martres Apr 27 '18 at 12:04
  • @GuillaumeMartres I added an example to the question – Dima Apr 28 '18 at 22:49
  • Something is missing in your example, the BaseManager constructor takes a parameter `storage` but `FooManager` extends `BaseManager` without arguments. – Guillaume Martres Apr 29 '18 at 23:32
  • @GuillaumeMartres sorry, fixed it ... – Dima Apr 30 '18 at 15:43
  • @Dima The code in `Foo-Bar` MCVE doesn't compile. `Container` has generic and in `Manager[T <: Container]` it's used without generic. `ContainerType` is defined twice in `Manager`. – Dmytro Mitin May 30 '19 at 12:06
  • @DmytroMitin ok, I fixed that. For the record, I intended it to just be an illustration of the approach to a design, not a working example for actual implementation, so didn't pay much attention to proper syntax. – Dima May 30 '19 at 14:50
  • @Dima Thank you but now it doesn't compile: `Error: covariant type ContentType occurs in contravariant position in type Manager.this.ContentType of value content`. Well, I just don't want to guess what you mean. – Dmytro Mitin May 30 '19 at 15:03
  • I am not sure ... It compiles fine for me in repl. Also, I am not sure getting it compiled will necessarily help you understand what I mean. If the intent is unclear, perhaps, just ask? – Dima May 30 '19 at 15:29
  • @Dima https://scastie.scala-lang.org/qcVdzhLoTke5RHTL6oMtJg – Dmytro Mitin May 30 '19 at 17:12
  • @Dima Maybe `def createContainer[U >: ContentType](contents: U): ContainerType` should be instead of `def createContainer(contents: ContentType): ContainerType`. Anyway, does my [answer](https://stackoverflow.com/a/56382879/5249621) work for you? – Dmytro Mitin Jun 04 '19 at 11:47

1 Answers1

5

In Scala 2.12 type projections sometimes can be replaced with type class + path-dependent types

trait ContentType[T <: Container[_]] {
  type Out
}
object ContentType {
  type Aux[T <: Container[_], Out0] = ContentType[T] { type Out = Out0 }
  def instance[T <: Container[_], Out0]: Aux[T, Out0] = new ContentType[T] { type Out = Out0 }

  implicit def mk[T <: Contents]: Aux[Container[T], T] = instance
}

abstract class Manager[T <: Container[_]](implicit val contentType: ContentType[T]) {
  type ContainerType = T
  def getContents: contentType.Out
  def createContainer(contents: contentType.Out): ContainerType
}

Checked in Dotty 0.16.0-bin-20190529-3361d44-NIGHTLY (in 0.16.0-RC3 delegate should be instead of implied)

trait Contents
class Foo extends Contents
class Bar extends Contents

trait Container[T <: Contents] { type ContentType = T }
class FooContainer extends Container[Foo]
class BarContainer extends Container[Bar]

trait ContentType[T <: Container[_]] {
  type Out
}
object ContentType {
  implied [T <: Contents] for ContentType[Container[T]] {
    type Out = T
  }
}

trait Manager[T <: Container[_]] given (val contentType: ContentType[T]) {
  type ContainerType = T
  type ContentType = contentType.Out
  def getContents: ContentType
  def createContainer(contents: ContentType): ContainerType
}

One more option is to use match types

trait Contents
class Foo extends Contents
class Bar extends Contents

trait Container[T <: Contents] { type ContentType = T }
class FooContainer extends Container[Foo]
class BarContainer extends Container[Bar]

type ContentType[T <: Container[_]] = T match {
  case Container[t] => t
}

trait Manager[T <: Container[_]] {
  type ContainerType = T
  def getContents: ContentType[T]
  def createContainer(contents: ContentType[T]): ContainerType
}
Dmytro Mitin
  • 48,194
  • 3
  • 28
  • 66
  • In Dotty 0.17 `given [T <: Contents] as ContentType[Container[T]]...` should be instead of `implied [T <: Contents] for ContentType[Container[T]]...` – Dmytro Mitin Sep 07 '19 at 15:59
  • In Dotty 0.20.0-RC1 `given [T <: Contents]: ContentType[Container[T]] ...` should be instead of `given [T <: Contents] as ContentType[Container[T]]...` and `trait Manager[T <: Container[_]] (given val contentType: ContentType[T]) ...` should be instead of `trait Manager[T <: Container[_]] given (val contentType: ContentType[T]) ...` – Dmytro Mitin Nov 20 '19 at 12:58
  • https://users.scala-lang.org/t/converting-code-using-simple-type-projections-to-dotty/6516 – Dmytro Mitin Aug 21 '20 at 20:55
  • Using path dependent types requires a value of the type in question, while using match types seems closed/in-extensible. Type projections, while unsound, only needs a reference to the type (not a value of that type) and can be used openly, without having to define all known matches ahead of time. I'm guessing there is no way to get both of those benefits in Scala 3 at the moment? – anqit Jan 24 '23 at 23:26
  • @anqit Right. Moreover, match types work on value level currently not so well as type projections did. – Dmytro Mitin Jan 25 '23 at 02:18