I retrain Haskell after a 10-year hiatus, partly to see what has changed, and partly as an antidote to days spent in C #, SQL, and JavaScript, and partly because of this, to suddenly cool down; -)
I decided to install the Hanoi Towers as an encoding kata, a fairly simple thing, but I already feel that my code is not idiomatic and would like to hear what kind of hints any old Haskellโs hands can have.
To make kata a little more interesting, I divided the problem into two parts: the first part, the moves
function, generates a sequence of moves necessary to solve the puzzle. The rest of the code is for modeling towers and completing moves.
One part that I definitely feel unhappy with the moveDisc
function would be tedious to spread over 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] AB = [as, a:b, c] moveDisc [a:as, b, c] AC = [as, b, a:c] moveDisc [a, b:bs, c] BA = [b:a, bs, c] moveDisc [a, b:bs, c] BC = [a, bs, b:c] moveDisc [a, b, c:cs] CA = [c:a, b, cs] moveDisc [a, b, c:cs] CB = [a, c:b, cs] moves :: Integer -> Column -> Column -> Column -> [(Column,Column)] moves 1 a _ c = [(a,c)] moves nabc = moves (n-1) acb ++ [(a,c)] ++ moves (n-1) bac solve :: Towers -> Towers solve towers = foldl (\t (from,to) -> moveDisc t from to) towers (moves len ABC) 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],[],[]] AB ~?= True , validMove [[2,3],[1],[]] AB ~?= False , validMove [[3],[],[1,2]] AC ~?= False , validMove [[],[],[1,2,3]] AC ~?= False , moveDisc [[1],[],[]] AB ~?= [[],[1],[]] , moveDisc [[],[1],[]] BC ~?= [[],[],[1]] , moveDisc [[1,2],[],[]] AB ~?= [[2],[1],[]] , moveDisc [[],[2],[1]] CB ~?= [[],[1,2],[]] , moveDisc [[1,2],[],[]] AC ~?= [[2],[],[1]] , moveDisc [[3],[2],[1]] BA ~?= [[2,3],[],[1]] , moves 1 ABC ~?= [(A,C)] , moves 2 ABC ~?= [(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 ABC) ~?= 7 ]
I look forward to hearing any comments or suggestions for improvement.