Array Processing Design Recipes
There are multiple ways to process arrays. In a language
like Haskell, where recursion is the main control mechanism, there are
two ways that you should be familar with.
Linear, item by item, processing.
- Scan over the elements from lowest to highest.
- Write a pair of functions.
- A wrapper function that computes the bounds of the array, and then calls the worker with the bounds as arguments.
- A worker function that recurses over the the two bounds.
- The base case is when the lower bound becomes larger than the upper bound.
- A step case where the lower bound is less than or equal to the upper bound. This case consists of at least two actions
- Perform some action based upon the lower bound
- Make a recursive call which increments the lower bound but which leaves the upper bound unchanged.
- The wrapper function usually has 2 or three actions.
- Compute the bounds of the array (and possibly other initialization actions).
- Calls the workewr function
- Performs some clean up actions
Example linear scan. Printing each element of an array.
printEach :: Show a => Array a -> IO () -- write a contract
printEach a = -- define the wrapper
do { (low,high) <- boundsArr a -- compute the bounds
; worker (low,high) a -- call the worker
; return () -- clean up
}
worker (low,high) a | low > high = return () -- base case
worker (low,high) a = -- step case has 2 actions
do { item <- readArr a low -- 1) an action based upon low
; print item
; worker (low+1,high) a -- 2) make a recursive call
}
Index pre-computation, and scan over index list.
- The design recipe has two parts
- Index calculation, where we compute a list of indexes.
- Index scanning, where we scan over the list performing an action for each index.
- Each part can correspond to either a separate function, or separate actions. This is the designers choice.
- The two parts are then sequenced by using a "do" command.
- Note that index calculation can often be wriiten with a simple list creating operation, like a sequence or comprehension.
- Scanning is just a simple recursive function over the structure of lists
- Like many such list functions it has two cases
- When the index list is empty
- When the index list has the shape (x:xs). In this case
perform an action and make a recursive call.
An example of index precalculation
revIndexes :: (Int,Int) -> [(Int,Int)]
revIndexes (low,high) = zip countup countdown -- Part 1, index precalculation
where n = high - low + 1
half = n `div` 2
countup = [low..half]
countdown = [high, high -1 .. low]
rev5 :: Array a -> IO ()
rev5 a =
do { (low,high) <- boundsArr a
; let scan :: [(Int,Int)] -> IO () -- Part 2, defining scanning
scan [] = return () -- base case of scan
scan ((i,j):xs) = -- step with 2 actions
do { swap a (i,j); scan xs }
; scan (revIndexes (low,high)) -- calling the scan step
}
Back to the Daily Record.
Back to the class web-page.