Meta-Programming with Scala: Conditional Compilation and Loop Unrolling

29 10 2008

The kind of comments I keep getting on my static meta-programming with Scala blogs are often along the lines of: “The ability to encode Church Numerals in Scala still seems uselessly academic to me, but cool none-the-less”. In this blog I will show how meta-programming can be applied in practice – at least theoretically.

In my previous blogs I introduced a technique for static meta-programing with Scala. This technique uses Scala’s type system to encode values and functions on these values. The Scala compiler acts as interpreter of such functions. That is, the type checking phase of the Scala compiler actually carries out calculations like addition and multiplication.

In this post I show how to apply meta-programming for two practical problems: conditional compilation and loop unrolling. The examples make use of type level encoding for booleans and natural numbers. While I introduced an encoding for natural numbers before, I use an alternative method which is more powerful in this post. Previously it was not possible to build nested expressions having expressions as operands themselves. The new encoding supports such expressions. However, in general the new encoding depends on the -Yrecursion compiler flag which is experimental and as of now only available in the Scala trunk. The type level encoding for booleans is along the same lines as the one for natural numbers.

Conditional Compilation

Conditional compilation is useful for example for enabling or disabling debugging or logging statements. Ideally code which is excluded by a compile time condition does not have any effect on the run-time behavior of the rest of the code. That is, the rest of the code behaves exactly as if the excluded code were not there at all. Optimizing compilers generally remove code which is unreachable. This is where meta-programming fits in: type level encoded functions (meta-functions) are evaluated at run-time. The result of the evaluation is a type. Now we only need to trick the compiler into compiling a block of code or not compiling it depending on that type.

Lets first define meta-booleans and some operations on them (full code here):

object Booleans {
  trait BOOL {
    type a[t <: BOOL, f <: BOOL&#93; <: BOOL
    type v = a&#91;TRUE, FALSE&#93;
  }
  final class TRUE extends BOOL {
    type a&#91;t <: BOOL, f <: BOOL&#93; = t
  }
  final class FALSE extends BOOL{
    type a&#91;t <: BOOL, f <: BOOL&#93; = f
  }
  trait IF&#91;x <: BOOL, y <: BOOL, z <: BOOL&#93; extends BOOL {
    type a&#91;t <: BOOL, f <: BOOL&#93; = x#a&#91;y, z&#93;#a&#91;t, f&#93;
  }
  trait NOT&#91;x <: BOOL&#93; extends BOOL {
    type a&#91;t <: BOOL, f <: BOOL&#93; = IF&#91;x, FALSE, TRUE&#93;#a&#91;t, f&#93;
  }
  trait AND&#91;x <: BOOL, y <: BOOL&#93; extends BOOL {
    type a&#91;t <: BOOL, f <: BOOL&#93; = IF&#91;x, y, FALSE&#93;#a&#91;t, f&#93;
  }
  trait OR&#91;x <: BOOL, y <: BOOL&#93; extends BOOL {
    type a&#91;t <: BOOL, f <: BOOL&#93; = IF&#91;x, TRUE, y&#93;#a&#91;t, f&#93;
  }

  // aliases for nicer syntax
  type !&#91;x <: BOOL&#93; = NOT&#91;x&#93;
  type ||&#91;x <: BOOL, y <: BOOL&#93; = OR&#91;x, y&#93;
  type &&&#91;x <: BOOL, y <: BOOL&#93; = AND&#91;x, y&#93;
}
&#91;/sourcecode&#93;

The following pre-processor object contains an <a href="http://www.scala-lang.org/node/114">implicit</a> method for converting a value of type <em>TRUE</em> to an <em>Include</em> object whose <em>apply</em> method executes a block of code. Similarly it contains an implicit method for converting a value of type <em>FALSE</em> to an <em>Exclude</em> object whose <em>apply</em> method simply does nothing. The strange line where <em>null</em> is being cast to <em>B</em> is a trick for getting a witnesses of a value of type <em>B</em>.


object PreProc {
  def IF[B] = null.asInstanceOf[B]

  object Include {
    def apply(block: => Unit) {
      block
    }
  }

  object Exclude {
    def apply(block: => Unit) { }
  }

  implicit def include(t: TRUE) = Include
  implicit def exclude(f: FALSE) = Exclude
}

Using these definitions is quite convenient now:

object IfDefTest {
  import PreProc._

  type LOG = TRUE
  type ERR = TRUE
  type WARN = FALSE

