I'm relearning Haskell after a 10 year hiatus, partly to see what's changed and partly as an antidote to days spent in C#, SQL and JavaScript and partly as it's cool all of a sudden ;-)
I decided to set myself the Towers of Hanoi as a coding kata, simple enough stuff but I already feel that my code is non-idiomatic and would love to hear what hints and tips any Haskell old hands might have.
To make the kata slightly more interesting I split the problem into two parts, the first part, the function moves
, generates the sequence of moves required to solve the puzzle. The remainder of the code is designed to model the towers and execute the moves.
One part I definitely feel unhappy with is the moveDisc
function, this would be tedious to extend to 4 towers.
Hanoi.hs
module Hanoi
where
import Data.Maybe
type Disc = Integer
type Towers = [[Disc]]
data Column = A | B | C deriving (Eq,Show)
getDisc :: Towers -> Column -> Maybe Disc
getDisc t A = listToMaybe $ t !! 0
getDisc t B = listToMaybe $ t !! 1
getDisc t C = listToMaybe $ t !! 2
validMove :: Towers -> Column -> Column -> Bool
validMove tower from to
| srcDisc == Nothing = False
| destDisc == Nothing = True
| otherwise = srcDisc < destDisc
where srcDisc = getDisc tower from
destDisc = getDisc tower to
moveDisc :: Towers -> Column -> Column -> Towers
moveDisc [a:as, b, c] A B = [as, a:b, c]
moveDisc [a:as, b, c] A C = [as, b, a:c]
moveDisc [a, b:bs, c] B A = [b:a, bs, c]
moveDisc [a, b:bs, c] B C = [a, bs, b:c]
moveDisc [a, b, c:cs] C A = [c:a, b, cs]
moveDisc [a, b, c:cs] C B = [a, c:b, cs]
moves :: Integer -> Column -> Column -> Column -> [(Column,Column)]
moves 1 a _ c = [(a,c)]
moves n a b c = moves (n-1) a c b ++ [(a,c)] ++ moves (n-1) b a c
solve :: Towers -> Towers
solve towers = foldl (\t (from,to) -> moveDisc t from to) towers (moves len A B C)
where len = height towers
height :: Towers -> Integer
height (t:_) = toInteger $ length t
newGame :: Integer -> Towers
newGame n = [[1..n],[],[]]
TestHanoi.hs
module TestHanoi
where
import Test.HUnit
import Hanoi
main = runTestTT $ "Hanoi Tests" ~: TestList [
getDisc [[1],[2],[2]] A ~?= Just 1 ,
getDisc [[1],[2],[3]] B ~?= Just 2 ,
getDisc [[1],[2],[3]] C ~?= Just 3 ,
getDisc [[],[2],[3]] A ~?= Nothing ,
getDisc [[1,2,3],[],[]] A ~?= Just 1 ,
validMove [[1,2,3],[],[]] A B ~?= True ,
validMove [[2,3],[1],[]] A B ~?= False ,
validMove [[3],[],[1,2]] A C ~?= False ,
validMove [[],[],[1,2,3]] A C ~?= False ,
moveDisc [[1],[],[]] A B ~?= [[],[1],[]] ,
moveDisc [[],[1],[]] B C ~?= [[],[],[1]] ,
moveDisc [[1,2],[],[]] A B ~?= [[2],[1],[]] ,
moveDisc [[],[2],[1]] C B ~?= [[],[1,2],[]] ,
moveDisc [[1,2],[],[]] A C ~?= [[2],[],[1]] ,
moveDisc [[3],[2],[1]] B A ~?= [[2,3],[],[1]] ,
moves 1 A B C ~?= [(A,C)] ,
moves 2 A B C ~?= [(A,B),(A,C),(B,C)] ,
"acceptance test" ~:
solve [[1,2,3,4,5,6], [], []] ~?= [[],[],[1,2,3,4,5,6]] ,
"is optimal" ~:
length (moves 3 A B C) ~?= 7
]
I look forward to hearing any comments or suggestions for improvement.
` and friends.
– hammar May 05 '11 at 14:30