LoginSignup
9
5

More than 5 years have passed since last update.

具体例で学ぶHaskell

Last updated at Posted at 2018-06-11

はじめに

前回 すごいH本が楽しかったのでまとめた という記事を書きました.
Haskellの勉強が楽しかったので,『プログラミングHaskell』を読みました.
その内容の簡単なまとめ記事です.

関数型パーサー

第8章の内容になります.
関数を組み合わせて便利なパーサーを構築する過程が面白かったので,題材として取り上げました.

パーサーの型

a 型のパーサーを,String 型を受け取り,Maybe (a, String) 型を返す関数として定義します.
失敗する場合を扱えるように Maybe を用いています.
Maybe の中身は,a 型の結果と消費されなかった残りの文字列のタプルになります.
定義は以下のようになります.

type Parser a = String -> Maybe (a, String)

基本的なパーサー

基本的なパーサーから始め,徐々に便利なパーサーを構築します.
a 型の値と入力文字列を受け取り,文字列を消費しないパーサーを返す関数 return' を定義します.

return' :: a -> Parser a
return' v s = Just (v, s)

入力文字列にかかわらず常に失敗する関数 failure を定義します.

failure :: Parser a
failure s = Nothing

入力文字列が空なら失敗し,そうでなければ最初の文字を消費する関数 item を定義します.

item :: Parser Char
item []     = Nothing
item (x:xs) = Just (x, xs)

それぞれの関数を試してみます.

ghci> return' 1 "abc"
Just (1,"abc")

ghci> failure "abc"
Nothing

ghci> item ""
Nothing

ghci> item "abc"
Just ('a',"bc")

連結と選択

まず,パーサーを連結させることを考えます.
入力文字列に1番目のパーサー p を適用し,失敗すれば全体も失敗します.
成功の場合は,1番目のパーサーの結果 v に関数 f を適用し,2番目のパーサを生成します.
この2番目のパーサーに1番目のパーサーの結果の残りの文字列 s' を渡すと最終的な結果が得られます.

