Implementing Least-Strict Natural Numbers

Introduction

On this page I want to summarize some insights about the behaviour of natural number implementations (in respect to non-strictness). I have gained these by using a tool called StrictCheck. StrictCheck was developed by Olaf Chitil and is supposed to test whether a function is unnecessarily strict. In the long term my goal is to improve the suggestions made by StrictCheck because in some cases StrictCheck is too ambitious. More Information about the development of StrictCheck can be found on a separate page. This page is supposed to be some kind of motivation for further development of StrictCheck. It shows that StrictCheck can be used to gain surprising insights about functions. For example the multiplication of Peano numbers like it is implemented naively is not least-strict. Furthermore the examples should demonstrate that StrictCheck is especially useful in the area of functional logic programming.

This work also initiated an implementation of natural numbers by a binary representation in Haskell. This implementation is similar to the implementation presented in Section “Binary Numbers”. This implementation is efficient in comparison to Peano numbers and is still less strict than the external numbers. A cabal package can be found at hackageDB.

The idea is to identify functions that are not least-strict. A function is not least-strict if we can alter it such that it behaves equally for total values but yields a more defined result for a partial value. In a functional logic programming language it is very important that functions are least-strict because least-strict functions result in the smallest possible search space when we use free variables.

Peano Numbers

We start with an implementation of Peano numbers.

data Peano = Z | S Peano

We will see that even for this trivial data type a naive implementation is not least-strict.

Multiplication

We implement a addition and on the basis of this a multiplication for Peano numbers.

(+) :: Peano -> Peano -> Peano
Z   + y = y
S x + y = S (x + y)

(*) :: Peano -> Peano -> Peano
Z   * _ = Z
S x * y = y + (x * y)

The following example is supposed to demonstrate that even the implementation of simple Peano arithmetic is not least strict if we are not careful. The Curry function check takes a number n and checks whether there is another number m such that m * n = 1.

check :: Peano -> Peano 
check n | m * n == S Z = m
  where m free

This function guesses a Peano number m and checks whether it yields 1 when multiplied by n. If we apply check to Z the function does not terminate. If we swap the arguments of (*) in check it yields no more solutions because there is no natural number m with the property 0 * m = 1. But there is also no number m with the property m * 0 = 1.

Although least-strictness is more important in a functional logic language the multiplication shows up an undesirable behaviour in a functional programming language, too. For example we can define the infinite Peano number as follows.

infinity :: Peano
infinity = S infinity

While the evaluation of Z * infinity yields Z the evaluation of infinity * Z does not terminiate.

So, let's have a closer look at the behaviour of (*). We use StrictCheck to check whether (*) is least-strict.

Failed:  f _|_         Z = _|_    but should be Z
Failed:  f (S _|_)     Z = _|_    but should be Z
Failed:  f (S (S _|_)) Z = _|_    but should be Z

Note that we can check only a finite number of partial values and there are more examples where (*) is not least-strict. These results state that an application of (*) to _|_ and Z yields _|_ but should yield Z. Sadly this is not possible (at least in Haskell) because StrictCheck also demands that Z * _|_ yields 'Z'. But also in the context of Curry we are only interested in deterministic functions. We do not want to introduce non-determinism if it is not necessary. Therefore we would prefer StrictCheck to take this into account. I want to address exactly this problem in the future.

So, let's take a look at the second counter example and check whether we can satisfy this one. The operation (+) evaluates its first argument to HNF to yield a HNF. Therefore the expression y + (x * y) demands the evaluation of y to yield a HNF. The recursive call to (*) demands the evaluation of its first argument. Therefore it would be cleverer to switch the arguments of the recursive call of (*).

(*) :: Peano -> Peano -> Peano
Z   * _ = Z
S x * y = y + (y * x)

We check this new implementation with StrictCheck.

Failed:  f _|_ Z = _|_  but should be Z

That is the other two cases are least-strict now. Furthermore now the evaluation of check Z yields no more solutions and infinity * Z yields Z.

Comparison

We want to take a look at the implementation of comparison functions for Peano numbers now. First we define a comparison of two Peano numbers which yields a value of type Ordering.

compare :: Peano -> Peano -> Ordering
compare Z     Z     = EQ
compare Z     (S _) = LT
compare (S _) Z     = GT
compare (S x) (S y) = compare x y

On basis of this function we can define (<), (<=), (>) and (>=). For example (>) is defined as follows.

(>) :: Peano -> Peano -> Bool
x > y = compare x y == GT

In Haskell these functions belong to the type class Ord. It is sufficient to only define compare. In this case (<) is exactly defined as stated above. So let's check (<) for least-strictness.

Failed:  f _|_ Z = _|_          but should be False
Failed:  f (S _|_) (S Z) = _|_  but should be False

