Eight years ago, I posted a description of an Alphametics Helper. I wasn’t much interested in the answers (as was evidenced by the fact it gave wrong answers, and I only just noticed) so much as the architecture – I wanted to implement a new Puzzle Solving Framework that later proved to be very versatile.
Today, I revisit Alphametics solvers, but not using the Framework – instead doing a relatively straight forward brute-force search for the answers.
This is in response to an article by Mark Dominus in the Universe of Discourse blog.
Mark demonstrates that the Alphametics search for SEND + MORE = MONEY (a classic example, that I also used in 2008) can be implemented fairly simple in Haskell:
import Control.Monad (guard) digits = [0..9] to_number = foldl (\a -> \b -> a*10 + b) 0 remove rs ls = foldl (\a -> \b -> remove' a b) ls rs where remove' ls x = filter (/= x) ls -- S E N D -- + M O R E -- --------- -- M O N E Y solutions = do s < - remove [0] digits e <- remove [s] digits n <- remove [s,e] digits d <- remove [s,e,n] digits let send = to_number [s,e,n,d] m <- remove [0,s,e,n,d] digits o <- remove [s,e,n,d,m] digits r <- remove [s,e,n,d,m,o] digits let more = to_number [m,o,r,e] y <- remove [s,e,n,d,m,o,r] digits let money = to_number [m,o,n,e,y] guard $ send + more == money return (send, more, money)
I emphasize that this code is copyright Mark Dominus, not me. I recommend reading his article to understand how it works.
Mark concludes his article:
It would be an interesting and pleasant exercise to try to implement the same underlying machinery in another language. I tried this in Perl once, and I found that although it worked perfectly well, between the lack of the do-notation’s syntactic sugar and Perl’s clumsy notation for lambda functions (sub { my ($s) = @_; … } instead of \s -> …) the result was completely unreadable and therefore unusable. However, I suspect it would be even worse in Python because of semantic limitations of that language. I would be interested to hear about this if anyone tries it.
This post is my response to that claim. I hope to show that there is a line-for-line equivalent piece of Python code that is not hampered by the semantic limitations of the language, and is of a similar aesthetic.
I am somewhat hampered in that I haven’t programmed in Haskell, so there is a risk I have missed some element of the aesthetic (although I have experience in Miranda, so functional languages and type-inference are familiar to me.)
Here we go. I have tried to stick as close as possible to the Haskell implementation:
digits = set(range(10)) def to_number(list_of_digits): return reduce( lambda x, y: 10*x+y, list_of_digits) def remove(banned_list, source_set): return source_set - set(banned_list) def solution(): for s in remove([0], digits): for e in remove([s], digits): for n in remove([s, e], digits): for d in remove([s, e, n], digits): send = to_number([s, e, n, d]) for m in remove([0, s, e, n, d], digits): for o in remove([s, e, n, d, m], digits): for r in remove([s, e, n, d, m, o], digits): more = to_number([m, o, r, e]) for y in remove([s, e, n, d, m, o, r], digits): money = to_number([m, o, n, e, y]) if send + more == money: return (send, more, money) print solution()
Unlike the Haskell version, this version requires indenting for each nested loop which may displease some people. I think I am used to it, because I welcome it. It required some extra line-breaks for the definitions, which meant it couldn’t be exactly line-by-line. There are probably more characters in the Python version, but that has always been a poor metric to optimise for.
I concern myself with types in a couple of places – the remove function deals with lists and sets and it is necessary to convert between them.
Because of my choice of iterating over sets rather than lists, this does not guarantee to search in increasing order for the digits – the order it searches is arbitrary, but that better satisfies the functional programmer in me. I am saying “go and check every possible value” rather than the imperative “here is how you look, one by one, through the digits”.
Despite this limitations, I believe this Python code to be of equivalent complexity and aesthetic to the original in Haskell. If you like one, I think you should like the other.
Comment by Julian on April 25, 2015
Mark Dominus tells me that, yes indeed, I have missed some element of the aesthetic, and that his use of monad lists instead of iteration was an important part of what he was demonstrating. Fair enough.
Comment by Michael Wilson on May 31, 2016
Hi,
I was browsing the internet and saw your article on an alphametic solver. Just thought you may be interested in the following Haskell solution. It’s not as concise as Mark’s but it is more generic. All but the comb routine are my own piece of work, but I’m relatively new to Haskell so watch out! Recently as well as this I’ve done a countdown (given list of numbers and total make a calculation using + – / *) and suko ‘brute force’ Haskell programs.
Mike
PS. Have previewed the code it seems you may need to insert spaces in some lines, especially around te where clauses.
-- alphametic.hs
-- Brute force alphametic solver
--
import Data.List (nub, permutations)
import Data.Maybe (fromJust)
-- LookupList is a simple dictionary/map between a letter (key) and the digit it represents
type LookupList = [(Char,Int)]
-- Return combinations of given list
-- E.g. comb 2 [1,2,3] == [[1,2],[1,3],[2,3]]
-- from http://rosettacode.org/wiki/Combinations#Dynamic_Programming_2
comb :: Int -> [a] -> [[a]]
comb m xs = combsBySize xs !! m
where
combsBySize = foldr f ([[]] : repeat [])
f x next = zipWith (++) (map (map (x:)) ([]:next)) next
--createLookup "abcd" [4,5,6,7] => [('a',4), ('b',5), ('c',6),('d',7)]
-- i.e. creates a map of letters(keys) and their associated values (digit)
createLookup :: String -> [Int] -> LookupList
createLookup = zip
-- Given a code (list of keys) and a lookupList returns the calculated value of the code
-- E.g. codeToInt "cda" [(a,1),(b,2),(c,3),(d,5)] => 351
codeToInt :: String -> LookupList -> Int
codeToInt code lookupList = foldl (\b a -> b*10 + a) 0 $ map charToInt code
where charToInt char = fromJust $ lookup char lookupList
-- Creates a list of lookupList for given string which contains unique letters (keys)
-- E.g. createlookupLists "abc" => [ [('a',0),('b',1),('c',2)], [('a',0),('b',1),('c',3)], ...]
-- Note that no attempt is made to disallow 0 values for any letter
createLookupLists letters = map (createLookup letters) combinations
where combinations = concatMap permutations $ comb (length letters) [0..9]
-- Returns true if given lookup is valid
-- A lookup is valid if none of the given keys are zero
lookupIsValid :: String -> LookupList -> Bool
lookupIsValid keys lookupList = all isValid keys
where isValid key = lookup key lookupList /= Just 0
-- Find all solutions to alphametic equation. E.g. for example,
-- "send" + "more" == "money"
-- would be written as
-- alphametic ["send", "more"] "money"
-- which will return
-- [[('m',1),('o',0),('n',6),('e',5),('y',2),('s',9),('d',7),('r',8)]]
alphametic :: [String] -> String -> [LookupList]
alphametic codes sumCode = let
keys = nub.concat $ sumCode : codes -- "sendmory"
keysThatCannotBeZero = nub $ map head (sumCode : codes) -- "sm"
sumOfCodes codes lookup = foldr (\code total -> total + codeToInt code lookup) 0 codes -- returns sum (of list of codes) using given lookup
in
-- return list of (valid) lookups where sum of codes == sum of sumCode
[lookup | lookup <- createLookupLists keys, -- create lookup
lookupIsValid keysThatCannotBeZero lookup, -- where lookup values for "s" and "m" are not zero, and...
sumOfCodes codes lookup == sumOfCodes [sumCode] lookup ] -- ...sum of codes == sum of target
solution1 = alphametic ["bad", "mad" ] "mama"
solution2 = alphametic ["i", "bb" ] "ill"
solution3 = alphametic ["send", "more"] "money"
answer = head solution3
send = codeToInt "send" answer
more = codeToInt "more" answer
money = codeToInt "money" answer
main :: IO()
main = do
print "Calculating"
print $ " send : " ++ (show send)
print $ " more : " ++ (show more)
print $ " ---- : -----"
print $ "total : " ++ (show (send + more))
print $ " ---- : -----"
print $ "money : " ++ (show money)