Here's a program I wrote to convert numbers into English words using Haskell.
import Data.Char
import Data.List
type WordNum = String
ones :: (Integral a, Show a) => a -> WordNum
ones 1 = "one"
ones 2 = "two"
ones 3 = "three"
ones 4 = "four"
ones 5 = "five"
ones 6 = "six"
ones 7 = "seven"
ones 8 = "eight"
ones 9 = "nine"
ones n = error (show n ++ " is not a one-digit value")
teens :: (Integral a, Show a) => a -> WordNum
teens 10 = "ten"
teens 11 = "eleven"
teens 12 = "twelve"
teens 13 = "thirteen"
teens 14 = "fourteen"
teens 15 = "fifteen"
teens 16 = "sixteen"
teens 17 = "seventeen"
teens 18 = "eighteen"
teens 19 = "nineteen"
teens n = error (show n ++ " is not a teen")
tens :: (Integral a, Show a) => a -> WordNum
tens 1 = "ten"
tens 2 = "twenty"
tens 3 = "thirty"
tens 4 = "forty"
tens 5 = "fifty"
tens 6 = "sixty"
tens 7 = "seventy"
tens 8 = "eighty"
tens 9 = "ninety"
tens n = error (show n ++ " is not a tens place value")
groups :: [WordNum]
groups = ["", " thousand", " million", " billion", " trillion"]
groupToWord :: (Integral a, Show a) => a -> String
groupToWord n
| n == 0 = ""
| n < 10 = ones n
| n < 20 = teens n
| n < 100 = tens (n `div` 10) ++ ' ' : (groupToWord $ n `mod` 10)
| n < 1000 = ones (n `div` 100) ++ " hundred " ++ (groupToWord $ n `mod` 100)
| otherwise = error (show n ++ " is not a 3-digit group")
-- Splits a number into groups in reverse order
splitNum :: (Integral a, Show a) => a -> [a]
splitNum n
| n <= 999 = [n]
| otherwise = (n `mod` 1000) : splitNum (n `div` 1000)
numToWord :: (Integral a, Show a) => a -> String
numToWord n
| n == 0 = "zero"
| n >= 10^15 = error "Doesn't support numbers bigger than trillions"
| otherwise = concat $ intersperse ", " [w ++ g | (w,g) <- reverse (zip wordGroups groups)]
where
wordGroups = toWordGroups $ splitNum n
toWordGroups :: (Integral a, Show a) => [a] -> [WordNum]
toWordGroups (g:gs) = groupToWord g : toWordGroups gs
toWordGroups _ = []
It seems like there's a lot of redundancy, and I'm also not very happy with how I had to join the lists. Also, is there a way to do more consing and less appending? I was hoping to cons the groups back together after they were made words so I wouldn't have to reverse the zipped list afterward. Also, as always, I'd really appreciate general comments on improvements to style and best practice.
1 Answer 1
Pattern matching or list element selection?
It's possible to reduce the size of ones
, tens
, teens
and so on if we use !!
on a list instead of pattern matching. That's a completely other style though:
ones :: (Integral a, Ord a) => a -> WordNum
ones n
| n > 0 && n < 10 = onsies !! fromIntegral n
| otherwise = error $ "ones: not a one-digit value"
where
onsies = words "one two three four five six seven eight nine"
Note that I removed the Show a
constraint, because it's not necessary if our value n
is in our range. It's only necessary in the error
case. Instead, we should add where the error happened. Newer variants of GHC/base include a call-stack, therefore we will know the malfunctioning line, but it's always nice to know at least the function name. It also follows the base
-style error messages, e.g.
head (x:_) = x
head [] = error "Prelude.head: empty list"
Either way, back to the code. Several functions are partial. Usually, you want your exported functions to be total, e.g. they never return _|_
(an infinite loop, an error, undefined
,...). So instead of the ones
above, we could write
ones :: (Integral a, Ord a) => a -> Maybe WordNum
ones n
| n > 0 && n < 10 = Just $ onsies !! fromIntegral n
| otherwise = Nothing
where
onsies = words "one two three four five six seven eight nine"
However you wouldn't export all your helper functions either way, so lets keep the error
variant. By the way, I've used words "one..."
since my keyboard is slightly broken, but it's the same as ["one","two","three",...]
.
divMod
, map
and other standard library functions
Sometimes re-inventing the wheel is fun, but usually we want to try to get the most out of our standard library functions.
divMod
There are several occasions where we use both d = a `div` b
and m = a `mod` b
. If we use both, we can use (d,m) = a `divMod` b
. That prevents typos like
| n < 100 = tens (n `div` 10) ++ ' ' : (groupToWord $ n `mod` 11)
-- whoops ^
So let us use it in splitNum
:
splitNum :: Integral a => a -> [a]
splitNum n
| d == 0 = [n]
| otherwise = m : splitNum d
where
(d,m) = n `divMod` 1000
Note that quotRem
will return the same values as divMod
on positive numbers and is slightly faster.
zipWith
and map
You use zip
in your list comprehension just to ++
the zipped elements together. We can achieve this in a single step with zipWith
:
[w ++ g | (w,g) <- reverse (zip wordGroups groups)]
= [ w ++ g | (w,g) <- reverse (zipWith (,) wordGroups groups)]
= [ x | x <- reverse (zipWith (++) wordGroups groups)]
= reverse $ zipWith (++) wordGroups groups
toWordGroups
is map groupToWord
. If we use both functions in numToWord
, we end up with
numToWord :: (Integral a, Ord a) => a -> String
numToWord n
| n == 0 = "zero"
| n >= 10^15 = error "numToWord: Doesn't support numbers bigger than trillions"
| otherwise = concat $ intersperse ", " $ reverse $ zipWith (++) wordGroups groups
where
wordGroups = map groupToWord $ splitNum n