Converting numbers to words in Haskell

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.

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