I am working on a Haskell problem from exercism, which is a learn-to-code website.
The problem statement is as follows.
Write a function to solve alphametics puzzles.
Alphametics is a puzzle where letters in words are replaced with numbers.
For example
SEND + MORE = MONEY
:S E N D M O R E + ----------- M O N E Y
Replacing these with valid numbers gives:
9 5 6 7 1 0 8 5 + ----------- 1 0 6 5 2
This is correct because every letter is replaced by a different number and the words, translated into numbers, then make a valid sum.
Each letter must represent a different digit, and the leading digit of a multi-digit number must not be zero.
Write a function to solve alphametics puzzles.
You can solve this exercise with a brute force algorithm, but this will possibly have a poor runtime performance. Try to find a more sophisticated solution. Hint: You could try the column-wise addition algorithm that is usually taught in high school.
Here is my WIP solution. I would like to know how to shorten this up. I feel it's unnecessarily verbose.
module Alphametics (solve) where
import Data.List ((\\))
import Data.List.NonEmpty (NonEmpty)
import qualified Data.Char as C
import qualified Data.List as L
import qualified Data.List.NonEmpty as NE
import qualified Data.Maybe as MB
solve' :: NonEmpty String -> Maybe [(Char, Int)]
solve' terms = findFirstSolution . zipLettersAndDigits . filterLeadingZeroes . L.permutations $ [0 .. 9]
where
initials = L.nub . MB.catMaybes . NE.toList . NE.map MB.listToMaybe $ terms
nonInitials = L.nub (concat terms) \\ initials
letters = initials ++ nonInitials
n = length initials
findFirstSolution = L.find (`solves` terms)
zipLettersAndDigits = map (zip letters)
filterLeadingZeroes = filter noZerosPriorTo
noZerosPriorTo p = 0 `notElem` take n p
solve :: String -> Maybe [(Char, Int)]
solve puzzle = solve' =<< NE.nonEmpty terms
where
terms = filter (all C.isUpper) . words $ puzzle
solves :: [(Char, Int)] -> NonEmpty String -> Bool
solves cmap terms = (sum <$> mapM termToInt (NE.init terms)) == termToInt (NE.last terms)
where
termToInt :: [Char] -> Maybe Int
termToInt xs = fromDigits <$> mapM (`lookup` cmap) xs
fromDigits :: [Int] -> Int
fromDigits = L.foldl' (\n x -> n * 10 + x) 0
In particular, I'd like to avoid splitting the solve
function into solve
and solve'
, but I don't know how to inline solve'
without just sticking it in a where
clause, which isn't really any nicer. Seems like there should be a way to perform this bind without a separate function. Also, just general advice would be lovely, thanks!