すごいHaskellたのしく学ぼう を読んだ

ゴールデンウィークに時間ができたので,言わずと知れた名著「すごいHaskellたのしく学ぼう」を読みました. 簡単に振り返ってみます.

どんな本か

Haskellの素晴らしい入門書.全人類読むべき. Haskellのサンプルコードを通して,Haskellの言語仕様と関数型プログラミングの概念について学び,Haskellに備わっている標準関数や型を実装したり,具体的な問題を解いていきます.

冗談を交えながら軽い語り口調で進めていくのですが,実はかなり難しいです. この記事では,この本の書評だけでなく,私が重要だと感じた部分,難しいと感じた部分をちょこっと解説するので,この本に興味がある人や読み始めたけど詰まってしまった人は読んでみてください. また,私自身Haskell初心者なので,間違っているところがあれば教えていただけるとありがたいです.

難しい本ですが,Haskellに興味のある人,関数型プログラミングが気になっている人には自身を持ってオススメできる本です.

内容

第1章 はじめの第一歩

第1章では,関数呼び出しやリスト,そしてリスト内包表記などについて簡単に触れます.

第2章 型を信じろ!

第2章では,型や型クラスについて簡単に触れます.この型クラス初級編の時点で私は少し苦戦しました. 難しければとにかく次に進んで,あとで読み直すとすっきり理解できると思います.

第3章 関数の構文

第3章では,関数の構文,パターンマッチやガードについて見ていきます. もちろんif式で実現できることも多いのですが,これらの構文を使った方が非常にエレガントに書けますし,後の再帰が非常に理解しやすくなります.

第4章 Hello 再帰

関数型プログラミングの真髄,再帰です. この章はこの本でもっとも感動した部分の1つです.私はこの本のおかげで再帰を理解することができました. 再帰に苦手意識のある人は,Haskellに興味がなくてもこの章だけ読んでいってもらいたい(?)

Haskellでは再帰が重要です.なぜなら,Haskellでは命令型言語のように計算をどうやってするかを指定するのではなく, 求めるものが何であるかを宣言して計算を行うからです(p.52).

再帰を使う際の定跡は,まず基底部を見極め,次に,解くべき問題をより小さな部分問題へと分割する方法を考えることです. 基底部と部分問題の詳細を考える必要はありません.部分問題の解が正しいという保証をもとに,より大きな最終問題の解を構築すれば良いだけです(p.59).

声に出して読みたい名文ですね.

それから,あの有名なHaskellによるもっとも単純なクイックソートの実装が現れるのもこの章です.

quicksort :: (Ord a) => [a] -> [a]
quicksort [] = []
quicksort (x:xs) = 
    let smallerOrEqual = [a | a <- xs, a <= x]
        larger = [a | a <- xs, a > x]
    in quicksort smallerOrEqual ++ [x] ++ quicksort larger

美しいですね.特に最終行.最終的に欲しいリストの構造をそのまま定義するだけで実現できています.

第5章 高階関数

第5章では,カリー化関数,ラムダ式,畳み込みなどについて解説しています. この章を読むと,なぜHaskellの関数定義では引数と返り値の区別が付かないような書き方をするのか,その謎が解けます.

第6章 モジュール

この章では,Haskellの言語仕様から離れて,モジュールの作り方,標準で用意されているモジュールの使い方など, 非常に実用的な事柄を説明しています.

第7章 型や型クラスを自分で作ろう

第7章には,独自の型を定義する方法,型引数やレコード構文といった便利な構文,型クラスとそのインスタンスの作り方, そしてファンクタなど,非常に多くの重要な概念が含まれます.だんだん難しくなってきます.

その中でもファンクタは特に重要です.この本によれば,ファンクタを一言で説明すると,「全体を写せる(map over)ものの型クラス」だそうです. そして,ファンクタのインスタンスは型コンストラクタ(具体的な型をとって型を作るもの)で,その型によって生み出された具体的な値を,ファンクタ値といいます.

この説明では少しわかりにくいかもしれませんが,例えばMaybeやリストはファンクタのインスタンスであり,型コンストラクタです. つまり,MaybeはIntをとってMaybe Int型を作りますし,リストはIntをとって[Int]型を作ります. そして,Just 5や[3, 4]などがファンクタ値にあたります.

これらの例でわかる通り,ファンクタ値には,値だけでなく,Just や []など,不思議なもの(値コンストラクタ)が付いています. この不思議なものたちは,「文脈」を値に付加していると考えられます. つまり,Just 5は,5という数字だけでなく,「失敗する可能性のあるMaybe Int型の処理で,うまく成功して5を返したよ」という文脈を持ちます. リストに関しても同様に,[3, 4]はただの3や4ではなく,「3と4の両方だよ」という文脈を持ちます.

かなり難しくなってきますが,このファンクタ値のもつ「文脈」をどう扱うかがこの本の1つの大きなテーマになっていて,これが以降の章に繋がっていきます. もっと言うと,ファンクタさえ理解できれば,この本を読み進められるので,頑張ってください.

