Built-ins
The function showTermList applies show to each item in the list (map show) and puts , [COMMA] between any two items of the list (intercalate ","). You can just use this predefined functions to write:
showTermList = intercalate "," . map show
Also in tokens:
| isSpace c = tokens cs -- eat all whitespace
Eating all white-space is better expressed as filter (not . isSpace).
tokens :: String -> [Token]
tokens = tokens' . filter (not . isSpace)
where
tokens' [] = []
tokens' (c:cs) = let (token, cs') = nextToken (c : cs)
in token : tokens cs'
Repetition
You have the repetition of (rule, in your function:
parseRule :: [Token] -> (Rule, [Token])
parseRule [] = error "No tokens to parse"
parseRule ts =
if null ts' || token (head ts') /= dot then
(rule, ts')
else
(rule, tail ts')
where (rule, ts') = parseRule' ts
You could avoid it by assigning a tail variable:
parseRule ts = (rule, tail)
where
(rule, ts') = parseRule' ts
tail = if null ts' || token (head ts') /= dot then ts' else tail ts'
Guards are even more visually immediate then if else:
parseRule ts = (rule, decideTail ts')
where
(rule, ts') = parseRule' ts
decideTail ts'
| null ts' || token (head ts') /= dot = ts'
| otherwise = tail ts'
Now we note that:
(rule, decideTail ts')
where
(rule, ts') = parseRule' ts
Is built-in under the name of mapSnd
parseRule ts = mapSnd decideTail $ parseRule' ts
where
decideTail ts'
| null ts' || token (head ts') /= dot = ts'
| otherwise = tail ts'
Or even pointfree:
parseRule = mapSnd decideTail . parseRule'
where
decideTail ts'
| null ts' || token (head ts') /= dot = ts'
| otherwise = tail ts'