Haskellで100マス計算を解いてみた

Haskellでの100マス計算を以下のように実装しました。

module Try.Hyakumasu where

data MathData = MathData {col :: [Int], row :: [Int]} deriving (Show, Eq)

hyakumasu :: (Int -> Int -> Int) -> MathData -> [[Int]]
hyakumasu f x = [f c <$> (col x) | c <- (row x)]

printMath :: (Int -> Int -> Int) -> MathData -> IO ()
printMath f a = do
    mapM_ listPrint $ [[0] ++ col a] ++ zipWith (\a b -> [a] ++ b) (row a) (hyakumasu  f a )

listPrint :: [Int] -> IO ()
listPrint a = print $  concat $ map (\x ->  concat [" " | c <- [1..(4 - length (show x))]] ++ show x) a

各処理について

まず行と列の情報を持ったデータ型を以下のように定義しています。
data MathData = MathData {col :: [Int], row :: [Int]} deriving (Show, Eq)
例えば行が[1,2,3,4]で列が[5,6,7]のデータに対して計算をするのであれば以下のようにインスタンスを生成します。
MathData [1..4] [5..7]

MathDataのデータ型と(Int -> Int -> Int)型の関数を受け取ってマスごとの計算を行なっているのが以下の処理になります。

hyakumasu :: (Int -> Int -> Int) -> MathData -> [[Int]]
hyakumasu f x = [f c <$> (col x) | c <- (row x)]

関数の定義はhyakumasu :: (Int -> Int -> Int) -> MathData -> [[Int]]の部分で計算内容はhyakumasu f x = [f c <$> (col x) | c <- (row x)]になります。 for内包表記を使って各列の要素を順番に取り出し、<$> を使い行の配列の各要素に対して引数で渡した(Int -> Int -> Int)型の関数を適用しています。つまり、以下のようになっています。

Prelude> (+) 1 <$> [1..10]
[2,3,4,5,6,7,8,9,10,11]

計算結果の各行を表示するのには以下の関数を使っています。

listPrint :: [Int] -> IO ()
listPrint a = print $  concat $ map (\x ->  concat [" " | c <- [1..(4 - length (show x))]] ++ show x) a

(\x -> concat [" " | c <- [1..(4 - length (show x))]] ++ show x)の部分配列内の要素を4文字詰めで表示するための文字列に変換しています。map関数により各要素に適用した後concatでmap適用後の配列を連結し一つの文字列にしています。それからprintで画面への出力を行なっています。
画面への標準出力のため関数の定義はlistPrint :: [Int] -> IO ()で出力はIO ()で画面への標準出力を返すことを示しています。

計算結果全体の出力は以下のようになっています。

printMath :: (Int -> Int -> Int) -> MathData -> IO ()
printMath f a = do
    mapM_ listPrint $ [[0] ++ col a] ++ zipWith (\a b -> [a] ++ b) (row a) (hyakumasu  f a )

関数の定義はprintMath :: (Int -> Int -> Int) -> MathData -> IO ()となっていて引数として計算結果を渡すのではなく、計算に使う関数と行と列のデータMathDataをそのまま渡していてこれはMathDataの行と列が表示に必要のためですが計算結果がわかりづらいのでhyakumasuの結果とMathDataの行と列のデータを合わせたものを返す関数を別途用意した方が見通しはよくなりそうです。 zipWith (\a b -> [a] ++ b) (row a) (hyakumasu f a )については各行の計算結果の先頭に列の要素を連結しています。動きをみてみると以下のようになります。

Prelude> zipWith (\a b -> [a] ++ b) [1,2,3] [[1,2,3],[2,3,4],[5,6,7]]
[[1,1,2,3],[2,2,3,4],[3,5,6,7]]

ちなみにzipWithをつけない場合は外側の配列にそのまま適用されますが、これは欲しい結果ではありません。

Prelude> (\a b -> [a] ++ b) [1,2,3] [[1,2,3],[2,3,4],[5,6,7]]
[[1,2,3],[1,2,3],[2,3,4],[5,6,7]]

それから[[0] ++ col a] の部分はMathDataの行のデータに対して先頭に0を追加したものを返しています。mapM_ listPrint の部分についてlistPrintはIO ()を返しており、mapMはIOモナドに対するmap関数であるmapMを使用しています。 最終的な実行結果は以下のようになります。

main :: IO ()
main = do
    printMath (+) $ MathData [1..10] [1..10]
    printMath (*) $ MathData [1..10] [1..10]

"   0   1   2   3   4   5   6   7   8   9  10"
"   1   2   3   4   5   6   7   8   9  10  11"
"   2   3   4   5   6   7   8   9  10  11  12"
"   3   4   5   6   7   8   9  10  11  12  13"
"   4   5   6   7   8   9  10  11  12  13  14"
"   5   6   7   8   9  10  11  12  13  14  15"
"   6   7   8   9  10  11  12  13  14  15  16"
"   7   8   9  10  11  12  13  14  15  16  17"
"   8   9  10  11  12  13  14  15  16  17  18"
"   9  10  11  12  13  14  15  16  17  18  19"
"  10  11  12  13  14  15  16  17  18  19  20"
"   0   1   2   3   4   5   6   7   8   9  10"
"   1   1   2   3   4   5   6   7   8   9  10"
"   2   2   4   6   8  10  12  14  16  18  20"
"   3   3   6   9  12  15  18  21  24  27  30"
"   4   4   8  12  16  20  24  28  32  36  40"
"   5   5  10  15  20  25  30  35  40  45  50"
"   6   6  12  18  24  30  36  42  48  54  60"
"   7   7  14  21  28  35  42  49  56  63  70"
"   8   8  16  24  32  40  48  56  64  72  80"
"   9   9  18  27  36  45  54  63  72  81  90"
"  10  10  20  30  40  50  60  70  80  90 100"