Proposal to remove general type projection from the language

I’d have to look deeper. But before doing that it would be good to know the answer to my question: Is erased fundamental or just an optimization? If it is just an optimization, we can probably make it work. If it is fundamental, it looks like it will exploit a soundness hole.

Yes exactly. It’s an optimisation, but I think a very important one. You have to imagine that almost every object of every type in my system has S type parameter, with type projections / Scala 2 this incurs zero overhead, but suddenly I would have to store an instance of S in every object, and pass it around. This should really be erased at compile time.

Also - even if I have erased, why would that introduce unsoundness? The compiler would still require that a value of that type is present when constructing the instances, even if it erases the actual code at a later stage. No?

That’s good to know. I’ll look into it.

1 Like

From my understanding of the problem erased shouldn’t make any difference. Requiring an actual value to avoid the unsoundness doesn’t work anyway because at the very least you have unsound initialization to deal with. And if you solve it by disallowing projections from arbitrary types, you can safely allow other ways of not having to construct a value (like erased or lazy vals).

Initialization is indeed a problem. That’s one of the motivations for @liufengyun’s work on initialization checking. See https://github.com/lampepfl/dotty/issues/5854

Say I have:

trait Foo {
  type Baz
  val baz : Baz
}

Can I write the following?

class FooUser[F <: Foo](implicit f2b : F => F#Baz)

No, since F is a type parameter.

So I understand such codes need to be modified like

class FooUser[F <: Foo](foo : F)(implicit f2b : F => foo.Baz)

or

class FooUser[F <: Foo] {
  def someFuncThatUsesFoo(foo : F)(implicit f2b : F => foo.Baz) : Unit = {}
}

Another option might be,

class FooUser(implicit erased foo: Foo, f2b: foo.type => foo.Baz)

“erased is the new hash”

:wink:

1 Like

Would be following structure allowed?

  trait StateBase

  trait Entity {
    type Id
    type State <: StateBase
  }

  trait DomainRepository[F[_]] {
    def get[A <: Entity](id: A#Id, states: Seq[A#State])(implicit tag: ClassTag[A]): F[A]
  }

  sealed trait CustomerState extends StateBase
  case object Active extends CustomerState
  case object Inactive extends CustomerState

  class Customer extends Entity {
    type Id = UUID
    type State = CustomerState
  }

  val repo: DomainRepository[Id] = ???

  repo.get[Customer](UUID.randomUUID(), Seq(Active))

This is simplified construction that we’re using for domain models manipulation, which we highly depends in our projects.
Unfortunately I don’t know any other way to express dependent types on call site without generic parametrization for each type which extremely verbose and boilerplate.

No, it has the same issue with A# being a disallowed selector even with a given upper boundary: https://scastie.scala-lang.org/QGRwwwCaQiainhWbetf9UA

Did you try anything along these lines? If you did, where did it go wrong?

General thoughts about this SIP:
For me, the unsoundness is not a reason to remove this language feature, and even dotty has a Scala2 mode to support it. Many code bases already use it and to force users apply significant rewrites just for soundness’ sake isn’t just.
The unsoundness is a good reason to mandate a flag “import language.thisIsVeryBad”.
To discourage the community from using this feature we can, for example, remove libraries from the community-build if they use this feature.

1 Like

No it wouldn’t: A in A#Id is abstract.

Would something like this work for you?

Thanks Miles; nesting Txn inside Sys won’t work for me. But as posted earlier, I could make it work with dependent types; only current Dotty version won’t allow me to use paths of erased values, although Stefan Zeiger’s comment seems to suggest that it would be safe to allow that. If I could have an erased value, I could probably migrate the project. Except of course that it won’t work any longer in Scala 2, unless erased is backported.

Could you say a bit about why?

That’s because the entire idea of having abstracted Txn is that it will be the base type for other more specific transactions, so there are various systems that share the base Txn trait, so it can’t be inside one particular system.

Hmmm, you can always cheat?

package private_api

trait Txn[S <: Sys] {
  val system: S
}

trait Sys {
  type Tx <: Txn[this.type]
}

trait A[S <: Sys] {
  private[private_api] val s: S = null.asInstanceOf[S]

  def bla()(implicit tx: s.Tx): Unit = println("bla!")
}

object Main {
  def main(args: Array[String]): Unit = {
    val x = new A[Sys] {}
    println("That was easy.")
  }
}

(this is why I think erased values should anyway allow path selections)

Couldn’t we get soundness by not prohibiting projections but instead taking a stricter interpretation of what they mean? In cases like the one on the ticket, you would get a type that either couldn’t be assigned from Int or to String or both. Maybe Nothing, or maybe there should be such a thing as an empty union (no type). Thus you would be forced to use .asInstanceOf, which is always the way to say “I know the type checker says this is unsound but I want to do it anyway.”