  def errTest() {
    IF[(LOG && ERR)#v] {
      println("err")
    }
  }

  def warnTest() {
    IF[(LOG && WARN)#v] {
      println("warn")
    }
  }

  def main(args: Array[String]) {
    errTest()
    warnTest()
  }
}

Running the above code will print err but wont print warn to the console.

Loop Unrolling

Another application for static meta-programming is loop unrolling. When the number of iterations of a loop is small and only depends on quantities known at compile time, run time performance might profit from unrolling that loop. Instead of resorting to copy paste, we can use similar techniques like above.

Again let’s first define meta-naturals and their operations (full code here):

object Naturals {
  trait NAT {
    type a[s[_ <: NAT&#93; <: NAT, z <: NAT&#93; <: NAT
    type v = a&#91;SUCC, ZERO&#93;
  }
  final class ZERO extends NAT {
    type a&#91;s&#91;_ <: NAT&#93; <: NAT, z <: NAT&#93; = z
  }
  final class SUCC&#91;n <: NAT&#93; extends NAT {
    type a&#91;s&#91;_ <: NAT&#93; <: NAT, z <: NAT&#93; = s&#91;n#a&#91;s, z&#93;&#93;
  }
  type _0 = ZERO
  type _1 = SUCC&#91;_0&#93;
  type _2 = SUCC&#91;_1&#93;
  type _3 = SUCC&#91;_2&#93;
  type _4 = SUCC&#91;_3&#93;
  type _5 = SUCC&#91;_4&#93;
  type _6 = SUCC&#91;_5&#93;

  trait ADD&#91;n <: NAT, m <: NAT&#93; extends NAT {
    type a&#91;s&#91;_ <: NAT&#93; <: NAT, z <: NAT&#93; = n#a&#91;s, m#a&#91;s, z&#93;&#93;
  }
  trait MUL&#91;n <: NAT, m <: NAT&#93; extends NAT {
    trait curry&#91;n&#91;_&#91;_&#93;, _&#93;, s&#91;_&#93;&#93; { type f&#91;z&#93; = n&#91;s, z&#93; }
    type a&#91;s&#91;_ <: NAT&#93; <: NAT, z <: NAT&#93; = n#a&#91;curry&#91;m#a, s&#93;#f, z&#93;
  }

  // aliases for nicer syntax
  type +&#91;n <: NAT, m <: NAT&#93; = ADD&#91;n, m&#93;
  type x&#91;n <: NAT, m <: NAT&#93; = MUL&#91;n, m&#93;
}
&#91;/sourcecode&#93;

The pre-processor object defines a trait <em>Loop</em> having an <em>apply</em> method which takes a block of code as argument. Again there are two implicit conversion methods. One which converts the zero type to a <em>Loop</em> with an empty <em>apply</em> function. An another one which convert the type <em>N + 1</em> to a a <em>Loop</em> with an <em>apply</em> function which executes the block once and then applies itself to the type <em>N</em>.


object PreProc {
  def LOOP[N] = null.asInstanceOf[N]

  trait Loop[N] {
    def apply(block: => Unit)
  }

  implicit def loop0(n: ZERO) = new Loop[ZERO] {
    def apply(block: => Unit) { }
  }

  implicit def loop[N <: NAT&#93;(n: SUCC&#91;N&#93;)(implicit f: N => Loop[N]) = new Loop[SUCC[N]] {
    def apply(block: => Unit) {
      block
      null.asInstanceOf[N].apply(block)
    }
  }
}

Again using this is easy and convenient:

object LoopUnroll {
  import PreProc._

  def unrollTest() {
    // The following line needs the -Yrecursion 1 flag
    // LOOP[(_3 x _2)#v] {
    LOOP[_6] {
      println("hello world")
    }
  }

  def main(args: Array[String]) {
    unrollTest()
  }
}

The above code prints the string “hello word” six times to the console.

Conclusion

Scala’s type system is powerful enough for encoding commonly encountered functions. Together with Scala’s strong capability for creating internal DSLs, this results in convenient techniques for static meta-programming. Such techniques can be applied to practical problems – at least in theory. In practice Scala’s support is not (yet?) there. For one the technique presented here depends on an experimental compiler flag (-Yrecursion). Further the types required for meta-programming might causes an exponential growth in compilation time which is not desirable. And finally an analysis with c1visualizerwith showed, that the compiler seems not to remove all unnecessary calls.

Advertisements