Divide And Conquer
Last semester I dipped into functional programming. Today I want to show you the part which stood out the most for me: Readability. Even without extensive programming knowledge, you can sort of understand what is going on, since you don’t need to worry about variables/memory and all that other nasty stuff that makes programming complicated at the beginning.
This is part of an assignment we got to show the adaptability of divide and conquer (which is used for example in quicksort, which we will examine later). To do so we were given the following function:
divAndConquer :: (p -> Bool) -> (p -> s) -> (p -> [p]) -> (p -> [s] -> s) -> p -> s divAndConquer ind solve divide combine initProblem = dac initProblem where dac problem | ind problem = solve problem | otherwise = combine problem (map dac (divide problem))
The first line may look intimidating if you have never worked with functional programming before, but let me take your fear because it is actually fairly simple to read.
This is the name of the function, no surprises here
(p -> Bool)
This specifies the first argument we pass to the function, which happens to be another function taking a variable of the type p (note that this is not a real type, it is only used to specify that later occurances of p must all have the same type) and returning a Bool. In the second line we see we call this function ind which is maybe a bad name, as later this function will be called on a problem to determine if it is solvable.
(p -> s)
So for the second argument we got another function, taking a problem and returning a solution. We fittingly name this one solve.
(p -> [p])
Now we go for the divide part of the algorithm. Again a function that takes p and something new for the return type, a list of p.
(p -> [s] -> s)
This is our combine function to use on the solved subproblems.
The problem we are going to solve
The return type of our divAndConquer function
In the next step we are going to use this abstract function to implement quicksort. If you never heard of this sorting algorithm before, don’t worry, we will have a look at it right now.
quickSort :: Ord a => [a] -> [a] quickSort = divAndConquer indq solveq divideq combineq
If we break this function down, you see Ord a which requires that a derives from the Ord class. This enables the use of comperatores < and > and all their forms on elements of a.
The rest should be fairly obvious. Our input is a list of a, as is our output. In the second line we see that we call divAndConquer with subfunctions we have to define now.
indq :: [a] -> Bool indq problem | length(problem) <= 1 = True -- This is called a guard. It is similar to if's in imperative | otherwise = False -- languages.
As mentioned above this function checks if we can solve a problem or if we have to divide it further. In our case this is really simple since an unordered list is always ordered when it’s length is smaller or equal to 1. Else we continue dividing.
solveq :: [a] -> [a] solveq p = p
Here we ‘solve’ the list if its length is 1. This is not really needed in quickSort, but again this is just to show that general idea of divAndConquer.
divideq :: Ord a => [a] -> [[a]] divideq  = [] -- If used on an empty List we return a list of empty lists divideq [x] = [[x]] -- If used on an List with only one element we return a list -- of a list with the single element divideq (x:xs) = [smaller,[x],bigger] --(x:xs) <- this splits up a list, --x beeing the first element, xs the rest where smaller = [y | y <- xs, y <= x] bigger = [y | y <- xs, y > x]
Now for the magic: If a list is not solvable we split it up in three parts. A list of elements which is smaller than the first element of the list, the element we use to split up the list itself and a list of bigger elements.
To construct the list of smaller and bigger elements we use a thing called list comprehension. Without going into too much detail, it is similar to the set-builder annotation used in math. You can read it like this: we build a list of y. But what is y? y is an element of xs with the added condition that y is smaller and equal or bigger then x.
combineq :: [a] -> [[a]] -> [a] combineq problem solutions = concat solutions
Here we take a all divided and solved solutions and glue them together using concat.
And that is all for Quicksort. Of course you can implement it simpler, but then the whole demonstration of Divide And Conquer and abstraction is missing.
Compare the subfunctions to the ones used in the Quicksort example. The function indb, as indq above, decides if we can solve the problem with solveb. If we can’t, we have to call divideb on our problem to split it up, try to solve it again (and so on…) and combineb the solutions together.
indb :: (Integer,Integer) -> Bool indb (n,k) | n == k = True | k == 0 = True | k == 1 = True | k == 2 = True | (k+1) == n = True | k > n = True | otherwise = False solveb :: (Integer,Integer) -> Integer solveb (n,k) | n == k = 1 | k == 0 = 1 | k == 1 = n | (k+1) == n = n | k == 2 = n*(n-1) `div` 2 | k > n = 0 | otherwise = error "not solvable" divideb :: (Integer,Integer) -> [(Integer,Integer)] divideb (n,k) = [(n-1,k-1),(n-1,k)] combineb :: (Integer,Integer) -> [Integer] -> Integer combineb _ solutions = sum solutions binom :: (Integer,Integer) -> Integer binom = divAndConquer indb solveb divideb combineb
As you can see, even though the goal of both algorithms is completly different they use the same structure and overall idea to solve their problems. And thanks to functional programming this abstract way of solving problems is easier to see and use than in imperative programming languages.
If you want to learn more about functional programming, check out learnyouahaskell.com which is a great ressource to start with haskell.
Please note that this implementation lacks a proper handling of symmetry (n,k) == (n,n-k). So it is by far not efficient to run it like this. ↩