Tonλ's blog May the λ be with you

PIH - ch11 - The countdown problem - exercises

by @ardumont on

It has been a while since I read some programming in haskell book (matasano challenges, euler problems, 4clojure problems, and so on kept me busy :D). So here it goes, the chapter 11 exercises was about the countdown problem.

Given a list of numbers and a result, find the possible operations which, when evaluated, renders such result.

choices

Redefine the combinatorial function choices using a list comprehension rather than the library functions concat and map.

My first point-free implementation:

choices :: [a] -> [[a]]
choices = (concatMap perms) . subs

Using list comprehension:

choices :: [a] -> [[a]]
choices xs = [ p | s <- subs xs, p <- perms s]

*Problem> choices [1,2,3]
[[],[3],[2],[2,3],[3,2],[1],[1,3],[3,1],[1,2],[2,1],[1,2,3],[2,1,3],[2,3,1],[1,3,2],[3,1,2],[3,2,1]]
*Problem> choices [1,2,3,4]
[[],[4],[3],[3,4],[4,3],[2],[2,4],[4,2],[2,3],[3,2],[2,3,4],[3,2,4],[3,4,2],[2,4,3],[4,2,3],[4,3,2],[1],[1,4],[4,1],[1,3],[3,1],[1,3,4],[3,1,4],[3,4,1],[1,4,3],[4,1,3],[4,3,1],[1,2],[2,1],[1,2,4],[2,1,4],[2,4,1],[1,4,2],[4,1,2],[4,2,1],[1,2,3],[2,1,3],[2,3,1],[1,3,2],[3,1,2],[3,2,1],[1,2,3,4],[2,1,3,4],[2,3,1,4],[2,3,4,1],[1,3,2,4],[3,1,2,4],[3,2,1,4],[3,2,4,1],[1,3,4,2],[3,1,4,2],[3,4,1,2],[3,4,2,1],[1,2,4,3],[2,1,4,3],[2,4,1,3],[2,4,3,1],[1,4,2,3],[4,1,2,3],[4,2,1,3],[4,2,3,1],[1,4,3,2],[4,1,3,2],[4,3,1,2],[4,3,2,1]]

isChoice

Define a recursive function isChoice :: Eq a ⇒ [a] → [a] → Bool that decides if one list is chosen from another, without using the combinatorial functions perms and subs.

Hint: start by defining a function that removes the first occurrence of a value from a list.

We'll follow the hint and implement the function remove1:

remove1 :: Eq a => a -> [a] -> [a]
remove1 _ [] = []
remove1 x (y:ys)
  | x == y = ys
  | otherwise = y:remove1 x ys

*Problem> remove1 1 [3,2..1]
[3,2]
*Problem> remove1 1 [9,8..1]
[9,8,7,6,5,4,3,2]
*Problem> remove1 1 [9,8..0]
[9,8,7,6,5,4,3,2,0]
*Problem> remove1 1 [9,8..0] ++ [1]
[9,8,7,6,5,4,3,2,0,1]

Now, we can use it to help us implement the isChoice function

isChoice :: Eq a => [a] -> [a] -> Bool
isChoice [] _     = True
isChoice _ []     = False
isChoice (x:xs) l = elem x l && isChoice xs (remove1 x l)

*problem> isChoice [11] [1..10]
False
*Problem> isChoice [2..3] [1..10]
True
*Problem> isChoice [] [1..10]
True
*Problem> isChoice [] []
True
*Problem> isChoice [1..2] []
False

split

What effect would generalising the function split to also return pairs containing the empty list have on the behaviour of solutions?

Here is the split function:

split :: [a] -> [([a], [a])]
split []     = []
split [_]    = []
split (x:xs) = ([x], xs) : [(x: ls, rs) | (ls, rs) <- split xs]

*Problem> split [1,3,7,10,25,50]
[([1],[3,7,10,25,50]),([1,3],[7,10,25,50]),([1,3,7],[10,25,50]),([1,3,7,10],[25,50]),([1,3,7,10,25],[50])]

And the client call:

exprs :: [Int] -> [Expr]
exprs []  = []
exprs [x] = [Val x]
exprs xs  = [ x | (ls, rs) <- split xs,
                  l        <- exprs ls,
                  r        <- exprs rs,
                  x        <- combine l r]

Problem> exprs [1, 10]
[App Mul (Val 1) (Val 10),App Add (Val 1) (Val 10),App Sub (Val 1) (Val 10),App Div (Val 1) (Val 10)]

Adding the empty pairs to the existing result, [[], l], [l, []] (l is the initial list), would result in exprs to never reduce the size of the list when calling recursively exprs. Thus breaking.

Checks

Using choices, exprs, and eval, verify that there are 33665406 possible expressions over the numbers 1, 3, 7, 10, 25, 50, and that only 4672540 of these expressions evaluate successfully.

Computing all possible expressions from all possible choices from the list [1, 3, 7, 10, 25, 50] renders:

*Problem> length [ es | cs <- choices [1,3,7,10,25,50], es <- exprs cs]
33665406

With the current definition of a valid expression:

valid :: Op -> Int -> Int -> Bool
valid Add x y = x <= y
valid Sub x y = x > y
valid Mul x y = x <= y && x /= 1 && y /= 1
valid Div x y = y /= 1 && x `mod` y == 0

