スタイル別 Haskell コードの読み方
命令スタイルと関数スタイル
先日、こんな tweet があったので、func1
の定義で、func2
を使うはずだったのを展開してしまう(func2
の定義は不要になってしまう)ミスがふくまれています。筆者としては、
module Main where
main :: IO ()
main = putStrLn =<< join (func1 =<< getLine)
func1 :: String -> IO (IO String)
func1 = (func2 <$>) . (<$> getLine) . (++)
func2 :: String -> IO String
func2 = (<$> getLine) . flip (++)
というつもりでした。
それはさておき、読むのが難しい理由は以下のたぶん2つ。
-
do
構文を使っていない関数スタイルである- 命令の順次実行という制御に沿った記述になっていない
- ポイントフリースタイルである
- 関数の定義が、関数適用の有り様を示した記述になっていない
それぞれのスタイルの Haskell のコードを読んでみましょう。
命令スタイルの Haskell コードの読み方
もとのコードは解説の必要のないくらいシンプルなものですが、読んでみましょう。
もとのコードは do
構文を使った命令スタイルで書かれていますので、命令スタイル風に読むことにします。
まず、func2
func2 x2 = do
e <- getLine -- 1.
return (e ++ x2) -- 2.
func2
は標準入力から読み込んだ文字列を、引数で与えた文字列の前に連結してできた文字列を結果とする手続き。分解すると
- 標準入力から1行読む手続き
getLine
の呼び出し結果をとりだしe
に格納する。 -
e
の中身とx2
の中身を連結した文字列を結果とする。
となっています。次に func1
func1 x = do
d <- getLine -- 1.
return (func2 (x ++ d)) -- 2.
func1
は標準入力から読み込んだ文字列に、引数で与えた文字列を連結したものを、func2
に渡して、できた手続きを返す手続き。分解すると
- 手続き
getLine
の呼び出し結果をとりだしd
に格納する。 - 引数
x
の中身とd
の中身を連結したものをfunc2
に渡し、文字列を返す手続きを作成し、その手続きを結果とする。
となります。そして、最後 main
main :: IO ()
main = do
a <- getLine -- 1.
b <- func1 a -- 2.
c <- b -- 3.
putStrLn c -- 4.
- 手続き
getLine
の呼び出し結果をとりだしa
に格納する -
func1 a
の呼び出し結果(文字列を返す手続き)をとりだしb
に格納する -
b
の中身の手続きの呼び出し結果(文字列)をc
に格納する -
c
の中身をputStrLn
に渡して印字する
これらの命令的手続きが、関数的に表現されるとどうなるでしょう。
do
構文を使わない関数スタイルの Haskell コードの読み方
do
構文を使わない関数スタイルのコードは以下のようになります。
まず、func2
は文字列(String
)から実行時文字列[1](IO String
)への関数です。
func2 x2 :: String -> IO String
func2 :: String -> IO String
func2 x2 = (++ x2) <$> getLine
-- ~~~~~~~ ~~~ ~~~~~~~
-- 3 2 1
- 標準入力から読み込む実行時文字列(型
IO String
) - 関数
String -> String
をIO String -> IO String
に変換してから、実行時文字列IO String
に適用する拡張適用演算子[2] -
x2
を後ろへ連結する関数(型String -> String
)
次に func1
は文字列(String
)から実行時·実行時文字列(IO (IO String)
)への関数です。
func1 :: String -> IO (IO String)
func1 x = func2 <$> ((x ++) <$> getLine)
-- ~~~~~ ~~~ ~~~~~~~~~~~~~~~~~~~~
-- 3 2 1
-
x
に標準入力から読み込んだ文字列を連結した実行時文字列(IO String
) - 関数
String -> IO String
をIO String -> IO (IO String)
に変換してから、実行時文字列IO String
に適用する拡張適用演算子[2:1] - 文字列を
func2
で実行文字列へ変換し、それをreturn
で実行時·実行時文字列にする関数
最後に main :: IO ()
です。
main :: IO ()
main = (putStrLn =<<) =<< (func1 =<< getLine)
-- ~~~~~~~~~~~~~~ ~~~ ~~~~~~~~~~~~~~~~~~~
-- 3 2 1
- 標準入力から読み込む実行時文字列に
func1
を拡張適用して生成した実行時·実行時文字列(IO (IO String)
) - 関数
IO String -> IO ()
をIO (IO String) -> IO ()
に変換してから、実行時·実行時文字列に適用する拡張適用演算子[3] - 実行時文字列(
IO String
)の内容を標準出力に置くコマンド(実行時ユニットIO ()
)に変換する関数
do
構文を使っていませんが、それほど読むのに苦労はないように見えますね。もちろん、もともとが単純な逐次構造だからですが、do
構文を使わないからといって、必ずしも読み難くなるわけではないようです。
ポイントフリースタイルの Haskell コードの読み方
関数の定義のスタイルは2つに大別されます。
- 関数抽象による定義(関数適用の結果を定義)「
f x = 10 * x + 3
f
をx
に適用したものは、10 * x + 3
である」
このスタイルをポイントフリースタイルに対して、ポイントワイズスタイルと呼びます。 - 関数合成による定義(関数を別の関数を組み合わせとして定義)「
f = (+ 3) . (10 *)
f
は、(+ 3) . (10 *)
である」
ポイントワイズスタイルの定義は、機械的にポイントフリースタイルに書き直せます。
f x = 式
のようなポイントワイズスタイルの定義において、右辺の式を g $ h $ x
という形にできれば、あとは、以下のような書き換えするだけです。
f x = g $ h $ x
≡ { $ の定義 }
f x = g (h x)
≡ { . の定義 }
f x = (g . h) x
≡ { η 変換 }
f = g . h
ポイントワイズスタイルの定義を読むときは、f = g . h
を f x = g $ h $ x
のように引数を補って、右側から読むと意味を掴みやすいでしょう。
func2
のポイントフリースタイルの定義を読んでみましょう。
func2 :: String -> IO String
func2 = (=<< getLine) . flip (++)
flip
は以下のように定義された Prelude
関数です。
flip :: (a -> b -> c) -> (b -> a -> c)
flip f x y = f y x
したがって、引数 x2
を補って以下のように読めます。
(=<< getLine) $ flip (++) $ x2
= { $ を適用 }
(=<< getLine) $ (++ xs)
= { $ を適用 }
(++ xs) =<< getLine
関数スタイルのところで読んだ形になりましたね。
つぎは、 func1
のポイントフリースタイルの定義を読んでみましょう。
func1 :: String -> IO (IO String)
func1 = (func2 <$>) . (<$> getLine) . (++)
引数 x
を補って読みましょう
(func2 <$>) $ (<$> getLine) $ (++) $ x
= { $ を適用 }
(func2 <$>) $ (<$> getLine) $ (x ++)
= { $ を適用 }
(func2 <$>) $ (x ++) <$> getLine
= { $ を適用 }
func2 <$> ((x ++) <$> getLine)
こちらも関数スタイルのところで読んだ形になりましたね。
最後に main
。これは関数ではありませんが、関数スタイルのところと少し違いがありますので、読んでおきましょう。
main :: IO ()
main = putStrLn =<< join (func1 =<< getLine)
ここでは、join
を使って、実行時·実行時文字列 IO (IO String)
を実行時文字列 IO String
に変換しています。join
は以下のように定義されています。
join :: Monad m => m (m a) -> m a
join x = id =<< x
これを展開すると以下のように読めます。
putStrLn =<< join (func1 =<< getLine)
= { join を適用 }
putStrLn =<< (id =<< (func1 =<< getLine))
= { f =<< (id =<< x) ≡ (f =<<) =<< x }
(putStrLn =<<) =<< (func1 =<< getLine)
これで、関数スタイルのところで読んだ形になりましたね。
-
IO
のことを「実行時」ということにします。もちろん、一般的ではなく筆者独自の勝手造語です。 ↩︎ -
<$>
は実際は、多相演算子で、型シグネチャは、(<$>) :: Functor f => (a -> b) -> (f a -> f b)
です。「拡張適用演算子」も一般的な用語ではなく筆者の造語(オレオレ用語)です。初出は以下です。 https://github.com/nobsun/hday2019/blob/master/doc/ftype.pdf ↩︎ ↩︎ -
=<<
は実際は、多相演算子で、型シグネチャは、(=<<) :: Monad m => (a -> m b) -> (m a -> m b)
です。 ↩︎
Discussion