From oleg-at-okmij.org Thu Feb 2 22:42:08 2006 To: haskell@haskell.org Subject: Class-parameterized classes, and the type-level logarithm From: oleg@pobox.com Message-ID: <20060203064208.07FBDA9D0@Adric.metnet.navy.mil> Date: Thu, 2 Feb 2006 22:42:08 -0800 (PST) Status: OR We show invertible, terminating, 3-place addition, multiplication, and exponentiation relations on type-level Peano numerals, where _any_ two operands determine the third. We also show the invertible factorial relation. This gives us all common arithmetic operations on Peano numerals, including n-base discrete logarithm, n-th root, and the inverse of factorial. The inverting method can work with any representation of (type-level) numerals, binary or decimal. Furthermore, the inverter itself is generic: it is a type-class function, that is, a type-class parameterized by the type-class to `invert'. There has been a proposal on Haskell' to give equal rights to types and classes. In Haskell98+multi-parameter type classes, classes are already first-class, for all practical purposes. We can quite easily define (potentially, higher-order) type functions on type classes. Ashley Yakeley wrote: ] I know for the usual Peano representation of natural numbers, at least, ] it's possible to represent addition and subtraction with a single class ] with two fundeps (because classes represent relations between types ] rather than functions on them). That can be done even for decimal type numerals, cf. number-parameterized-types paper. But we can do better: we can have _three_ functional dependencies, so that any two operands of the type class determine the third. The key insight was the result of a conversation with Chung-chieh Shan and Gregory Price in the evening of Nov 10, 2003. Although our computational algorithms, over ground numerals, are assuredly terminating, one often needs a global analysis of instances to see that. Hence we have to resort to the undecidable instances extension. > {-# OPTIONS -fglasgow-exts #-} > {-# OPTIONS -fallow-undecidable-instances #-} > > module PeanoArithm where > > data Z > data S a > __ = __ First we define a semi-sum relation, with only two functional dependencies: > class Sum2 a b c | a b -> c, a c -> b > instance Sum2 Z a a > instance Sum2 a b c => Sum2 (S a) b (S c) Now we add the third dependency. It is that simple. > class Sum a b c | a b -> c, a c -> b, b c -> a > instance (Sum2 a b c, Sum2 b a c) => Sum a b c After defining a few numbers, > add:: Sum a b c => a -> b -> c > add = __ > > type One = S Z > type Two = S (S Z) > > zero = __ :: Z > one = __ :: One > two = __ :: Two > three = add one two > n4 = add two two we see that the typechecker can indeed add and subtract: > ta1 = \x -> (add three x `asTypeOf` three) > ta2 = \x -> (add x two `asTypeOf` three) > -- ta3 = \x -> (add three x `asTypeOf` two) -- fails *PeanoArithm> :t ta1 ta1 :: Z -> S (S (S Z)) *PeanoArithm> :t ta2 ta2 :: S Z -> S (S (S Z)) Before we move to products, we introduce a few utilities > class NLE x y > instance Sum2 x a y => NLE x y > > class Mul a b c | a b -> c -- a single-dependency multiplication > instance Mul Z b Z > instance (Mul a b c', Sum c' b c) => Mul (S a) b c -- (a+1)*b = a*b + b The relation |NLE x y| holds if |x| is less or equal to |y|, that is, there exists a numeral |a| that in sum with |x| gives |y|. We now introduce the generic inverter of an arithmetic function defined on the whole set of natural numbers. The inverter is parameterized by the numeric function (that is, a type class) to invert. We define the type of these classes by the following class. It maps types to classes (see also the HList paper): > class Registry clas a b c | clas a b -> c The inverter, the type-class function, is so trivial that I'm ashamed to discuss it. It is a simple for-loop. > class Inv clas init limit a b c | clas init a b -> c > instance (NLE x limit, Registry clas x b a', Sum2 a' r a, > Inv' r clas x limit a b c) > => Inv clas x limit a b c > class Inv' r clas x limit a b c | r clas x a b -> c > instance Inv' Z clas x limit a b x -- Found it > instance Inv clas (S x) limit a b c -- try next x > => Inv' (S r) clas x limit a b c Division is defined as an inverse of multiplication: > data RegMul > instance Mul a b c => Registry RegMul a b c > > class Div m n q | m n -> q -- m = n * q > instance Inv RegMul Z m m n q => Div m n q We partially apply Inv to the right operands, and get the Div relation in return. > pdiv :: Div a b c => a -> b -> c > pdiv = __ > > -- Only where all dependencies exist... > class Product a b c | a b -> c, a c -> b, b c -> a > instance (Mul a b c, Div c b a, Div c a b) => Product a b c > > prod:: Product a b c => a -> b -> c > prod = __ Now the typechecker can multiply and divide > n6 = prod two three > tm1 = \x -> (prod three x `asTypeOf` n6) > tm2 = \x -> (prod x two `asTypeOf` n6) > -- tm3 = \x -> (prod x n4 `asTypeOf` n6) > tm4 = \x -> (prod x n6 `asTypeOf` n6) > -- tm5 = \x -> (prod x zero `asTypeOf` n6) > tm6 = \x -> (prod x zero `asTypeOf` zero) The inferred types are tm1 :: S (S Z) -> S (S (S (S (S (S Z))))) tm2 :: S (S (S Z)) -> S (S (S (S (S (S Z))))) tm4 :: S Z -> S (S (S (S (S (S Z))))) The commented-out tests raise type errors. The exponentiation is trivial: > class Raise b e c | b e -> c -- c = b ^ e > instance Raise a Z (S Z) > instance (Raise b e c', Mul b c' c) => Raise b (S e) c The logarithm and the n-th root are two inverses of (partially-applied) exponentiation. It takes a couple of lines to define each > data RegRaiseI > instance Raise b a c => Registry RegRaiseI a b c > > class Log n b e | n b -> e -- e = log_b n > instance Inv RegRaiseI Z n n b e => Log n b e > > data RegRaise > instance Raise a b c => Registry RegRaise a b c > > class Root a b c | a b -> c -- c = b-th root of a > instance Inv RegRaise Z a a b c => Root a b c We introduce the full exponentiation relation > class Exp b e c | b e -> c, b c -> e, e c -> b > instance (Raise b e c, Log c b e, Root c e b) => Exp b e c > > pexp:: Exp a b c => a -> b -> c > pexp = __ and test it > n8 = add n4 n4 > n9 = prod three three > te1 = pexp three two > te2 = \x -> (pexp two x `asTypeOf` (add n8 n8)) > te3 = \x -> (pexp x three `asTypeOf` n8) > -- te4 = \x -> (pexp x three `asTypeOf` n6) > te5 = \x -> (pexp three x `asTypeOf` three) *PeanoArithm> :t te2 te2 :: S (S (S (S Z))) -> S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S Z))))))))))))))) *PeanoArithm> :t te3 te3 :: S (S Z) -> S (S (S (S (S (S (S (S Z))))))) Indeed, 4 is the binary logarithm of 16, and two is the cubic root of 8. Having gone that far, it is impossible to avoid factorial and its inverse (as an evidence of further evolution of the Haskell programmer) > class FactF a b | a -> b > instance FactF Z (S Z) > instance (FactF a b', Mul (S a) b' b) => FactF (S a) b > > data RegFact > instance FactF a c => Registry RegFact a b c > > class FactR a c | a -> c -- Inverse of Fact > instance Inv RegFact (S Z) a a Z c => FactR a c > > class Fact a b | a -> b, b -> a > instance (FactF a b, FactR b a) => Fact a b > > fact :: Fact a b => a -> b > fact = __ -- :t fact n4 *PeanoArithm> :t \x -> (fact x `asTypeOf` (prod n4 n6)) \x -> (fact x `asTypeOf` (prod n4 n6)) :: S (S (S (S Z))) -> S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S Z))))))))))))))))))))))) Indeed, 4 is the inverse factorial of 24. It took only a split second. The practical outcome of this exercise is the creation of a benchmark suite for Haskell typecheckers, e.g., computing the inverse factorial of 720. Perhaps there should be a shoot-out entry for the speed of typechecking. The implementation of RSA on type level is left for future work. > -- This is for printing only > class Nat a where nat :: Num b => a -> b > instance Nat Z where nat _ = 0 > instance Nat a => Nat (S a) where nat _ = 1 + (nat (__::a))