Evaluating those valid expressions renders:

*Problem> length [ es | cs <- choices [1,3,7,10,25,50], es <- exprs cs, eval es /= []]
245644

which differs from the problem.

Using the basic implementation for valid, we obtain indeed the same number.

valid :: Op -> Int -> Int -> Bool
valid Add _ _ = True
valid Sub x y = x > y
valid Mul _ _ = True
valid Div x y = y /= 0 && x `mod` y == 0

*Problem> length [ es | cs <- choices [1,3,7,10,25,50], es <- exprs cs, eval es /= []]
4672540

Checks 2

Similarly, verify that the number of expressions that evaluate successfully increases to 10839369 if the numeric domain is generalised to arbitrary integers.

Hint: modify the definition of valid.

Now modifying the signature of valid to permit no particular filter on those numbers:

valid :: Op -> Int -> Int -> Bool
valid Add _ _ = True
valid Sub _ _ = True
valid Mul _ _ = True
valid Div x y = y /= 0 && x `mod` y == 0

We obtain:

*Problem> length [ es | cs <- choices [1,3,7,10,25,50], es <- exprs cs, eval es /= []]
10839369

Modifications

Modify the final program to:

  • allow the use of exponentiation in expressions;
  • produce the nearest solutions if no exact solution is possible;
  • order the solutions using a suitable measure of simplicity.

We first need to add the exp operation in the data Op:

-- operation
data Op = Add | Sub | Mul | Div | Exp deriving (Show)

Then update the definition of valid expression. We will follow the same guideline, that is no negative exponent and as a ^ 1 = a=, we will refuse any exponent 1. Thus the following definition:

valid :: Op -> Int -> Int -> Bool
valid Add x y = x <= y
valid Sub x y = x > y
valid Mul x y = x <= y && x /= 1 && y /= 1
valid Div x y = y > 1 && x `mod` y == 0
valid Exp _ y = y == 0 || y > 1

*Problem> valid Exp 10 2
True
*Problem> valid Exp 10 (-1)
False
*Problem> valid Exp 10 1
False

Then updating the apply function to add the evaluation part of the Exp operation:

apply :: Op -> Int -> Int -> Int
apply Add x y = x + y
apply Sub x y = x - y
apply Mul x y = x * y
apply Div x y = x `div` y
apply Exp x y = x ^ y

*Problem> apply Exp 10 2
100

At last, updating the ops function to add Exp to the possible operations:

ops :: [Op]
ops = [Mul, Add, Sub, Div, Exp]

Now everything is good to be able to compute possible expressions. For example, with 1, 2, 10, and a solution 100, we only have the expression 10 ^ 2:

*Problem> solutions' [1,2,10] 100
[App Exp (Val 10) (Val 2)]

QuickCheck

We will use a little bit of quickCheck to ensure everything is still ok.

First we need to deal with the property we want to ensure is ok. This property is the fact that every expression generated from the list [1, 3, 7, 10, 25, 50], for a given n, when evaluated, indeed renders [n].

For this, we will need a generator of not too big interval [1..1000] seems good.

prop_solution = forAll (elements [1..1000]) $ \n -> all (\x -> eval x == [n]) (solutions' [1, 3, 7, 10, 25, 50] n)

Then, as this treatment is heavy (the number of expressions can be quite huge), we will limit ourselves to only 2 passing tests (feel free to increase).

deepCheck :: Testable prop => prop -> IO ()
deepCheck p = verboseCheckWith stdArgs { maxSuccess = 2 } p

Now we can run the tests.

test :: IO ()
test = do
  deepCheck prop_solution

*Problem> test
Passed:
852
Passed:
332
+++ OK, passed 2 tests.

QuickCheck gave us the number it elected, for the curious, we can see those results:

*Problem> solutions' [1, 3, 7, 10, 25, 50] 852
[App Sub (App Mul (App Sub (Val 25) (Val 10)) (App Add (Val 7) (Val 50))) (Val 3),App Add (App Sub (Val 3) (Val 1)) (App Mul (App Add (Val 7) (Val 10)) (Val 50)),App Add (Val 3) (App Sub (App Mul (App Add (Val 7) (Val 10)) (Val 50)) (Val 1)),App Sub (App Add (Val 3) (App Mul (App Add (Val 7) (Val 10)) (Val 50))) (Val 1),App Sub (App Mul (App Add (Val 1) (Val 10)) (App Add (Val 7) (App Mul (Val 3) (Val 25)))) (Val 50),App Add (App Sub (Val 3) (App Exp (Val 1) (Val 25))) (App Mul (App Add (Val 7) (Val 10)) (Val 50)),App Sub (App Mul (App Sub (Val 25) (Val 7)) (App Sub (Val 50) (Val 1))) (App Mul (Val 3) (Val 10)),App Add (Val 3) (App Sub (App Mul (App Add (Val 7) (Val 10)) (Val 50)) (App Exp (Val 1) (Val 25))),App Sub (App Add (Val 3) (App Mul (App Add (Val 7) (Val 10)) (Val 50))) (App Exp (Val 1) (Val 25))]
*Problem> length $ solutions' [1, 3, 7, 10, 25, 50] 852
9
*Problem> length $ solutions' [1, 3, 7, 10, 25, 50] 332
145

Latest posts