(>>==) :: Parser a -> (a -> Parser b) -> Parser b
p >>== f = \s -> case p s of
    Nothing -> Nothing
    Just (v, s') -> f v s'    

文字を3つ消費し,1番目と3番目を組にして返してみます.

ghci> (item >>== \x -> item >>== \y -> item >>== \z -> return' (x, z)) "abcdef"
Just (('a','c'),"def")

次にパーサーの結果を選択することを考えます.
1番目のパーサーに入力文字列を渡し,成功したらその結果を,失敗したら2番目のパーサーを適用した結果を返します.

(+++) :: Parser a -> Parser a -> Parser a
p +++ q = \s -> case p s of
    Nothing -> q s
    Just (v, s') -> Just (v, s')

いくつか試してみます.

ghci> (item +++ return 'd') "abc"
Just ('a', "bc")

ghci> (failure +++ return 'd') "abc"
Just ('d', "abc")

ghci> (failure +++ failure) "abc"
Nothing

パーサーの部品

基本的なパーサーと,連結や選択を組み合わせると意味のあるパーサーを定義できます.
sat, string, many, many1 を見ていきます.

sat
述語 p を満足する一文字用のパーサー sat を以下のように定義します.

sat :: (Char -> Bool) -> Parser Char
sat p = item >>== \c -> if p c
    then return' c
    else failure

sat に色々な述語を渡すと,特定の文字に対するパーサーを定義できます.

import Data.Char

digit :: Parser Char
digit = sat isDigit

lower :: Parser Char
lower = sat isLower

alphanum :: Parser Char
alphanum = sat isAlphaNum

char :: Char -> Parser Char
char x = sat (== x)

試してみます.

ghci> digit "123"
Just ('1',"23")

ghci> digit "abc"
Nothing

ghci> char 'a' "abc"
Just ('a',"bc")

ghci> char 'a' "123"
Nothing

string
パーサー char を使ってパーサー string を定義します.
パーサー string は,引数の文字列がすべて消費される場合に限って成功します.

string :: String -> Parser String
string [] = return' []
string (x:xs) = char x >>== \_ -> string xs >>== \_ -> return' (x:xs)

Parser をモナドインスタンスとして宣言していれば,do記法で以下のように定義できます.

string :: String -> Parser String
string [] = return []
string (x:xs) = do
    char x
    string xs
    return (x:xs)

例を使って動作を追いかけてみます.
パーサー char で一文字目を消費し,成功なら Just を,失敗なら Nothing を返します.
Just の場合次のパーサー string に残りの文字列が渡されます.
string "abc" は,全てが成功した場合 Just ("abc", 残りの文字列) を返し,
どこかで失敗した場合 Nothing を返すパーサーです.

string "abc"
char 'a' >>== \_ -> string "bc" >>== \_ -> return' ('a':"bc")
-- 入力文字列の先頭が 'a' かつ string "bc" の戻り値が Just なら Just ("abc", 残りの文字列) を返す

string "bc"
char 'b' >>== \_ -> string "c" >>== \_ -> return' ('b':"c")
-- 入力文字列の先頭が 'b' かつ string "c" の戻り値が Just なら Just ("bc", 残りの文字列) を返す

string "c"
char 'c' >>== \_ -> string "" >>== \_ -> return' ('c':"")
-- 入力文字列の先頭が 'c' なら Just ("c", 残りの文字列)を返す

string ""
return' ""
-- Just ("", 入力文字列) を返す
ghci> string "abc" "abcdef"
Just ("abc", "def")

ghci> string "abc" "abdef"
Nothing

many, many1
manymany1 はパーサーを失敗するまで適用し,成功した結果をリストにして返します.
many はパーサーを0回以上,many1 はパーサーを1回以上適用します.

many :: Parser a -> Parser [a]
many p = many1 p +++ return' []

many1 :: Parser a -> Parser [a]
many1 p = p >>== \v -> many p >>== \vs -> return' (v:vs)

many は,+++ により return' を選択しているため,many1 が失敗しても Just を返します.
many1 は,述語 p を1回適用したのち,その結果に many を連結しています.

ghci> many digit "123abc"
Just ("123","abc")

ghci> many digit "abcdef"
Just ("","abcdef")

パーサー many, many1 を使ってパーサー ident, nat, space を定義します.
ident は識別子,nat は自然数,space は空白のパーサーです.

ident :: Parser String
ident = lower >>== \x -> many alphanum >>== \xs -> return' (x:xs)

nat :: Parser Int
nat = many1 digit >>== \xs -> return' (read xs)

space :: Parser ()
space = many (sat isSpace) >>== \_ -> return' ()

それぞれの関数を試してみます.

ghci> ident "abc def"
Just ("abc"," def")

ghci> nat "123 abc"
Just (123," abc")

ghci> space "  abc"
Just ((),"abc")

前後の空白を無視する token を定義し,ident, nat を強化します.

token :: Parser a -> Parser a
token p = space >>== \_ -> p >>== \v -> space >>== \_ -> return' v

identifier :: Parser String
identifier = token ident

natural :: Parser Int
natural = token nat

これもそれぞれ試してみます.

ghci> identifier "   abc def"
Just ("abc","def")

ghci> natural "  123  456  "
Just (123,"456  ")

切符番号遊び

11章の内容になります.
計算に使用できる自然数と四則演算を組み合わせて,目標の数になる計算式を作成するゲームです.
使用する自然数の個数は任意であり,使えるのはそれぞれ一回ずつです.
途中計算に出てくる数も自然数でなければならないという制約があります.
例えば,与えられた自然数が1, 3, 7, 10, 25, 50であり,目標の数を765とします.
解の一つは (1 + 50) * (25 - 10) になります.

(1 + 50) * (25 - 10)
= 51 * (25 - 10)
= 51 * 15
= 765

演算の定義

まず,四則演算子の型を定義します.
valid は重複を排除するために工夫をしています.

data Op = Add | Sub | Mul | Div deriving (Show)

-- 演算子を適用した結果が自然数となるか否かを返す
valid :: Op -> Int -> Int -> Bool
valid Add x y = x <= y -- x + y = y + x より x <= y の場合のみ計算すれば良い
valid Sub x y = x > y -- 結果は自然数でなければならないので x > y
valid Mul x y = x /= 1 && y /= 1 && x <= y -- 結果が変わらないので 1 を除く
valid Div x y = y /= 1 && x `mod` y == 0 -- 結果が変わらないので 1 を除く

-- 演算子を適用した結果を返す
apply :: Op -> Int -> Int -> Int
apply Add x y = x + y
apply Sub x y = x - y
apply Mul x y = x * y
apply Div x y = x `div` y

次に,数式の型を定義します.
数式は,自然数or二つの式への演算子適用を表します.
eval は空リストで失敗を表現します.

data Expr = Val Int | App Op Expr Expr deriving (Show)

-- 式の中の自然数をリストとして返す
values :: Expr -> [Int]
values (Val n) = [n]
values (App _ l r) = values l ++ values r

-- 式全体の値を返す
eval :: Expr -> [Int]
eval (Val n) = [n | n > 0]
eval (App o l r) = [apply o x y | x <- eval l, y <- eval r, valid o x y]

以下のように動きます.

-- (3 - 2) + (4 * 5)
ghci> values $ App Add (App Sub (Val 3) (Val 2)) (App Mul (Val 4) (Val 5))
[3,2,4,5]
ghci> eval $ App Add (App Sub (Val 3) (Val 2)) (App Mul (Val 4) (Val 5))
[21]

組み合わせ

組み合わせを扱う便利な関数を定義します.
これから紹介する関数はどれも感動するものばかりです.

subs
リストの部分リストを返します.
例えば,[1,2,3] に対する全ての部分リストは
[[],[1],[2],[3],[1,2],[1,3],[2,3],[1,2,3]] になります.
各要素を使うか否かなので,生成される部分リストは,2^(リストの長さ)パターンになります.
以下のように定義します.

subs :: [a] -> [[a]]
subs [] = [[]]
subs (x:xs) = ys ++ map (x:) ys
    where ys = subs xs

空リストを受け取った場合,ネストした空リストを返します.
そうでなければ,tail xs から生成される部分リスト ys と,
head xys の先頭に追加したリスト map (x:) ys を結合したものを返します.
動きを追って見てみます.

subs [1,2,3]
subs [2,3] ++ map (1:) subs [2,3] -- (1)

subs [2,3]
subs [3] ++ map (2:) subs [3] -- (2)

subs [3]
subs [] ++ map (3:) subs []
[[]] ++ map (3:) [[]]
[[],[3]] -- (3)

-- (2) へ (3) を渡す
subs [2,3]
[[],[3]] ++ map (2:) [[],[3]]
[[],[3],[2],[2,3]] -- (4)

-- (1) へ (4) を渡す
subs [1,2,3]
[[],[3],[2],[2,3]] ++ map (1:) [[],[3],[2],[2,3]]
[[],[3],[2],[2,3],[1],[1,3],[1,2],[1,2,3]]

interleave
リストに新たな要素を挿入したリストを返します.
例えば,新たな要素 1[2,3,4] に挿入した結果は,
[[1,2,3,4],[2,1,3,4],[2,3,1,4],[2,3,4,1]] になります.
以下のように定義します.

interleave :: a -> [a] -> [[a]]
interleave x [] = [[x]]
interleave x (y:ys) = (x:y:ys) : map (y:) (interleave x ys)

空リストに要素を追加した場合,追加要素のみのネストしたリストを返します.
そうでなければ,先頭に新たな要素 x を追加したリスト (x:y:ys) と,
リストのtail ys に新たな要素 x を追加したリスト interleave x ys の先頭に head y を追加したリストを
結合したものを返します.
動きを追って見てみます.

interleave 1 [2,3,4]
(1:2:[3,4]) : map (2:) (interleave 1 [3,4]) -- (1)

interleave 1 [3,4]
(1:3:[4]) : map (3:) (interleave 1 [4]) -- (2)

interleave 1 [4]
(1:4:[]) : map (4:) (interleave 1 [])
[1,4] : map (4:) [[1]]
[1,4] : [[4,1]]
[[1,4],[4,1]] -- (3)

-- (3) を (2) へ渡す
interleave 1 [3,4]
[1,3,4] : map (3:) [[1,4],[4,1]]
[1,3,4] : [[3,1,4],[3,4,1]]
[[1,3,4],[3,1,4],[3,4,1]] -- (4)

-- (4) を (1) へ渡す
interleave 1 [2,3,4]
[1,2,3,4] : map (2:) [[1,3,4],[3,1,4],[3,4,1]]
[1,2,3,4] : [[2,1,3,4],[2,3,1,4],[2,3,4,1]]
[[1,2,3,4],[2,1,3,4],[2,3,1,4],[2,3,4,1]]

perms
リストの要素の順列を返します.
例えば,[1,2,3] の順列は,
[[1,2,3],[2,1,3],[2,3,1],[1,3,2],[3,1,2],[3,2,1]] になります.
以下のように定義します.

perms :: [a] -> [[a]]
perms [] = [[]]
perms (x:xs) = concat $ map (interleave x) (perms xs)

空リストの場合,ネストした空リストを返します.
そうでなければ,tail xs の順列に interleave を適用してhead x を新たな要素として挿入します.
動きを追って見てみます.

perms [1,2,3]
concat $ map (interleave 1) (perms [2,3]) -- (1)

perms [2,3]
concat $ map (interleave 2) (perms [3]) -- (2)

perms [3]
concat $ map (interleave 3) (perms [])
concat $ map (interleave 3) [[]]
concat $ [[[3]]]
[[3]] -- (3)

-- (3) を (2) へ渡す
perms [2,3]
concat $ map (interleave 2) [[3]]
concat $ [[[2,3],[3,2]]]
[[2,3],[3,2]] -- (4)

-- (4) を (1) へ渡す
perms [1,2,3]
concat $ map (interleave 1) [[2,3],[3,2]]
concat $ [[[1,2,3],[2,1,3],[2,3,1]],[[1,3,2],[3,1,2],[3,2,1]]]
[[1,2,3],[2,1,3],[2,3,1],[1,3,2],[3,1,2],[3,2,1]]

choices
リストから選択肢を返します.
上記の素晴らしい関数を適用していくだけです.
subs により生成された部分リストに map perms で順列を取り,
最後に concat して平らにしています.

choices :: [a] -> [[a]]
choices xs = concat $ map perms $ subs xs

split
リストを空でない二つのリストに分割して返します.
例えば,[1,2,3,4] を分割した結果は,[([1],[2,3,4]),([1,2],[3,4]),([1,2,3],[4])] になります.
以下のように定義します.

split :: [a] -> [([a], [a])]
split [] = []
split [_] = []
split (x:xs) = ([x], xs) : [(x:ls, rs) | (ls, rs) <- split xs]

空リストor要素数1のリストの場合,空リストを返します.
そうでなければ,([x], xs) なるタプルとtail xs を分割した結果の左要素のリストの先頭に head x を加えたものを
結合して返します.
動きを追って見てみます.

split [1,2,3,4]
([1], [2,3,4]) : [(1:ls, rs) | (ls, rs) <- split [2,3,4]] -- (1)

split [2,3,4]
([2], [3,4]) : [(2:ls, rs) | (ls, rs) <- split [3,4]] -- (2)

split [3,4]
([3], [4]) : [(3:ls, rs) | (ls, rs) <- split [4]]
[([3], [4])] -- (3)

-- (2) へ (3) を渡す
split [2,3,4]
([2], [3,4]) : [(2:ls, rs) | (ls, rs) <- [([3], [4])]]
([2], [3,4]) : [([2,3],[4])]
[([2],[3,4]),([2,3],[4])] -- (4)

-- (1) へ (4) を渡す
split [1,2,3,4]
([1], [2,3,4]) : [(1:ls, rs) | (ls, rs) <- [([2],[3,4]),([2,3],[4])]]
([1], [2,3,4]) : [([1,2],[3,4]),([1,2,3],[4])]
[([1],[2,3,4]),([1,2],[3,4]),([1,2,3],[4])]

問題を解く

組み合わせ関数を使って切符番号遊びを解きます.
まず,式と式全体を評価した値の組を表す型を定義します.

type Result = (Expr, Int)

-- 2つの Result を受け取り,結果が自然数となる四則演算の結果 [Result] を返す
combine :: Result -> Result -> [Result]
combine (l, x) (r, y) = [(App o l r, apply o x y) | o <- [Add, Sub, Mul, Div], valid o x y]

-- 自然数のリストを受け取り,それぞれの値が一回だけ使われている全ての式を返す
results :: [Int] -> [Result]
results [] = []
results [n] = [(Val n, n) | n > 0]
results ns = [res | (ls, rs) <- split ns,
    lx <- results ls,
    ry <- results rs,
    res <- combine lx ry]

切符番号遊びを満たす全ての式を返す関数 solutions を定義できます.
results は与えられた自然数リストの値が一回だけ使われる全ての式を返すので,
choices によって ns から選択肢 ns' を生成し,ns'results に渡します.
得られた値 m が目標となる値 n と等しくなるときの式 e が最終的な結果となります.

solutions :: [Int] -> Int -> [Expr]
solutions ns n = [e | ns' <- choices ns,
    (e, m) <- results ns',
    m == n]

おわりに

パーサーと切符番号遊びを具体例にHaskellを学ぶことができました.

9
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
5