Skip to content
Shunsuke Sogame edited this page Jul 1, 2015 · 24 revisions

design-guidelines

Description of sing design guidelines

The Beginning of Type-level Programming in Scala

Definitions

Caution: These terms are, for now, private at all.

Kind

Roughly speaking, a type of types is called a kind. In sing, a kind is a subtype of sing.Any:

    package sing
    trait Boolean extends sing.Any { ...

It represents a type in the type-level world. The reason why sing.Any is needed will be described later.

Typemethod

A typemethod is a type or a type-constructor which takes kinds. A form of abstract typemethod is something like:

    type not <: sing.Boolean // no parameters
    type equal[that <: sing.Boolean] <: sing.Boolean // one parameter

You can override a typemethod:

    override type not = ...
    override type equal[that <: sing.Boolean] = ...

override modifier is optional. Note that you cannot re-override a typemethod.

You can invoke typemethod by #:

    type foo[b <: sing.Boolean, c <: sing.Boolean] = b#equal[c]

A typemethod invocation is memoized.

Concrete Invocation

When a typemethod invocation doesn't depend on enclosing type parameters(or abstract types in official), we call it concrete.

    class X[b <: sing.Boolean] {
        type bar = `true`#not
    }

`true`#not is a concrete invocation, for it doesn't use enclosing type parameters. A concrete invocation is evaluated as soon as the Scalac reads; even if you don't invoke bar.

Termmethod

A termmethod is a polymorphic method whose type parameters are upper-bounded by kinds.

    def foo[x <: sing.Boolean](x: x) = ...

Notice the type and term have the same name x. In sing, types and terms are intended to be isomorphic.

Singmethod

A singmethod is an identifier which can be used as both typemethod and termmethod. The termmethod shall return a type which is the same as typemethod invocation:

     def foo[b <: sing.Boolean, c <: sing.Boolean](b: b, c: c): foo[b, c] = ...
    type foo[b <: sing.Boolean, c <: sing.Boolean] = ...

No Kind Generalization

Assume you are making if statement in the type-level world. First, you might describe it for the runtime world:

    def myIf[T](b: scala.Boolean, _then: => T, _else: => T): T

This is a polymorphic method(aka generics). Second, you might port it into the type-level world. (Ignore by-name parameters for now.):

    type typeIf[T, b <: sing.Boolean, _then <: T, _else <: T] <: T // ???

This is considered as generics in the type-level world.

Unfortunately, trivial unit-tests reveal this doesn't work at all. Any form of type-level generics seems not to work for now. See:

Now we have to go back to the start. Imagine you live in a world which has neither generics nor by-name parameters. That's good old Java:

    trait MyFunction0 {
        def apply: scala.Any
    }

    def myIf(b: scala.Boolean, _then: MyFunction0, _else: MyFunction0): MyFunction0

We replace by-name parameters with MyFunction0. It is useful in nested if statements to return MyFunction0.

Let's port it into the type-level world:

    package sing

    trait Function0 extends sing.Any {
        type apply <: sing.Any
    }

    type `if`[b <: sing.Boolean, _then: sing.Function0, _else: sing.Function0] <: sing.Function0

Well done.

sing.Any

Type-level asInstanceOf

Without type-level generics, we need a way to downcast, that is, asInstanceOf for the type-level world. But asInstanceOf is inherently generic! What can we do?

sing.Any offers a painful way:

    package sing

    trait Any {
        type asBoolean <: sing.Boolean
        type asFunction0 <: sing.Function0
        // ...
    }

    sealed abstract class `true` extends sing.Boolean {
        type asBoolean = `true`
        // ...
    }

Though type-level isInstanceOf is feasible, it has not been implemented yet.

Type-level this

The this.type is. But it is not well synchronized with the type-inference rule. So you have to make it by yourself. It is called self in sing.

    package sing

    trait Any {
        type self <: Any
        // ...
    }

    sealed abstract class `true` extends sing.Boolean {
        type self = `true`
        // ...
    }

Primitives

As Scala doesn't contain any type-level primitives, we must implement them from scratch. sing offers the following:

  • Boolean
  • Nat (Natural numbers)
  • Unit
  • FunctionN
  • TupleN
  • etc...

sing.nat.peano and sing.nat.dense implements Nat. The former is good at small numbers, the latter at large numbers.

Singleton Types

sing is built upon singmethods, which emulate singleton-type system; Return types of methods vary depending on the expression.

    def myAssert(a: sing.`true`) = ()

    def testsingity {
        import sing.nat.peano.Literal._
        val i: _3 = _3
        val j: _5#minus[_2] = _5.minus(_2)
        myAssert(i.equal(j)) // type-level check
    }

The object _3 has its own type _3(the same name). i.equal(j) too has its own type(unspecified). Scalac reveals the concrete invocation _5#minus[_2] to the type true. Singleton-type system guarantees only one object has true type. That's why a runtime check is unneeded.

Singleton-type system provides a good subset of dependent-type system, which is not fully supported by the Scalac yet. Though the Scalac provides the built-in way: x.type, its usage is very restricted.

For further reading, see HList in Haskell or Lightweight Dependent-type Programming.

Crossing Boundary

unsing

Any sing thing can escape from the sing world by using sing.Any.unsing:

    import sing.nat.peano.Literal._

    def testUnsing {
        val i: scala.Int = _3.times(_2).unsing
        assertEquals(6, i)
    }

Boxing

A normal type requires a conversion into sing world by using sing.Box:

    def testBox {
        val j = sing.Tuple2(sing.Box(2), sing.Box(3))
        assertEquals(3, j._2.unsing)
    }

Unfortunately no auto-boxing is provided.

Lifting

As boxing is cumbersome, several short-cuts are offered:

    def testLift {
        val j = sing.Tuple(2, 3) // == Tuple2(Box(2), Box(3))
        val xs = 3 #:: 4 #:: 5 #:: sing.Nil // == Box(3) :: Box(4) :: Box(5) :: Nil
        val y = xs.foldLeft(j._1, sing.Function.lift2((y: Int, x: Int) => y + x))
        assertEquals(2+3+4+5, y.unsing)
    }

Heterogeneous List

Any sing data structure is heterogeneous; concrete types are preserved. For an example, look into sing.List:

    import sing.nat.peano.Literal._

    object add2 extends sing.Function1 {
        override type self = add2.type
        override  def apply[x <: sing.Any](x: x): apply[x] = x.asNat plus _2
        override type apply[x <: sing.Any] = x#asNat#plus[_2]
    }

    def testTrivial {
        val xs = _3 :: _4 :: _5 :: _6 :: sing.Nil
        val u = xs.map(add2) // returns a view
        assertEquals(5 :: 6 :: 7 :: 8 :: Nil, u.unsing)
        locally {
            import sing.::
            val v: _5 :: _6 :: _7 :: _8 :: sing.Nil = u.force
        }
    }

Because it is unknown how to implement the sing lambda, you have to write down sing.FunctionN implementation one by one.

Notice sing.List methods return views. force turns a view into a concrete form.

Unlike MetaScala, sing doesn't make use of implicit. In general, implicit is not modular. You can't build a large method from small methods.

Computational Model

The type-level programming in Scala is (as far as I know):

  • Pure(no-side effects) object-oriented. (Have you ever seen such a language?)
  • Actual parameters are fully evaluated before passed to a typemethod.
  • Type equality, that is, type-level eq is infeasible in practical performance.
  • A typemethod invocation is memoized.
  • Greedy; a concrete type is computed in advance.

Fibonacci numbers:

    type fibonacci[n <: sing.Nat] = sing.`if`[n#lt[_2], sing.const0[n], FibElse[n]]#apply#asNat

    trait FibElse[n <: sing.Nat] extends sing.Function0 {
        type self = FibElse[n]
        override type apply = fibonacci[n#decrement]#plus[fibonacci[n#decrement#decrement]]
    }

This works in O(n), but:

    type fibonacci[n <: sing.Nat] = sing.`if`[n#lt[_2], sing.const0[n], FibElse[n]]#apply#asNat

    trait FibElse[n <: sing.Nat] extends sing.Function0 {
        type self = FibElse[n]
        override type apply = fibonacci[n#minus[_1]]#plus[fibonacci[n#decrement#decrement]]
    }

This is O(n^2), for n#minus[_1] escapes memoization. See C++ Template Metaprogramming APPENDIX C.3.1 if you are a C++ programmer.

You might notice that, because termmethods are not memoized, the performance complexitiy of typemethod and termmethod is different.

Defects

User-defined Kinds

Type-level asInstanceOf is implemented by sing.Any.asXXX. As a result, it is impossible for users to define new kinds. Users must make full use of sing.TupleN and sing.map.

Assertion

It is not known to implement the type-level assertion or exception. (In fact, one possible way is to brew up the stack-overflow in the Scalac intentionally, but it's not informative at all.) You can use sing.Test.assertTrue, which is the same as myAssert above, only if it takes a concrete invocation.

Type Safety

Scalac doesn't warn for an illegal invocation of sing.Any.asXXX, which returns something like Nothing type. Nothing is the type-level null. null is the disaster. (This would be fixed if type-level assertion were feasible.)

Boxing

It is difficult to generate boxed types, which wraps normal scala types, with practical usefulness: type-equality, conformance-ordering, etc... Working in theory, but complication would last for a few days.

Pitfalls

Structural Complexity of Typemethod Arguments

See C++ Template Metaprogramming APPENDIX C.3.7. Compilation speed is strongly influenced by the complexity of typemethod expression structure. Trivial benchmark shows O(n^2) with nested level n. isGround at nsc/symtab/Types.scala seems a bottle neck.

Class in Class

    class Outer[m <: sing.Nat](m: m) {
        type apply[n <: sing.Nat] = ...

        case class Inner[n <: sing.Nat](n: n) extends sing.Function0 {
            type self = Inner[n]
            override type apply = ...
            // ...
        }
    }

This form possibly makes compilation significantly slower. equals at nsc/symtab/Names.scala seems a bottle neck. Prefer class in object.

case class is OK.

case doesn't effect on compilation time. You can easily implement singmethods thanks to case classes.

Typemethod in Kinds shall be Abstract

    trait MyKind extends sing.Any {
        type foo <: sing.Nat
        type bar = foo#plus[foo] // !!?
    }

bar possibly returns a wrong type in call-site. (The minimal condition is not known yet.) Define intermediate classes for default typemethod implementation; as Java does in AstractXXX classes.

Links

Shunsuke Sogame <okomok@gmail.com>