Like I wrote in my earlier post, I almost always work top down. So,
to take up your challenge, I will start with a top-level outline:
read in the inputs, process them somehow, and then show the results.
Since I don't know what it means (yet) to perform each of those tasks,
I will use simple placeholders like id to represent each
stage of the outline:
module Main where
main = interact $ showOutput . process . readInput
showOutput = id
process = id
readInput = id
This is legal Haskell that compiles and executes. It doesn't do much,
however, merely echoing its input:
$ echo alpha beta | ghc -e main WordIndex.hs
alpha beta
Still, it shows how easy it is to start sketching a solution at a high
level.
Next, working down, I will flesh out the outline. I won't
worry about reading input from files or writing output to an index just
yet. For now, I will focus on the core word-counting problem, adding
logic to do just that:
module Main where
import qualified Data.Map as Map
main = interact $ showOutput . process . readInput
showOutput = unlines . Map.foldWithKey show1 []
process = foldl accumulate Map.empty
readInput = words
show1 key count = (unwords [key, show count] :)
accumulate hash key = Map.insertWith (+) key 1 hash
Notice that I did not need to alter the outline. All I did
was add low-level detail to it. Now I have counts:
$ echo alpha beta | ghc -e main WordIndex.hs
alpha 1
beta 1
Continuing to refine the lower-level functionality, I will return
to the I/O tasks. Like before, I will add lower-level functions,
leaving the top-level outline largely unchanged:
module Main (main) where
import Control.Monad
import qualified Data.Map as Map
import System.Environment
main = control $ showOutput . process . readInput
showOutput = unlines . Map.foldWithKey show1 []
process = foldl accumulate Map.empty
readInput = words
show1 key count = (unwords [key, show count] :)
accumulate hash key = Map.insertWith (+) key 1 hash
control f = readFiles >>= writeIndex . f
readFiles = getArgs >>= liftM concat . mapM readFile
writeIndex = writeFile "words.index"
Let me recap. I started with a top-level, executable outline and
worked down from there. With two simple iterations I was able to add
the counting functionality and then the I/O functionality. Now I have
a complete solution that is largely equivalent to your code (ignoring details that don't matter for our top-down coding
discussion).
As a demonstration, I will compile the code and use it to index itself:
$ ghc -O2 --make -o WordIndex WordIndex.hs
$ ./WordIndex *.hs
$ cat words.index
"words.index" 1
$ 1
(+) 1
(main) 1
(unwords 1
. 5
1 1
:) 1
= 9
>>= 2
Control.Monad 1
Data.Map 1
Main 1
Map 1
Map.empty 1
...
There you have it: top-down functional programming, or at least the
way I do it in Haskell. I hope you will trust me that it was quick and
easy. (It took much longer to write this explanation than the code.)
Cheers, Tom
|