Allow traits to be transparently or "invisibly" mixed in

Hi,

I wonder if this would be a worthwhile construct: A modifier to make a type (class, trait) “transparent”, very much like a non-opaque type alias is transparent. The motivation is to allow easier mixin of implementing traits without them showing up in the formal type of the class that uses the mixin. Kind of the opposite of self-types (trait Impl { self: Public => ... }). Something like (a more or less random example):

trait Ex[A] {
  def name: String
}

transparent trait ExImpl[A] {
  self: Ex[A] with Product =>

  def name: String = productPrefix
}

case class Example() extends Ex[Int] with ExImpl[Int] 

A syntactic variant would be

trait Ex[A] {
  def name: String
}

trait ExImpl[A] {
  self: Ex[A] with Product =>

  def name: String = productPrefix
}

case class Example() extends Ex[Int] {
  with ExImpl[Int] =>
}

Thus when you look at the type of Example, it would not bear any witness of ExImpl.

My current work around for avoiding the pollution with implementation details is this:

object Example {
  def apply(): Example = Impl()

  private final case class Impl() extends Example with ExImpl[Int]
}
trait Example extends Ex[Int]

So you see the amount of ceremony needed here.

WDYT?

I would rather use a qualifier at usage site than at definition site. And instead of transparent I would just use private, like in C++:

trait Ex[A] {
  def name: String
}

trait ExImpl[A] extends Ex[A] {
  def name: String = ...
}

case class Example() extends Ex[Int] with private ExImpl[Int]

We could also support protected inheritance.

The current state (always public inheritance) is sometimes problematic (see for instance how the supertypes of List mix implementation details such as StrictOptimizedSeqOps or IterableFactoryDefaults with the rest of the hierarchy).

2 Likes

Oh neat, didn’t know that (know very little C++). Post explaining this; although here it looks like it will make public members private, so I think the use case and semantics are different? Nevertheless, the syntax makes sense.

Another workaround (used in scala-reflect) is to expose a typealias with a subset of traits you want to be public:

trait XApi
trait XImpl extends XApi

type X <: XApi
object X {
  def apply(): X = new XImpl
}

Yes, but that is what I wrote as “my current work around”. It works, but it’s not nice because it leads to a lot of extra code in larger libraries.

Also given that we may have the new keyword deemphasised, there is even less incentive to create companion objects “for the sake of it”.

See companion requirements make abstract definition impossible for another reason not to take that route.

2 Likes

I think that’s worth considering. What rules do you propose for private/protected inheritance?

How would you prevent people from discovering the hidden inheritance via pattern matching? As in Example() match { case _: ExImpl[_] => ... }. If you don’t prevent that, the inheritance isn’t really “private” IMHO, as it’s publicly observable.

I think that if you write with private ExImpl, the compiler has to splice in the body of ExImpl without generating an “interface” (or whatever it would normally do) for ExImpl.

(I don’t know if anything related to the way extends Any works would come handy?)

The members of a privately inherited class may not be selected on subclasses (and wouldn’t show up in the documentation).

The members of a protectedly inherited class may only be selected within subclass implementations.

This should not compile for the same reason that 42 match { case "foo" => () } does not compile.

In that case, why would one want to inherit the class at all? Why not use composition instead?

I can see two reasons:

  1. the privately inherited class brings operations that are useful to implement the subclass, but these operations should remain hidden implementation details (example: https://www.scala-lang.org/api/2.13.0-RC1/scala/collection/StrictOptimizedSeqOps.html)
  2. to not have to manually write the forwarders:
class X extends MyInterface with private MyInterfaceImpl

vs

class X extends MyInterface {
  private impl = new MyInterfaceImpl
  def member1 = impl.member1
  ...
}

What about subtyping? if A extends private B, is A still a subtype of B?

No, A is not a subtype of B in that case.

With class A extends protected B, A is a subtype of B only within the A class.

I think Julien has introduced an idea in this thread quite different from the OP.

I think that if you mixin using A extends private Impl, this should allow Impl to define any API that A formally declares without becoming part of the type signature of A.

So if class Foo extends Ordering[Int], and trait FooImpl { def compare(a: Int, b: Int): Int = if (a < b) -1 else if (a > b) +1 else 0 }, then obviously compare does not become private. I think that would be an extremely rarely needed “transformation”, that is also quite difficult to understand. If you just want to provide “operations {that} should remain hidden implementation details”, then you can already use protected methods, that’s exactly what they are for.

So sorry, but I think the thread no longer follows the original idea.

FAt the risk of sounding dim, are you not talking about splitting the notions of inheritance and subtyping?

This seems like a pretty neat idea to me, with, say, subtypes to mean subtyping, inherits to mean inheritance and extends to mean both.

Do you realize that both compile when the scrutinee is upcasted to Any?
It’s not a problem when matching "foo" (it just won’t match), but it would expose a private inheritance relation when matching ExImpl[_].

Yes. I thought that would be fine, though. Working with Any is not safe anyways.

It doesn’t have to be Any. It can be any trait up the inheritance DAG from the class with the private inheritance in question!

(Also, IMHO the goal of a type system is to be safe when you use its standard features; you can’t just say a foundational part of it is “unsafe anyways”, and hand-wave problems that way.)

3 Likes