Well, the first case sounds reasonable. If we check whether anything is smaller than Z we know this is false. If we take a look at compare we see that we preform pattern matching on both arguments. But we do not have to. If we would define (>) without using compare it would look like the following.

(>) :: Peano -> Peano -> Bool
Z   > _   = False
S _ > Z   = True
S x > S y = x > y 

This function is least-strict. It looks very similar to compare but we merge the first two cases of compare. Because we do not want to define all the comparison functions this way we define a special version of compare called compareLT. This function yields LT instead of EQ in case the two arguments are equal.

compareLT :: Peano -> Peano -> Ordering
compareLT Z     _     = LT
compareLT (S _) Z     = GT
compareLT (S x) (S y) = compareLT x y

Now we can define a least-strict version of all the comparison functions on basis of compareLT.

x > y = compareLT x y == GT
x >= y = compareLT y x == LT
x < y = compareLT y x == GT
x <= y = compareLT x y == LT

Binary Numbers

Peano numbers are nice if you want to use narrowing but they are very inefficient if you want to do arithmetic. Therefore Curry provides an implementation of natural numbers by a binary representation. The implementation in Curry differs slightly from the one presented here.

data Nat1 = IHi | O Nat | I Nat

On the basis of this data type we implement natural numbers including zero (which nobody really likes but apparently everybodty needs ;) ).

data Nat = Zero | Pos Nat1

On the basis of this data type we implement signed integers.

data Int = Neg Nat1 | Pos0 Nat

Multiplication

The multiplication of two binary numbers can be implemented as follows.

(*) :: Nat1 -> Nat1 -> Nat1
IHi * y = y
I x * y = y + O (x * y) 
O x * y = O (x * y)

This implementation has a similar drawback (with respect to least-strictness) as the implementation of the multiplication of Peano numbers. The addition evaluates both its arguments to HNF to yield a HNF. Therefore it is better with respect to least-strictness to swap the arguments of the recursive call of (*). The following implementation is least-strict.

(*) :: Nat1 -> Nat1 -> Nat1
IHi * y = y
I x * y = y + O (y * x) 
O x * y = O (x * y)

Note that it is not a good idea to swap the second recursive call of (*). Note furthermore that this implementation is not the canonical one and it is very difficult to see whether this function is least-strict. Therefore a supporting tool would be very valuable.

Minus

(-) :: Nat1 -> Nat1 -> Int
IHi - y   = inc (Neg y)          
O x - IHi = Pos (pred (O x))    
I x - IHi = Pos (O x)
O x - O y = o (x - y)
O x - I y = dec (o (x - y))
I x - O y = inc (o (x - y))
I x - I y = o (x - y)     

o :: Int -> Int
o (Neg n)  = Neg (O n)
o (Pos0 n) = 
  Pos0 (case n of
             Zero  -> Zero
             Pos m -> Pos (O m))
(-) :: Nat1 -> Nat1 -> Int
x - y = 
  case minus x y of
    Pos0 n  -> Pos0 n
    Neg IHi -> zero
    Neg n   -> Neg (pred n)

minus :: Nat1 -> Nat1 -> Int
minus IHi     y     = Neg y
minus x@(O _) IHi   = pos (pred x)
minus (I x)   IHi   = pos (O x)
minus (O x)   (O y) = incNeg (o (minus x y))
minus (I x)   (I y) = incNeg (o (minus x y))
minus (O x)   (I y) = decNeg (o (minus x y))
minus (I x)   (O y) = decNeg (o (negate (minus y x)))
 
incNeg :: Int -> Int
incNeg (Neg n)  = Neg (pred n)
incNeg (Pos0 n) = Pos0 n

decNeg :: Int -> Int 
decNeg (Neg n)  = Neg n
decNeg (Pos0 n) = Pos0 (pred n)

Division

A simple implementation of the division of two natural numbers is implemented as following.

divmodNat :: Nat -> Nat -> (Nat,Nat)
divmodNat _ Zero = error "divide by zero"
divmodNat Zero _ = (Zero,Zero)
divmodNat (Pos x) (Pos y) = divmodNat1 x y

divmodNat1 :: Nat1 -> Nat1 -> (Nat,Nat)
divmodNat1 x y
  | x<y       = (Zero,Pos x)
  | otherwise = (succ q,r)
 where
  (q,r) = divmodNat (Pos x-Pos y) (Pos y)

You can implement the test whether a number is even or not on basis of divmodNat. In Haskell you have actually no choice as the function even is implemented as following by demanding its argument to be a member of the type class Integral. For natural numbers mod and rem are the same.

even :: Nat -> Bool
even n = n `rem` Pos (O IHi) == Zero

Note that it is furthermore not possible to define 'even for the data type Nat in Haskell as it does not support a zero.

Let's test whether a number is even or not. In our representation we can check this by simply taking a look at the first bit. But with the above implementation the expression even (Pos (O _|_)) is evaluated to _|_. But we improve this implementation as follows.

