Scala type level utilities which anyway will be erased during compilation :-D
In Scala there are some restrictions applied to type level functions. Everyone could have seen illegal cyclic reference
error defining recursive type:
trait Vector[+T]
case object VNil extends Vector[Nothing]
case class Cons[+H, +T <: Vector[H]](h: H, t: T) extends Vector[H]
type Vec[N <: TNat, T] = N#Match[Vector[T], VNil.type, ({type F[M <: TNat] = Cons[T, Vec[M, T]]})#F]
// illegal cyclic reference involving type Vec
But there is a loophole (please see apocalisp article for detailed explanation) and it is still possible to define infinite loop in type level.
The only thing we need is ability to stop in the right position. And Fix
(from package fix
) allows this:
type Fix[TIn <: Reducible, TOut, F[_[_ <: TIn] <: TOut, _ <: TIn] <: TOut, _ <: TIn] <: TOut
type Vec[N <: TNat, T] = Fix[TNat, Vector[T], ({
type R[F[_ <: TNat] <: Vector[T], N <: TNat] =
N#Match[Vector[T], VNil.type, ({type R[N <: TNat] = Cons[T, F[N]]})#R]
})#R, N]
// no illegal cyclic reference
The main obstacle of type level fix point combinator implementation was something allowing to stop reducing of a term consisting of types (in this case it's more expansion than reduction).
During the computation (reduction) there is always the main object doing the job. And this very object initiates reduction. In samples above such a role is played by N <: TNat
. So all we need is tie together fix point expansion and initiator object reduction. We will do it in the following way:
- Create trait
Reducible
with abstracttype Reduce[...]
; - Create trait
ReducibleImpl
extendingReducible
with "implementation" oftype Reduce[...]
; - Make type level type (like
TNat
in samples above) extendingReducible
(seenat/TNat.scala
), that is it's "reducible" but abstract, so cannot "reduce"; - Make all the "instances" of that type level type (like
Zero
andSucc
) extendingReducibleImpl
.
That's it. When we have some type level function accepting Reducible
(but not ReducibleImpl
) we do nothing, because Reduce
type is abstract. But as soon as this function is passed concrete type extending ReducibleImpl
(where Reduce
is able to do the job) computation is starting. And is stopped again when there is no more ReducibleImpl
to proceed.
So the rule is "make reduction step, when your main object is able to do its own job".
This API still doesn't stop defining infinite loop. So the developer have to make sure his function will terminate.