Cogito Ergo Sum.

我思う故に我あり

Haskell1か月

 Haskellの勉強を始めて約1か月が過ぎたのだが…、学習がなかなか捗らない。関数型プログラミング言語に関して全くの初学者を対象としたHaskell入門書を2冊購入したのだが、どちらも25〜45%読んだところでストップしてしまっている。その内の1冊なんかは200ページ足らずしかない薄い本で、まさか1か月経っても読み終えられないとは思ってもいなかった。まぁ、先に進むことよりも充分に理解することを重視して、同じところを2度も3度も読み返しているからなのだが…。

 関数型言語入門の定番の流れは、「式の評価」→「関数の定義」→「再帰関数」→「高階関数」みたいな感じで始まる。ここまでが前半。Haskell入門としてはここまでがウォーミングアップで、ここからがいよいよ本番。「型」や「型クラス」の定義だの「遅延評価」だの「モナド」だの「ファンクター」だのといった難しい話はここから始まる。

 ところが…、僕は「高階関数」の章辺りで学習スピードがガクンと落ちてしまった。「map」や「filter」はまだいいのだが、「fold」系辺りで躓いている。苦手の「再帰関数」の章辺りはワリとスムーズに進んだんだけどなぁ…。

 たいていのHaskell入門は「GHCi」なり「Hugs」なりのREPL環境を用いた学習を想定しているので、「標準入出力」や「ファイル入出力」の話は中盤以降にならないと出てこない(でも考えてみると、これらのトピックが中盤以降にならないと出てこないのは、命令型(手続き型)のプログラミング言語入門でも同じかもしれないな…)。しかも、それらの「副作用」を伴う処理を「純粋関数型言語」としての建前を崩さないように実現する仕組みはそれなりにヤヤコシイ。中盤の山場を超えないとその辺りの仕組みをスンナリとは理解できないので、1か月経つのに未だ「標準入出力」すら覚束ない。

 そんな中、僕の悪いクセで、ITエンジニアのための転職支援サービス(兼・プログラミング教育サービス)「Paiza」の「スキルチェック」問題の、最も易しいDランク問題を全てHaskellで解く!なんてことを始めてしまった。Dランク問題と言えば、「標準入出力」と「文字列と数値の相互変換」、それらのデータに対するちょっとした「演算」、最小限の「If文」が書ければ正解できるレベルで、普通の命令型(手続き型)プログラミング言語でなら、どれも1分以内で解けるような問題バカリである。

 2024年02月04日現在、出題中(掲載中)のDランク問題は全部で267問あり…、この1週間くらいでこれらの問題を全てHaskellで解き直してみた(各問題に対する「初チャレンジ」時にはHaskellでの解答は認められていないが、「再チャレンジ」の際にはHaskellも使える)。「そんなことやって意味あるの?」と問われると難しいところだが…、ほとんどの問題は簡単に解けたが中には手こずった問題もあり、まぁやって良かったと思う。自分はデフォルトで読み込まれるライブラリである「標準Prelude」で定義されている関数にすら精通していなかったことを痛感した。

 と言うワケで、今更ながら、「Haskell 98 言語とライブラリ 改訂レポート」の「標準Prelude」「List」「Char」「IO」辺りを見ている(ようやく見方が少しずつわかってきた(笑))。これまた僕の悪いクセで、古い「Haskell 98」の資料から見始めてしまったのだが…(買った本が古い本で、「Haskell 98」準拠だったため)。今から読むなら、「Haskell 2010 言語報告書」の「標準Prelude」「Data.List」「Data.Char」「System.IO」か。

 PaizaのDランク問題を解きまくっていて、やはりこれくらいは知っていた方が良かったと感じたのは次のような関数。

--標準入出力
 getLine, getContents, getChar
 print, putStrLn, putChar
 read, readLn, show
 mapM_

--数値(整数)処理
 (+), (-), (*), (^), div, mod, divMod
 abs, negate, subtract
 fromIntegral, round
 even, odd
 max, min
 sum, product, maximum, minimum

--タプル処理
 fst, snd
 zip

--リスト処理(兼、文字列処理)
 (!!), (:), (++)
 head, tail, init, last
 length, null
 reverse, sort, nub
 replicate
 map, filter
 all, any, and, or

--文字列処理(兼、リスト処理)
 words, unwords, lines, unlines
 concat, intersperse, intercalate
 span, break
 take, takeWhile
 drop, dropWhile
 elem, notElem
 isPrefixOf, isInfixOf, isSuffixOf

--文字処理
 isUpper, isLower, toUpper, toLower
 isDigit, digitToInt, intToDigit
 ord, chr

 あとは、「Data.Map」「Data.Array」辺りの関数を少し使えるようになれば、Cランク問題も全部解けるようになると思うんだけど…(Cランク問題は、命令型(手続き型)言語で言えば、入門書を1冊通して読んでいなくても解けるレベル)。

 と言うワケで、この1か月間の自分自身の成長を実感できずにいたのだけど、ほぼ1か月前に解いた「Atcoder Beginner Contest 002」の一番易しいA問題「正直者」をさっき再び解いてみて、2つの解答を見比べてみると…、歴然とした差が。

・2024年01月04日(木)の解答

main = do
    line <- getLine
    let arr = [read w :: Int | w <- (words line)]
    print(maximum arr)

・2024年02月06日(火)の解答

main = print . (+0). maximum . map read . words =<< getLine

 これが「良いコード」「洗練されたコード」かって言うと自信ないけど、少なくとも1か月前のコードの「野暮ったさ」は姿を消している。一応、成長はしてるのかなぁ…。