divmodNat :: Nat -> Nat -> (Int,Int)
divmodNat x     IHi   = (Pos x,Zero)
divmodNat (O x) (O y) = (q,o r) 
 where
  (q,r) = divmodNat x y
divmodNat (I x) (O y) = (q,i r) 
 where
  (q,r) = divmodNat x y
divmodNat x y
  | x<y       = (Zero,Pos x)
  | otherwise = (succ q,r)
 where
  (q,r) = divmodNat (Pos x-Pos y) (Pos y)

o :: Int -> Int
o Zero    = Zero
o (Pos n) = Pos (O n)

i :: Int -> Int
i Zero    = Pos IHi
i (Pos n) = Pos (I n)

When we now check even (Pos (O _|_)) we get True as we have expected.

Comparison

The following shows a canonical way of implementing the compare function for the Nat data type.

compare :: Nat -> Nat -> Ordering
compare x y = cmpNat EQ x y

cmpNat :: Ordering -> Nat -> Nat -> Ordering
cmpNat eq IHi IHi = eq
cmpNat _  IHi (O _) = LT
cmpNat _  IHi (I _) = LT
cmpNat _  (O _) IHi = GT
cmpNat _  (I _) IHi = GT
cmpNat eq (O x) (O y) = cmpNat eq x y
cmpNat eq (I x) (I y) = cmpNat eq x y
cmpNat _  (O x) (I y) = cmpNat LT x y
cmpNat _  (I x) (O y) = cmpNat GT x y

Instead of using a case distinction on the result of the recursive call it uses an extra parameter. When we check this function using StrictCheck we get the following result.

Failed:  f (2*_|_+1) (2*1) = _|_        but should be GT
Failed:  f (2*1) (2*_|_+1) = _|_        but should be LT

Are these counter examples satisfiable? Let's take a look at more counter examples.

Failed:  f (2*(2*_|_+1)) (2*(2*1)) = _|_        but should be GT
Failed:  f (2*(2*_|_+1)) (2*(2*1)+1) = _|_      but should be GT

These all have in common that you do not have to evaluate the second argument if the other-one is IHi. And you do not have to because you know from a less significant bit that one of the two numbers is less than the other. In the definition of compare if the first argument is IHi we check the other one. But we do not have to if the numbers differ in one of the previous bits. Therefore we define a function compareLT like we did for Peano numbers which yields LT in the case that its arguments are equal. This way we can merge the first three cases of the function and do not have to pattern match on the second argument. Instead of defining a similar function compareGT we use a function invOrd which changes LT to GT and LT to GT.

compareLT :: Nat1 -> Nat1 -> Ordering
comapreLT IHi _     = LT
compareLT (O _) IHi = GT
compareLT (I _) IHi = GT
compareLT (O x) (O y) = compareLT x y
compareLT (I x) (I y) = compareLT x y
compareLT (O x) (I y) = compareLT x y
compareLT (I x) (O y) = invOrd (compareLT y x)

On basis of compareLT we define compare.

compare :: Nat1 -> Nat1 -> Ordering
compare IHi IHi   = EQ
compare IHi (O _) = LT
compare IHi (I _) = LT
compare (O _) IHi = GT
compare (I _) IHi = GT
compare (O x) (O y) = compare x y
compare (I x) (I y) = compare x y
compare (O x) (I y) = compareLT x y
compare (I x) (O y) = invOrd (compareLT y x)
 

We can reduce the code duplication by introducing a functional parameter. When we check the new implementation of compare there are no more counterexamples. Another way of implementing this is using minus and checking the sign of the result.

Performance

The following Curry function can be used to generate Pythagorean triples.

pyt :: (Nat,Nat,Nat)
pyt | a*a+b*b==c*c = (a,b,c)
  where a,b,c free

testPyt n = take n (allValuesB (searchTree pyt))
no of triples old implementation lest-strict implementation
100 3.2s 1.2s
200 21.8s 3.9s
300 46.2s 13.8s

Conclusion

If you want to define a function which is as non-strict as possible you have to invest brain power. Furthermore it is very difficult to reason about the non-strictness of a function. Even for simple functions like the multiplication of Peano numbers the canonical implementation is not least-strict. There are good reasons to be least-strict in a functional programming language (for example infinite data structures like infinity). But there are even better reasons to be least-strict in a functional logic programming language.

Right now you have to interpret the results of StrictCheck which is not easy. Therefore we have to improve the tool to make applicable for everybody.

Jan Christiansen

/srv/dokuwiki/currywiki/data/pages/fun/naturals.txt · Last modified: 2014-06-13 12:35 by 127.0.0.1
Back to top
CC Attribution-Noncommercial-Share Alike 4.0 International
Driven by DokuWiki Recent changes RSS feed Valid CSS Valid XHTML 1.0