Let’s look at solving Project Euler problem #4 at the type level. Since we are working with binary numbers in Scala’s type system, we have to substantially reduce our expectations for what we can solve.
The first simplification is that we will look at palindromes in base 2 since our numbers are already in that representation. Also, we won’t be looking at large numbers. We’ll just find the largest binary palindrome that is the product of two binary numbers less than N. Even N = 15 takes a while to finish, so we’ll stick with 15.
I’ll show the approach by starting with normal Scala code that implements a brute force solution:
object E4 { // class to hold the numbers i,j, prod = i*j case class Result(prod: Int, iMax: Int, jMax: Int) // compares Results based on the product implicit object ResultOrdering extends Ordering[Result] { def compare(a: Result, b: Result) = a.prod compare b.prod } // computes the largest binary palindrome made by multiplying // two numbers <= start def apply(start: Int): Result = all(start).max // iterates over (i, j) pairs, calculating products, and keeping track of palindromes def all(start: Int) : Traversable[Result] = for(i <- (start to 1 by -1); j <- (i to 1 by -1); val prod = i*j if isPalindrome(prod) ) yield Result(prod, i, j) // checks if the given integer is a binary palindrome def isPalindrome(i: Int) = i.toBinaryString == i.toBinaryString.reverse }
This is not easily translated directly to type-level programming, however. We need to convert it to explicitly recurse, explicitly track and update the maximum, and not use intermediate variables. We also drop the Result class in favor of a Tuple3.
object E4_Explicit { type Result = (Int, Int, Int) def apply(start: Int): Result = apply( (1,1,1), start, start) def apply(max: Result, i: Int, j: Int): Result = apply1(max, (i*j, i, j)) def apply1(max: Result, iteration: Result): Result = if(isPalindrome(iteration._1) && iteration._1 > max._1) apply2(iteration, iteration) else apply2(max, iteration) def apply2(max: Result, iteration: Result): Result = if(iteration._3 == 1) { if(iteration._2 == 1) max else apply(max, iteration._2 - 1, iteration._2 - 1) } else apply(max, iteration._2, iteration._3 - 1) def isPalindrome(i: Int) = i.toBinaryString == i.toBinaryString.reverse }
This still needs to be converted to better target type-level programming. Basically, ‘if’ statements and comparisons can be problematic. See the Expanded object in Euler4.scala for the normal Scala code closest to the type-level code that follows.
For the type-level code, let’s start with a test for binary palindromes:
type isPalindrome[D <: Dense] = D#Reverse[DNil]#Compare[D]#eq println( toBoolean[ isPalindrome[_3] ] ) // true println( toBoolean[ isPalindrome[_6] ] ) // false println( toBoolean[ isPalindrome[_15] ] ) // true
Then, let’s test if a number is a palindrome and greater than the current maximum palindrome:
type isLargerPalindrome[D <: Dense, Max <: Dense] = isPalindrome[D] && D#Compare[Max]#gt println( toBoolean[ isLargerPalindrome[ _7, _3] ] ) // true println( toBoolean[ isLargerPalindrome[ _3, _5] ] ) // false println( toBoolean[ isLargerPalindrome[ _8, _5] ] ) // false
What we want to do is then use Bool#If to process the result of isLargerPalindrome. Unfortunately, while everything worked ok up until this point, once we start building up the rest of the program, our type information gets lost. We have to do something less straightforward. We need to pass in what to return if the result is true or false. Roughly, we are inlining If, gt, and &&.
// if D is a palindrome and D > Max, return T, otherwise F type p2[D <: Dense, Max <: Dense, T <: Up, F <: Up, Up] = p1[D, D#Compare[Max]#Match[F, F, T, Up], F, Up] // if D is a palindrome, return T, otherwise F type p1[D <: Dense, T <: Up, F <: Up, Up] = D#Reverse[DNil]#Compare[D]#Match[F, T, F, Up]
Other than this, we can do a more or less straightforward translation into type-level code. First, we have to make a parent trait that declares the signature of our recursing abstract type:
trait Pali { // a type-level Tuple3 type Result = Triple[Dense, Dense, Dense] type Apply[max <: Dense, iMax <: Dense, jMax <: Dense, i <: Dense, j <: Dense, P <: Pali] <: Result }
And then implement it.
trait Pali0 extends Pali { // The entry point type App[start <: Dense, P <: Pali] = Apply[_1, _1, _1, start, start, P] // Recursion here type Apply[max <: Dense, iMax <: Dense, jMax <: Dense, i <: Dense, j <: Dense, P <: Pali] = Apply1[max, iMax, jMax, i, j, j#Mult[i], P ] type Apply1[max <: Dense, iMax <: Dense, jMax <: Dense, i <: Dense, j <: Dense, prod <: Dense, P <: Pali] = p2[prod, max, Apply2[prod, i, j, i, j#Dec, P], Apply2[max, iMax, jMax, i, j#Dec, P], Result ] type Apply2[max <: Dense, iMax <: Dense, jMax <: Dense, i <: Dense, j <: Dense, P <: Pali] = j#Match[ Apply3[max, iMax, jMax, i, j, P], Apply3[max, iMax, jMax, i#Dec, i#Dec, P], Result] type Apply3[max <: Dense, iMax <: Dense, jMax <: Dense, i <: Dense, j <: Dense, P <: Pali] = i#Match[ P#Apply[max, iMax, jMax, i, j, P], Triple.Apply[max, iMax, jMax, Dense], Result] // the palindrome tests from above type p1[D <: Dense, T <: Up, F <: Up, Up] = D#Reverse[DNil]#Compare[D]#Match[F, T, F, Up] type p2[D <: Dense, O <: Dense, T <: Up, F <: Up, Up] = p1[D, D#Compare[O]#Match[F, F, T, Up], F, Up] }
Running it gives:
println( toTuple3[Pali0#App[_7, Pali0]] ) // (21,7,3) // already pretty slow at _15 println( toTuple3[Pali1.App[_15, Pali1.type]] ) // (195,15,13)
Pingback: Type-Level Programming in Scala « Apocalisp
Pingback: Type-Level Programming in Scala, Part 6f: Deriving type class instances through HLists « Apocalisp