Haskellの教科書とか、つまりどころ等

1 前置き

以下、Haskellの勉強を進めたい人に参考になるかも知れない情報です。ただし、個々人でバックボーンとか嗜好がありますので万能ではないです。
こうやったらもう少し楽に勉強できたかな?という、過去の自分へのメッセージみたいなものです。
現状、自分でつくるPDFリーダー計画(https://polymony.net/2019/06/11/post-776/)がペンディングになっているので、一応、Haskell環境を整えてしまった人へ、何かの足しにになればという気持ちで書いています。

2 開発環境(エディタとかIDEとか)

勉強するには、ターミナルやコマンドプロンプトでghciでやるのは、リアルタイムな構文チェック、型チェックが入らないのでおすすめしません。

2.1 オススメのエディタ

EmacsのHaskell Modeや、Spacemacs(emacsの一拡張)のHaskell Layerがおすすめです。最近だとVSCodeのHaskell IDE Engine (HIE)も人気です。
HIEは、これまでのHaskell関連の開発パッケージをまとめるという計画らしく本命の感があります。が、以下の理由で自分はEmacs(Spacemacs)を使っています。

2.2 Spacemacs(Emacs)のメリット

2.2.1 マイナー言語への対応

新しいプログラミング言語(〇〇)が出てくると、まずEmacsで〇〇モードが作られる?らしいです。マイナー言語にも幅広く対応しています。別の記事で触れているAgdaもEmacsのみが対応しているようです。

2.2.2 LISP(A.I.系言語)との親和性

また、LISPを学習するにも良さそうです。LISPではプログラムをつくるプログラムを書けるみたいです。今流行りの機械学習の系統のA.I.とは別系統の人工知能系言語ですね。
というか人工知能という言葉はLISPの設計者(ジョン・マッカーシーさん)がつくったぽいです。EmacsはelispというLISP方言で書かれているので、LISPを勉強するにも最適な感があります。

2.2.3 拡張の容易さ(初心者向け)

Spacemacsは、いい感じのプラグインが最初から入っています。

2.2.4 Vimのキーバインドとの統合

Vimは操作性に癖がありますが慣れると楽にプログラムを書くことができます。

3 書籍(教科書とか)

3.1 教科書系

3.1.1 プログラミングHaskell

簡潔に文法をマスターできます(第二版の翻訳が出たのでリンク先、更新しました)。何よりページ数が少ない割にまとまっています。最初のうちはこれを理解できるまで何度もやるのがいいと思います。
ただし第6章の再帰関数、8、9章(パーサーやモナド)は難しいので最初は無視してもいいと思います。まず第7章の高階関数を使いこなせるようにしてください。
再帰はHaskellなどの関数型プログラミングの十八番とされており、事実そうだと思いますが、上記の高階関数で書けない、書けてもすっきりしない箇所に再帰を使うという習慣がいいと個人的には思っています。
map, ダメならfold, それでもダメなら再帰、位の優先順位で書いています。mapなら並列化可能な箇所と理解できます。再帰(およびFunctor、モナド等の圏論由来の概念)は初見殺しです。
後は8、9章(パーサーやモナド)を飛ばして第10章(型とクラスの定義)をやれば、下の各種の本を読み始める力がついたと思います。
モナドについては最初は最初のうちはmain関数(IOモナド)をdoと<-やletで書いていく事例を見つけて、それを修正しながら何か自分でプログラムを書いて、それから余裕が出てきてからで十分です。
関数について極力IOが出てこないようにする、そういう関数(純粋関数)をletの中で使う、という意識があれば十分と思います。
<$>とか<*>とかfmapはここではやめておいたほうがいいです。もう少し短く書けるやり方がある、という情報を頭に残しておいて、慣れてきたらそちらにシフトすればいいと思います。

3.1.2 Haskellによる関数プログラミングの思考法

非常に教科書的です。内容的には上の本よりも高度です。作者のBirdさんはAlgebra of Programmingという本も書かれており、そちらでは圏論でどう関数型プログラミングを構成していくかが説明されています。

3.2 辞書的なもの

3.2.1 HaskellWikibooks

タダです。英語ですが扱っている内容が広範でよいです。何か気になった時に、辞書的に使っています。

3.2.2 Real World Haskell

具体的なプログラムを書くことをネタにしています。パラパラと眺めて、面白そうなネタを見つけたら、参考にして自分で組んで見る、という使い方がいいと思います。
基本は他の本で押さえておいて、何か作りたくなったらつまみ食いする、というのがいいと思います。

3.3 読み物系

3.3.2 Haskell入門 関数型プログラミング言語の基礎と実践

上記2つは翻訳ではなく日本人の方が書いています。最初の本(Programming Haskell)の翻訳は違和感がないと自分は感じましたが、この2つから入るというのもありと思います。ただし、教科書的ではないです。
何か他の言語を既に習得している人向けと思います。

3.3.3 すごいHaskellたのしく学ぼう

口語調で進みます。内容的には難しいところまでやっています。こちらも教科書っぽくはない印象です。

3.3.4 簡約λカ娘(各巻)

日本のサークルの方々が書かれた同人誌です。Haskellに限らず、関数型プログラミングを中心ネタにしながら、高度な内容が展開されています。タイトル殺しです。

3.4 速度に関連するもの

Haskellは実行速度が遅い、いやいや速いとか色々な見解が散見されます。どの辺りで速いと言われたり、遅くなってしまったり、というのは以下の本を読んでみるとわかってくると思います。

3.4.1 Parallel and Concurrent Programming in Haskell

並列計算と並行計算についての専門書です。モナドを理解していないときついです。
前半の並列プログラミングは、評価戦略とか並列計算ライブラリ(Repa), GPUプログラミング(CUDAのDLSであるAccelerate)、後半の並行プログラミングは、IORef, MVar, STMについて触れられています。

3.4.2 Haskell High Performance Programming

Haskellで高速なプログラムを書くにはどうしたらよいか、についての本です。2016年時点でのオススメのライブラリについても触れられています。

3.4.3 Guiding parallel array fusion with index types

Stream-Fusionに関する論文。C言語との比較とか載っています。Haskellの開発者が書いています。

3.5 他分野への応用事例

3.5.1 Computational Semantics with Functional Programming

構文解析やセマンティクス(意味論)をHaskellでどう表現するか、が、書かれています。英文の構文解析を行う際に参考にしました。

どれも良い本ですね。自分もいずれPDFリーダーネタで本を書いてみたいです。

4 再帰でなく高階関数を極力使ったほうがいい理由 −純粋関数による数学的な最適化

map, fold, filter,〇〇By(sortByとかnubByとか)等の高階関数やzip、unzipみたいな筋のいい関数で書ける部分は極力書いておくと、GHC(Haskellのコンパイラ)が最適化してくれるという御利益があります。
一例はStream FusionとかRecyclingとかですね。Agdaみたいなのが発展すれば、それに応じて書いた関数が速度アップする見込みがあります。
また、mapやfoldは並列と直列に相当する概念で深いです。

5 最初に押さえておきたい関数(リスト操作関連)について

Haskellはリストを駆使して色々やる類の言語です。関数型言語のルーツ?とされるLISP(LIST Processing)の系譜にあります。
リストに関する関数としては、まずはHackageのVectorのページに出てくる関数を覚えれば間違いないです。速度を気にするならListでなくVectorを使ってください。
Vectorパッケージには、toListやfromListなる関数があり、これでVectorとListの間での型変換ができます。List側にあるけどVector側にない関数や、そもそもVectorの要素をどう書くの?という問題が解決します。
前者の例としては、sortByみたいのは、fromList . sortBy . toListと書けます。vSortByとか適当に名前をつけておけば後で使いやすいです。
後者はvs = fromList [0 .. 100]とかです。これもよく使います。ただ、VectorとListでは定義されている関数の名前がかぶるので、
import qualified Data.Vector as Vなどとhsファイルの冒頭のインポート箇所に書いておいて、区別するのがいいです。Vector側の関数や型はV.と書かないといけませんよ、という意味です。そうなると、上のは、vSortBy = V.fromList . sortBy . V.toListとか、vs = V.fromList [0 .. 100]などと書くことになります。
リスト内包表記も、リストで書いておいてfromListで可能ですが、map, filter, concatMap idで同じことができますので、そちらをまず覚えたほうがいいです。

6 ラムダ式とWhere

ポイントフリースタイルは、最初は触らなくていいです。

map length xss

も、最初のうちは

map (\xs -> length xs) xss

と書いたほうがラムダ式に早く慣れることができます。そもそも、ラムダ式で書いた方が引数の位置について自由で便利な気がします。flipやcurry, uncurryを多用する方が視認性が悪いと思います。例えば、

map  (flip div 2) [1 .. 10]
map  (\x -> div x 2) [1 .. 10]

のどちらと言われれば、私は後者の方が見やすいと思っています。
ワンライナーのラムダ式が冗長に感じられたら、

map f [1 .. 10]
  where
     f x = div x 2

と書くやり方もあります。where節はとても大事です。変数が有効になる範囲(スコープ)を留めてくれます。上の事例であれば変数はfになるのですが、where節の外側ではこのf x = div x 2という定義、関数は使えません。
なので別の箇所でf xs = length xsなどと定義しても問題なくなります。関数(上で言うとf)、を引数にとる高階関数(上で言うとmap)はwhere節とセットになることで力を発揮します。
使い捨ての関数を無名関数(λ式)だけでやろうとするのはきついですが、where節でいくらでも使い捨ての関数をスコープ付きで定義できます。
自分は最初はlet .. inだけでwhereを使ってませんでしたが、現在は上の便利さから極力whereを使って、letはdo構文でのみ使うスタイルになっています。
引数の部分適用もラムダ式とリストと組み合わせることで絶大な効果を発揮します。例えば、上の例で言うと、divの場合割る側と割られる側の2つの引数がありますが、以下のラムダ式で切り替えられます。

f xs = map (\x -> div x 2) [1 .. 10]
f xs = map (\x -> div 100 x) [1 .. 10]

ラムダ式で指定している変数(x)に穴が開いていて、そこにリストの各値を埋める各々出力する感じです。2つ穴を開けたければ、

f xs = map (\x -> map (\y -> div x y) [1 .. 100]) [1 .. 10]

などとやれます。これだとネストしたリストになるので、concatすれば一列のリストになります。

f xs = concat $ map (\x -> map (\y -> div x y) [1 .. 100]) [1 .. 10]

上記は有名なリスト内包表記で書けます。

f xs = [div x y | x <- [1 .. 100], y <- [1 .. 10]]

数学的に直感的に書けます。ただし、mapとconcat(要素を濾したい場合はfilterも)で代用できますので、そちらをまず覚えたほうがいいです。ネストしたリストが必要なときもあります(素朴な実装の場合の画像とかそうですね)。

7 undefinedについて

また、関数を定義する際に、ひとまずundefinedで逃げておいて、型チェックだけはかかるようにする、というのも何気に重要なテクニックです。AgdaでいうところのHoleみたいなものです。
自分はundefinedを半年くらい気付かずに無駄な時間を使いました。
リアルタイムの型チェックを使っている場合は必須です。undefinedを書いておけばとりあえず何か書き終えたと型チェッカーが認識してくれて、更に、辻褄の合う形で周りの型を決めてくれます。

8 asパターン(@マーク)

また、asパターンも大事です。$と同じでググりにくかったので簡単に触れます。xy@(x, y)=とか、\xy@(x, y) -> みたいなやつです。
where x = fst xyとか、fstとかsndを書くことが多くなって煩わしくなってきたら、こういうのがあったと思いだしてください。
asパターンと検索すると出てきます。自分は最初どう読むかわからず、調べるのに時間がかかりました。

9 $記号について

$もググりにくい感じでした。length (xs + ys)みたいのをlength $ xs + ysと書けます。$の右側をlengthと切り離して右に押し出しているイメージです。
間違えやすいのは、map $ length $ xss + yssみたいにやってしまうことです。これはmap length $ xss + yssが正解です。
$をカッコで置き換えると、map (length (xss + yss))になりますが、これだとmapの第一引数が(length (xss + yss))、第二引数は存在しない感じになっています。
mapの第一引数は関数である必要があるのに(length (xss + yss))は関数でないので、コンパイラに怒られます。押し出していいのはlengthの右側のxss + yssのみです。

10 副作用を許す件

Haskellは純粋関数のみで副作用を許さない、書けない、みたいな評判がありますが、正確には純粋関数で書かれた部分と副作用(IOとか参照とか)を明示的に分ける、ように言語自体が誘導している、というのが正解だと思います。
分別しているだけです。更に分別しない書き方もできます。しかし分別しない場合は上のようなGHCによる最適化のご利益が得られないですし、
慣れていないと型エラーチェックで弾かれてなかなかコンパイルできない、という事態になります。
それで楽にやろうとすると自然と関数型プログラミングスタイルで書く(純粋関数でなるべく書こうとする)ハメになる、というのが自分の経験でした。

11 多相関数、型コンストラクタを型理論の観点から理解する

型理論の観点からすると、型コンストラクタと多相関数(polymorphicな関数)は名前は全然違っていますが、互いに関連のある概念であることが分かります。
以下、Termは関数や変数、Typeは型と読み替えてください。
(A) Type dependent on Type (型コンストラクタ)
(B) Term dependent on Type (多相関数)
(C) Term dependent on Term (高階関数)
(D) Type dependent on Term (??)
(A)型コンストラクタの例としてはMaybeなどです。Maybeは単体では型(Type)ではなく、引数として型(Type)をもらうことで型(Type)になります。例としてはMaybe Intとかです。
(B)多相関数(polymorphicな関数)としては例えば、

map :: (a -> b) -> [a] -> [b] 

の場合、mapはaとbという型(Type)について多相な関数(Term)となります。型としては、Int, Bool, Double, String .. などが挙げられます。
いずれもa, bの位置に代入できます(そういう関数とリストを用意できれば、です)。
(C)高階関数の例としては例えば、

filterInt :: (Int -> Bool) -> [Int] -> [Int] 

が挙げられます。
Int -> Boolという関数(Term)を引数にとる関数(Term)です。
では、
(D)??は何に当たるかというと、依存型(Dependent type)なる概念に相当します。
依存型(Dependent type)の具体例としては、長さが100(これがterm)のリスト(これがType)などがあります。

このように型理論の観点だと、TermとTypeという概念から4通りの方向性が見えます。
Haskellは標準では(A)、(B)、(C)のみ対応のようですがAgdaはデフォルトで(D)も対応しています。Haskellはこのように、型理論の観点からすると、上記のTypeとTermについての関係について、一部が欠けています。
なので、型クラスとか多相関数とかの別の名前が付けられており、上のシンメトリックな関係性が見えず見通しが悪いです。
混乱したらラムダキューブを調べることをお勧めします。ただ、(D)はデメリット(型チェックが難しくなる)もあり、敢えて標準としては採用しない、となっているのかも知れません。
が、Agdaだと数学の定理を証明できたりとか、そちらはそちらで面白い感じです。

12 備考(OrgModeについて)

今回の記事はEmacs(Spacemacs)のOrgモードで書きました。こちらもいつかご紹介したいです。以下のような感じで構造化を意識し、色もついていてなおかつ折りたためます。
いずれ例のPDFリーダネタで本を書くための布石で勉強しています。

Author: polymony

Created: 2019-06-26 水 19:09

Validate

シェアする

  • このエントリーをはてなブックマークに追加

フォローする