Pre-SIP: Introducing elastic types, classes, functions, and products

Purpose

This proposal introduces the concept of “elastic” types, classes, function, and products to solve the compile-time and runtime problems when we want to add fields and default values in a backward compatible way.

Typing

An elastic type Foo of arity n has the expandable property where Foo[A1, A2, ..., An] =:= Foo[A1, A2, ... An, An+1, An+2, ..., An+m] if and only if An+i =:= Nothing, for all i=1...m.

Syntax and Compile-time Rules

Introduce the (soft) keyword modifier elastic; applicable for classes, functions, case classes.

elastic def foo(...): ... =
elastic class Foo(...)
elastic case class FooCC(...)

The elastic keyword modifies the function type for definitions, and both the constructor type and class type for classes and case classes:

  • elastic class ElasticFunctionN is introduced and is a subtype of FunctionN for any arity. Examples:
ElasticFunctionN[T1, T2] <:< Function2[T1, T2]
ElasticFunctionN[T1, T2, Nothing] <:< Function2[T1, T2]
ElasticFunctionN[T1, T2, T3] <:< Function3[T1, T2, T3]
  • elastic abstract class ElasticProduct is introduced and is a subtype of Product.

Runtime Elastic Dispatcher

To achieve runtime backward compatibility when adding fields to an elastic function/class, we change the runtime encoding to go through a dispatcher array for default values.

Example
The following def:

elastic def foo[A, B, C](arg1: A, arg2: B, arg3: Int = 1, arg4: Option[C] = None): Unit = {}

Will be encoded like

elastic def foo[A, B, C](
  arg1: A = foo$default[A](0), 
  arg2: B = foo$default[B](1), 
  arg3: Int = foo$default[Int](2), 
  arg4: Option[C] = foo$default[C](3)
): Unit = {}
//default arguments encoding
def foo$default[T](argIdx: Int): T =
  if (argIdx >= foo$default$dispatcher.size) throw new UnreachableArgumentException("foo", argIdx)
  else foo$default$dispatcher(argIdx)().asInstanceOf[T]
val foo$default$dispatcher: Array[() => Any] = Array(
  () => throw new MissingArgumentException("foo", "arg1"), 
  () => throw new MissingArgumentException("foo", "arg2"),
  () => 1, 
  () => None
)

Discussion

(Maybe it’s a totally idiotic idea, so feel free to chime in and let me know)

  • What would be the overloading rules? Can we allow more than one elastic signature with the same name?
  • What would an elastic case class encoding look like? Should field access go through a dispatcher as well?
  • Should Named Tuples extend ElasticProduct? Would that enable “intuitive” subtyping rules so tuples are subtypes of unnamed tuples?
  • How do we expand this to multi-block functions?
  • The proposed subtyping rule for elastic types is =:= and not <:<. This actually enables forward binary and source compatibility, with runtime exceptions that can be handled. Should this be possible?

Those rules make Foo poly-kinded, while at the same time actually accepting type argument lists. We do not have any precedent for that in the type system. It would probably destroy a lot of invariants of the type system spec and compiler implementation. For starters, it would not be possible to talk about “the inferred type parameter clause of T.

Is this relationship essential to the proposal? I understand the problem that elastic def addresses, but I’m not sure what value elastic classes would bring.

Isn’t this required so the constructor signature will be backwards compatible when adding fields to a class?

Not unless you actually change the type parameter list of the class. Most of the classes we want to evolve that way are monomorphic and stay that way, aren’t they?