Pre-SIP: Unboxed wrapper types

@non I’m glad to see this being considered with the concerns of type-safe programmers at the forefront, rather than the concerns of stringly-typed programming.

I think an “associated function” mechanism, like the ‘newtype instance methods’ included in the current draft, is a great feature to include. But for true “zero-cost” newtyping, I think a more powerful one is called for.

For the sake of argument, let’s assume that given

sealed abstract class =:=(A, B)
final case class Refl[A]() extends (A =:= A)
sealed abstract class <:<[-A, +B]
final case class SRefl[A]() extends (A <:< A)

case Refl() reveals the A = A equality in its consequent body, and case SRefl() reveals the A <: B relation in its consequent body. (We must currently simulate this with subst.)

Now consider a Set module in the spirit of @chrisokasaki’s Typelevel post.

trait SetModule {
  type T
  type E

  def wrap(underlying: Set[E]): T def unwrap(t: T): Set[E]
  def union2(l: T, r: T): T

  def unionN(xs: Set[T]): T
  def subsets(t: T): Set[T]
  def reveal: (T =:= Set[E])
}

object SetModule {
  val Module: SetModule = ... // elided

This provides a 100% zero-cost arrangement, assuming sufficient inlining of the functions in the module. That’s because the type equality T = Set[E] is visible to both the functions defined directly within the module, and any “extension” functions you might want to add, by means of

SetModule.Module.reveal match {
  case Refl() =>
    // here T = Set[E] is visible, so I can use normal Set[E]
    // signatures with Ts, and vice versa
}

By contrast, the style of single-receiver method definition is only well-suited to defining functions like wrap, unwrap, and to a slightly lesser extent union2—the “other” argument must be unwrapped and the result rewrapped, so it is not quite as neat as the “opaque type alias”-style definition.

By contrast, consider unionN. For the module, you can simply supply any existing function that conforms to Set[Set[E]] => Set[E]. The instance-method style doesn’t have it so easy: assuming you can sensibly choose one of the Ts to act as receiver—assuming there is one to choose at all—the tail Set[T] must still be maually unwrapped, in linear time, even in the body of a T method, in order to use an existing definition of unionN.

The same goes for return values, such as the case of subsets. No linear wrapping step is needed for the module version, but one is needed for a instance-method-style approach.

And that assumes mapping is even possible. With a full type equality visible, even an invariant, unmappable F[Set[E]] simply becomes an F[T] as requested—but only as requested.

reveal extends that equality to the public module interface; for many newtype use cases, it is probably “so open the abstraction falls out”, and a weaker property would probably be desirable, like T <:< Set[E]. This too might be revealed globally as an upper bound (i.e. newts’s @translucent), or constrained to pattern-matching blocks that explicitly ask for the relationship via case SRefl(), depending on how visible you want the relationship.

newts gives you the two choices (1) totally opaque or (2) globally visible supertype; Flow opaque type aliases offer the same two choices. GADTs/subst give us more possibilities via reveal, but I don’t know what the best way to provide those options would be.

In summary, I would like to see these aspects considered, which are not well served by an instance-method-like approach.

  1. The definition of functions alongside the type definition that do not fit the “single receiver” pattern; the closed set; and
  2. Whether there should be something like reveal to define further functions not alongside the type definition, yet knowing something about the representation, and how fine-grained the choices should be; the open set.

You might say these are well-served by the already-supported “module definition style” (notwithstanding #10283 and the like), and that anyone who wants these features should use these existing—if quite obscure—Scala features, but it would be really nice to not need to reach for them often in the future.