第8章 入出力

この章では,doprintgetLineなど,I/Oの扱い方について見ていきます.

第9章 もっと入力,もっと出力

第8章では,標準入出力だけを扱っていましたが,第9章では,ファイル読み書きがメインのテーマになります. また,ファイル読み書きのパフォーマンスを向上させるBytestring,そして難関である乱数についても見ていきます. 参照透明性をもつHaskellがどのように乱数生成を実現しているのかがわかり,面白いです.

第10章 関数型問題解決法

第10章は,逆ポーランド記法電卓の実装と最短経路探索という2つの問題を解きながら,関数型プログラミングに慣れていきます. Haskellでコンテストに出たくなりますね.

第11章 ファンクターからアプリカティブファンクターへ

7章にも出てきた,ファンクタをより詳しくみていきます. ファンクタは抽象的な概念ですが,ファンクタも型クラスの一種であること,ファンクタのインスタンスはファンクタ値の中身を写す関数fmapをもつものだということがわかれば,読み進めることができると思います. ファンクタという用語をそのまま理解するよりも,fmapで具体的なファンクタのインスタンスを写してみて,イメージをつかむことが大事です.

そして,ファンクタをさらに拡張した概念であるアプリカティブファンクタ(以後アプリカティブと略)についても説明されています. アプリカティブも型クラスで,そのインスタンスにはpure関数と<*>演算子が定義されています. アプリカティブ値は,以下のような操作が可能です.

ghci> Just (5*) <*> Just 3
Just 15

ghci> pure (*) <*> Just 5 <*> Just 3
Just 15

Maybeは実はアプリカティブなので,Justの中身に関数を入れられます.さらに,<*>という演算子を使って,Justの中身の関数を,もう一つのJustの中身に適用したJust値を得ることができます. また,pure関数は,引数をファンクタの持つ文脈に含められることができる関数で,fmap pure (*) Just 5Just (5*)を返します.

解説の中でfmapをいい感じに書けるアプリカティブスタイルという記法も導入されていますが,すでにたくさん記法が導入されているので,慣れるまでfmapで読み替えた方が頭が爆発しなくて済みます.

第12章 モノイド

また抽象的で難しい概念が出てくるのか...と思いきや,モノイドはそんなに難しくありません. モノイドはファンクタと同様型クラスの一つで,結合的な2引数関数とその関数に関する単位元からなる構造です. つまり,ある型同士の2引数関数が結合則を満たし,その関数に関する単位元が存在する場合,その型はモノイドのインスタンスになることができます.

モノイドを導入して何が嬉しいかというと,Haskellの便利な関数である畳み込みを,リスト以外の型にも適用できるようになります.

また,モノイドを扱う上でよく出てくるnewtypeキーワードに関しても解説があります.

第13章 モナドがいっぱい

モナドも型クラスの一種で,ファンクタの拡張ともいえます(結果的にですが,アプリカティブの拡張でもあります). したがって,モナドもまた「文脈」を値に付加するものです.ファンクタと大きく異なるのは,文脈を持つ値をさらに自由に扱うことができるバインド(>>=)という関数を持つ点です.

モナドは,バインドがキモです. バインドは,「文脈付きの値」と,「通常の値をとってモナド値を返す関数」を引数にとり,「通常の値をとってモナド値を返す関数」を,「文脈付きの値」の中身に適用し,文脈をつけて返します. これができるようになることで,文脈付きの値に対してできることの自由度が大幅に大きくなります.

例えば,以下のような操作が可能です.

ghci> Just 5 >>= \x -> Just $ x * 3
Just 15

これは,>>=によって,モナド値の中身に,3をかける関数を作用させて,モナドに包んで返していることを表しています.

そして,複数のモナド操作を繋げられるdo記法がどのようなものか説明されます. これで,IOに出てきた処理がわかるようになりました.

第14章 もうちょっとだけモナド

第14章はモナドの応用編で,WriterモナドやStateモナドモナド値を操作する関数を使ってみたり,自分で新しくモナドを作り出します. その中でもStateモナドに関する説明は非常に面白いです.「参照透明性」と相性が悪いように思われる「状態」を,どのようにHaskellが扱うのか,それをモナドを使って解決していきます.

第15章 Zipper

Haskellは副作用を持たないので,データの変更という操作は基本的にできません. もしデータを変更したいなら,元のデータを直接操作するのではなく,変更した後のデータを返す,という処理になります.

このような制約を持ちながら,木構造やリストなど,順番に辿る操作を効率的に実現する機能がZipperです. この章では,二分木やリスト,ファイルシステムといった木構造に対してパンくずリスト,つまり訪問履歴を付け加える方法について説明しています.

終わりに

Haskellの言語仕様と関数型プログラミングの考え方が学べる非常に素晴らしい本でした.

現在,多くの言語が高階関数ラムダ式,map,filterなどの関数をサポートしていることから, 関数型プログラミングはコードを書く人間にとって必須の教養であることがわかりますね.