新年明けて、「何か新しいことを始めてみたい!」と思い、前から学んでみたかった関数型プログラミング言語Haskellの学習を始めた。
Haskellの名を初めて知ったのは15年くらい前かなぁ…? 10年くらい前の「関数型言語ブーム」の頃から興味はあったのだが…、Haskellは「関数型言語のラスボス」的な紹介をされることが多く、正直ビビっていた。その頃は日本語で書かれた入門書も2〜3冊しかなかった。
それが、去年サロマを完走して短期的な目標を見失い、次の目標を探していた時に、本屋で偶然『すごいHaskellたのしく学ぼう!』(Lipovača(著) 田中・村主(訳) 2012年 オーム社)を見かけ、思っていたより小さな判型サイズの本だと知り、「満を持して(?)Haskellでも始めてみようかな!」と思ったのだが…、元来行動力のない当方、無為に半年が経ってしまった。
相変わらずのケチケチ作戦で、『すごいHaskellたのしく学ぼう!』ではなく、とにかく古本価格の安い本、『プログラミングHaskell』(Hutton(著) 山本(訳) 2009年 オーム社)をアマゾンのマーケットプレースで購入。古い本だから相場が下がっているのかと思っていたら、他社から『プログラミングHaskell(第2版)』(Hutton(著) 山本(訳) 2019年 ラムダノート)が出ていた! 表紙に見覚えはあったが、原著の書影だと思っていた(翻訳本に見えなかった)。まぁ、Haskellは本当に初めてなので、基礎の基礎から。古い本でいいだろう。
年末に注文して、届いたのは年明け。風邪で具合が悪かったので、本を読んで過ごした。関数型プログラミング言語には、命令型(手続き型)プログラミング言語とは異なる、王道の「説明の順序」がある(LISP、Scheme、OCaml、F#、そして、Haskell、だいたいどれも話の流れは同じ)。それが、命令型言語経験者にとって、「関数型言語は難しい」という印象を抱かせる一因となっているように思う。つまり、関数型言語には関数型言語なりの学び方があるのに、つい命令型言語の学び方で学ぼうとしてしまって、余計な苦労をしてしまうのだ。「関数型言語の説明の順序」に身を任せてしまった方がいい。
また、それなりに命令型言語ができるだけに、「関数型言語で、命令型言語的なプログラムを書くにはどうすればいいのだろう?」などとつい考えてしまう。それがいけない。
例えば、「FizzBuzz」。これは命令型言語の例題としては、条件分岐と繰り返し、数値の計算、数値から文字列への変換、等々を含んでおり、それなりに適切なのかもしれないが…、関数型言語を学ぶ際には、あまり適切な例題ではないと思う。関数型言語の最初の例としては、「ピタゴラス数を求める」とかの方がいいんじゃないのかな(それがまた「数学嫌いのプログラマ」に「関数型言語はとっつきにくい」という印象を抱かせる結果になってしまうのだが(笑))。
Prelude> n = 20
Prelude> [(x, y, z) | x<-[1..n], y<-[x..n], z<-[y..n], x * x + y * y == z * z]
[(3,4,5),(5,12,13),(6,8,10),(8,15,17),(9,12,15),(12,16,20)]
こんなヤツ(「n = 20」を「n = 1000」としてやってみたら、トンデモなく処理に時間がかかった(冗談じゃなく、10分以上かかった!)から、本物のHaskellerはこんなコードは書かないんだろうけど)。
と言うワケで、Haskellで「FizzBuzz」をやってみようとして、大いに手こずった。と言うか、今もまだ手こずり中…(涙)。
・1日目のFizzBuzz
main = loop [1..100]
loop (x:[]) = putStrLn(fizzBuzz x)
loop (x:xs) = do
putStrLn(fizzBuzz(x))
loop(xs)
fizzBuzz x
| x `mod` 15 == 0 = "FizzBuzz"
| x `mod` 5 == 0 = "Buzz"
| x `mod` 3 == 0 = "Fizz"
| otherwise = show x
「LISPやSchemeならどうするか?」と考えて、再帰でループ処理を行っているものの、そもそも「ループ処理を行う」というのが「命令型の発想」なのかもしれないな…。
・2日目のFizzBuzz(「FizzBuzz」関数はさっきと同じ(以下、同様)。)
main = putStrLn(concat [fizzBuzz x ++ ['\n']| x <- [1..100]])
Haskellの文字列の実体は「文字のリスト」に過ぎないということと、リスト内のリストを連結する「concat」という関数を知り、無理やり1行の文字列にしてしまったのだが…、どうだろう?
・3日目のFizzBuzz
main = putStr(unlines [fizzBuzz x | x <- [1..100]])
文字列のリストを受け取り、各要素の末尾に「改行文字」を追加した上で1つの文字列に連結してしまう「unlines」という関数を知った(「要素と要素の間に改行文字を挟む」のではなく、最後の要素の後にも改行文字が付加されてしまうようだ)。類似の関数として「unwords」も知った(「要素と要素の間にスペースを挟んだ上で1つの文字列に連結する」。最後の要素の後にスペースは付加されない)。
・4日目のFizzBuzz
main = mapM_ putStrLn [fizzBuzz n| n <- [1..100]]
「map」系の関数「mapM_」なる関数を使えば、リストの文字列を1つずつ出力できるっぽいことを知った。「M」は悪名高き(?)「Monad」の「M」か?
・5日目のFizzBuzz
main = mapM_ (putStrLn.fizzBuzz) [1..100]
関数を合成する「.」演算子を知り、早速活用。でも、「1行出力」の関数と「数値のFizzBuzz文字列化」の関数を合成する、ってのはどうも妙な気がするな…。
さあ、明日以降どこまで進化が続くかな!?