Haskell
stack

本気で Haskell したい人向けの Stack チュートリアル

本記事は内容量が多くなりすぎてしまったため、以下のサイトにまとめなおしました。

最新の内容は上記のサイトをご確認ください。

また、本記事のコードについては waddlaw/Qiita-stack に置いてあります。


TODO: liquidhaskell, profiling, circle ci, gauge, default-extention, ghc-option, extra-deps, tasty, restyled.io について追記する。

本記事では Pearls of Functional Algorithm Design (訳本: 関数プログラミング 珠玉のアルゴリズムデザイン) の第1章を題材として stack を使った Haskell プロジェクトの基本を解説しています。

プログラムの内容を理解できなくても、記事を最後まで読むことで Haskell プロジェクトの作り方を学ぶことができると思います。

Ubuntu or Mac で動作確認を取っています。

エディタについて

特にこだわりが無い人は VS Code + haskell-ide-engine で良いと思います。以下の記事でインストールについて解説していますので、興味がある方はどうぞ。

VS Code と haskell-ide-engine で Haskell 開発環境を構築する

haskell-ide-engine は本当に便利なのでどのエディタでも利用した方が良いと思います。

stack のバージョンについて

2018年7月16日現在の stack の最新バージョンは 1.7.1 です。

リリースの詳細は Stack 1.7.1 がリリースされました。 にまとめてあるので、そちらをご参照ください。

stackとhpackのバージョン確認
$ stack --version
Version 1.7.1, Git revision 681c800873816c022739ca7ed14755e85a579565 (5807 commits) x86_64 hpack-0.28.2

--version オプションを使うと以下の情報がわかります。

  • stack のバージョン: 1.7.1
  • hpack のバージョン: 0.28.2

stack コマンドの更新

stackコマンドの更新
$ stack upgrade

# バージョン指定 (前のバージョンに戻したい場合などに便利)
$ stack upgrade --binary-version 1.6.5

stack upgrade と良く似たコマンドに stack update というものがありますが、こちらはほぼ利用しません。なぜなら stack updatecabal update が行うようにパッケージインデックスの更新を明示的に行うコマンドですが、必要であれば stack の内部で自動的に stack update が実行されるためです。
=> How do I update my package index?

より詳しい更新方法についてはStack の更新にまとめました。

stack について

stackHaskell で開発を効率良く行うためのビルドツールです。(ghc のインストールやパッケージ管理まで全て行なってくれます)

今まで全く Haskell に触れたことが無い人から、多人数で管理される Haskell プロジェクトまで、かなり広範囲のユーザを対象として作られています。

なぜ stack を使うのか?

stack を使わずに Haskell で本格的なアプリケーションを開発することはとても大変です。仮に開発ができたとしても、規模が大きくなるにつれ、開発者しかビルドできなくなります。stack が登場する以前の Haskell 界隈では、この問題は特に珍しいことではありませんでした。(現在は NixOS, cabal sandbox, Docker などの選択肢もあります)

特に頭を悩ませていた問題が cabal hell と呼ばれるパッケージ依存性の問題です。1つのマシンで複数の Haskell プロジェクトをビルドしようとするだけで、全てがおかしくなっていました。

この問題を解決するために stack が現れます。2015年頃の話なので、Haskell の歴史からすると、比較的最近の出来事です。stackcabal を捨てたわけではありません。cabal を利用したまま、その上で cabal hell が起こらないような、バージョンを固定したパッケージの集合を管理するようにしました。それが Stackageltsnightly などの名前が付いているスナップショットです。

つまり、依存関係が壊れないように Haskell のライブラリ製作者・Stackage メンテナで頑張っています。(サポートツールも多いですが、コミュニティによる人間の作業も多いです)

これから業務なり、勉強なりで Haskell を使おうと思っている人は必ず stack を使ってください。現在 github などで見かけるプロジェクトの大多数は stack で管理されたものになっています。

Stackage とは何か?

StackageStable Hackage の略で、どんな組み合わせでも依存関係でエラーが起きないように調整されたパッケージの集合 (スナップショット) を提供しています。

スナップショットには以下の2種類があります。

  • 長期サポートの Long Term Support (lts)
  • 日々のスナップショット nightly

lts のバージョンは X.Y という形式になっており、X をメジャーバージョン, Y をマイナーバージョンと呼びます。nightly のバージョンは nightly-YYYY-MM-DD という形式です。

バージョンの上がるタイミングと内容は以下のようになっています。

バージョン タイミング 備考
メジャーバージョン 3 ~ 6 ヶ月に一度 lts-11.0 → lts-12.0 API の破壊的変更, パッケージの追加, パッケージの削除が行われる
マイナーバージョン 1週間に一度 (主に日曜日) lts-12.0 → lts-12.1 互換性のある API の変更, パッケージの追加が行われる
nightly 日々 nightly-2018-07-13 → nightly-2018-07-14 API の変更, パッケージの追加

lts で一度対応してしまえば、マイナーバージョンを上げた際にコードが壊れることは基本的にありません。なので、互換性を維持したまま新しい関数やバグフィックスされた関数などを使うことができます。

StackageHackage にアップロードされたパッケージをミラーしているので、自分の作ったパッケージを Hackage にアップロードすると、自動的に Stackage にもパッケージの情報が反映されます。この段階では lts にも nightly にも含まれません。

もし、自分の作ったパッケージを ltsnightly に含めたい場合は自分で別途申請する必要があります。申請方法は簡単で、stackage リポジトリ にプルリクエストを投げて、承認されれば完了です。

詳しいことは、MAINTAINERS.md にまとまっているため、興味がある方はどうぞ。

スナップショットに追加される基本的な流れとしては以下の通りです。

  1. Hackage にパッケージをアップロードする
  2. Stackage に自動的に反映される
  3. nightly の申請を行い、nightly に追加される
  4. lts のマイナーバージョンアップデートで lts に追加される

このスナップショットを stack.yamlresolver に指定する (後述) ことで、プロジェクトで利用するパッケージのバージョンを固定することができます。また、オプションとして渡すこともできます。(また、上記では紹介していませんが ghc-X.Y.Z のように GHC のバージョンを指定したスナップショットもあります)

resolverの指定方法
# resolver のスナップショットに lts-12.0 を指定して ghci を起動
$ stack ghci --resolver lts-12.0

# GHC-8.4.3 で ghci を起動
$ stack ghci --resolver ghc-8.4.3

# GHC-8.2.2 で ghci を起動
$ stack ghci --resolver ghc-8.2.2

# 最新の resolver で ghci を起動
$ stack ghci --resolver nightly
$ stack ghci --resolver lts

stack 以外の選択肢

Haskell の学習を始める前に、 stack 以外の選択肢についても簡単に確認しておきましょう。

cabal

個人的にはあまり推奨していません。その理由としては

  • cabal の記法が独特
  • ネット上に存在する日本語の情報が古い場合が多い

ただ、現在実装が進んでいる cabal new-build は非常に魅力的だと思いますし、本格的なプロジェクトを始める時に、再度検討すれば良いかなと思います。(cabal を使っている人も一定数います)

Haskell Platform

書籍に閉じた学習目的であれば、こちらも候補になります。しかしながら、以下の理由であまり推奨していません。

  • インターネット上に存在するチュートリアルでは様々なパッケージを利用するため。Haskell Platform にパッケージが含まれていないと追加するのは結構面倒だったと思います
  • Haskell のプロジェクトを始めようと思ったら結局は stackcabal を使うことになるので、初めからそちらを利用した方が良いです。

Nix

上級者向けなのでやめておきましょう。

はじめに

stack に関する情報は以下を参照しています。

stack のインストール

お好きな方法で stack をインストールしてください。僕のおすすめは、公式が推奨している curlwget を使った方法です。

stackのインストール
$ curl -sSL https://get.haskellstack.org/ | sh

$ wget -qO- https://get.haskellstack.org/ | sh

このタイミングで ~/.local/binPATH に追加しておくことをおすすめします。このディレクトリに stack install コマンドでインストールされた実行ファイルが保存されるため、パスを通しておくとどこからでもコマンドを実行できるようになります。

パスの追加
$ export PATH=~/.local/bin:$PATH

# 必要に応じて .bashrc などに追記してください
$ echo 'export PATH=~/.local/bin:$PATH' >> ~/.bashrc

config.yaml

一番最初だけ stack update を行い、グローバルプロジェクト用のフォルダを生成しましょう。

グローバルプロジェクト用のフォルダを生成
$ stack update
...

$ tree ~/.stack/
/root/.stack/
|-- config.yaml
`-- indices
    `-- Hackage
        |-- 00-index.tar
        |-- 00-index.tar.gz
        |-- 00-index.tar.idx
        |-- 01-index.tar
        |-- hackage-security-lock
        |-- mirrors.json
        |-- root.json
        |-- snapshot.json
        `-- timestamp.json

indices フォルダを触ることは基本的にありません。

config.yaml には stack new でプロジェクトを生成する際の設定を記述します。僕はいつもこんな感じに設定しています。

config.yaml
default-template: new-template
templates:
  scm-init: git
  params:
    author-name: Your Name
    author-email: youremail@example.com
    github-username: yourusername

詳しくはドキュメントをご確認ください。

config.yaml のよくある設定 にも少しまとめてあります。

hpack について

hpack はプロジェクト内に package.yaml が存在する場合のみ package.yaml から .cabal ファイルを自動生成するためのツールです。stack にバンドルされているため、別途 hpack をインストールする必要はありません。

また、最新版の hpack を利用したい場合は --with-hpack=<PATH> オプションを利用します。

hpack を利用するメリットとしては以下の2点です。

  • yaml 記法なので読みやすい
  • other-modulesLISENCE など、自動的に推論してくれる

stack で管理されているプロジェクトは、ほとんど hpack を利用しています。そのため、本記事でも hpack を利用します。

stack サブコマンドの自動補完

設定しておくと、stack のサブコマンドを補完してくれるので、非常に便利です。

$ eval "$(stack --bash-completion-script stack)"

zsh ユーザは別途マニュアルを参照してください。

完全なリビルド

通常 stack build でリビルドする時、前回ビルド時のキャッシュが利用されます。しかし、場合によって (警告をもう一度みたいなど) はキャッシュを無視してリビルドしたい時があります。

--force-dirty--ghc-options=-fforce-recomp などを使う方法もあるのですが、一番確実なのは stack clean することです。

キャッシュの削除
$ stack clean
$ stack build

# 上記でもダメな場合
$ stack clean --full
$ stack build

上記でもダメな場合は完全なリビルドをご確認ください。

stack のアンインストール

Haskell に飽きてしまった場合などに削除するフォルダは以下の3箇所です。

stackのアンインストール
# パッケージやGHCは以下のディレクトリに保存されます。
$ rm -rf ~/.stack/

# stack install でいれた実行ファイルを削除します。
$ rm -rf ~/.local/bin/

# stack の実行ファイルを削除します。
$ rm /usr/local/bin/stack

特に ~/.stack/ 以下は数GBほどになっているのが普通なので、忘れずに削除しましょう。

プロジェクトの新規作成

プロジェクトの新規作成
$ stack new PFAD
$ cd PFAD

stack new コマンドによって作られるデフォルトのディレクトリとファイルはこのようになります。

プロジェクトの初期構造
$ tree .
.
├── app
│   └── Main.hs
├── ChangeLog.md
├── LICENSE
├── package.yaml
├── PFAD.cabal
├── README.md
├── Setup.hs
├── src
│   └── Lib.hs
├── stack.yaml
└── test
    └── Spec.hs

おすすめの開発方法

その時によって違うといえば違うのですが僕は vscode のターミナルに以下のコマンドを打ち込んで自動的にビルドさせています。

$ stack test --fast --file-watch

各オプションの意味は以下の通りです。

オプション 意味
--fast 最適化を無効にする (-O0)
--file-watch ファイルの変更を検知するとリビルドする

準備

新たに作成するソースコードは app, src, test に以下にそれぞれ配置します。一応、以下のコマンドに対応するようにフォルダを分けることが多いです。

フォルダ 関連するコマンド
app stack exec
src stack build
test stack test

今回 src/Lib.hs は使わないので削除しておきましょう。

$ rm src/Lib.hs

そうすると当然 Lib.hs が無いのでコンパイルエラーになってしまいます。

$ stack build
...
app/Main.hs:3:1: error:
    Could not find module ‘Lib’
    Use -v to see a list of the files searched for.
  |
3 | import Lib
  | ^^^^^^^^^^

とりあえず現状は app/Main.hs を以下のように書き換えておきましょう。

app/Main.hs
module Main (main) where

main :: IO ()
main = undefined

これでビルドが通るようになりました。

ライブラリの作成

それでは書籍のコードを src/Minfree.hs として保存してみましょう。

src/Minfree.hs
module Minfree (minfree, minfree') where

import Data.Array (Array, elems, accumArray, assocs)
import Data.Array.ST (runSTArray, newArray, writeArray)

minfree :: [Int] -> Int
minfree xs = head ([0..] \\ xs)

(\\) :: Eq a => [a] -> [a] -> [a]
us \\ vs = filter (`notElem` vs) us

search :: Array Int Bool -> Int
search = length . takeWhile id . elems

checklist :: [Int] -> Array Int Bool
checklist xs = accumArray (||) False (0, n) (zip (filter (<= n) xs) (repeat True))
  where n = length xs

countlist :: [Int] -> Array Int Int
countlist xs = accumArray (+) 0 (0,n) (zip xs (repeat 1))
  where n = maximum xs

sort :: [Int] -> [Int]
sort xs = concat [replicate k x | (x,k) <- assocs (countlist xs)]

checklist' :: [Int] -> Array Int Bool
checklist' xs = runSTArray $ do
  a <- newArray (0, n) False
  sequence [writeArray a x True | x <- xs, x<=n]
  return a
  where n = length xs

partition :: (Int -> Bool) -> [Int] -> ([Int], [Int])
partition p xs = (filter p xs, filter (not . p) xs)

minfree' :: [Int] -> Int
minfree' xs = minfrom 0 (length xs, xs)

minfrom :: Int -> (Int, [Int]) -> Int
minfrom a (n, xs)
  | n == 0 = a
  | m == b - a = minfrom b (n-m, vs)
  | otherwise = minfrom a (m,us)
    where
      (us, vs) = partition (<b) xs
      b = a + 1 + n `div` 2
      m = length us

プログラムの実行

ここまでの内容を対話環境で実行したいと思います。

$ stack ghci
Using main module: 1. Package `PFAD' component exe:PFAD-exe with main-is file: /home/bm12/Desktop/Qiita/PFAD/app/Main.hs
PFAD-0.1.0.0: configure (lib + exe)
Configuring PFAD-0.1.0.0...
PFAD-0.1.0.0: initial-build-steps (lib + exe)
The following GHC options are incompatible with GHCi and have not been passed to it: -threaded
Configuring GHCi with the following packages: PFAD
GHCi, version 8.4.3: http://www.haskell.org/ghc/  :? for help
Loaded GHCi configuration from /home/bm12/.ghci
[1 of 2] Compiling Main             ( /home/bm12/Desktop/Qiita/PFAD/app/Main.hs, interpreted )
[2 of 2] Compiling Minfree          ( /home/bm12/Desktop/Qiita/PFAD/src/Minfree.hs, interpreted )

/home/bm12/Desktop/Qiita/PFAD/src/Minfree.hs:3:1: error:
    Could not find module ‘Data.Array’
    Perhaps you meant Data.Proxy (from base-4.11.1.0)
    Use -v to see a list of the files searched for.
  |
3 | import Data.Array (Array, elems, accumArray, assocs)
  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

/home/bm12/Desktop/Qiita/PFAD/src/Minfree.hs:4:1: error:
    Could not find module ‘Data.Array.ST’
    Use -v to see a list of the files searched for.
  |
4 | import Data.Array.ST (runSTArray, newArray, writeArray)
  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Failed, one module loaded.

<no location info>: error:
    Could not find module ‘Minfree’
    It is not a module in the current program, or in any known package.
Loaded GHCi configuration from /tmp/haskell-stack-ghci/52d428be/ghci-script

このエラーメッセージでは、Data.ArrayData.Array.ST というモジュールが見つからなかったので、インポートできず、エラーとなってしまったことを教えてくれています。

ghci の対話セッションから抜ける際は :q と入力します。

ghciの終了
*Main> :q
Leaving GHCi.

GHC について

ghciGlasgow Haskell Compiler’s interactive environment の意味です。 GHC はイギリスのグラスゴー大学で開発された Haskell コンパイラなのでこのような名前がついています。注意して欲しい点としては Haskell == GHC と理解してしまうことです。

Haskell というプログラミング言語の仕様が Haskell98, Haskell2010 と続いてきたなかで実際にこの仕様に沿ったプログラムを実行できるコンパイラ (処理系) の一つが GHC というのが正しい理解です。他にも UHCLHc などがあるそうです。ですので、Haskell2010 の仕様に無い機能等は GHC拡張 として実装されています。=> Implementations

処理系が複数あると言っても主流 (デファクトスタンダード) は GHC です。なので、基本的に Haskell の話をする時はみんな頭の中に GHC がインストールされていると思いますし、実務でも GHC 以外を採用することは一般的に考えにくいです。

依存関係の追加

base パッケージに含まれていないモジュールを import して使いたいというのは実際の開発においても頻繁に起こります。

今回の場合は先ほどのエラーメッセージから array パッケージをインストールすれば良いことがわかっています。

package.yaml ファイルの dependenciesarray を追記します。必要であればバージョンを明示的に指定しますが、今回は省略します。

package.yaml
dependencies:
- base >= 4.7 && < 5
- array

そして次のコマンドでプロジェクトをビルドするだけで、stack は自動的に array パッケージ及び、その依存関係をインストールしてくれます。

$ stack build

アプリケーションの作成

ここまででライブラリ (Minfree.hs) の作成が終わりました。

ここからは、そのライブラリを使って動く実行ファイルを作ってみましょう。

app/Main.hs の内容を以下のように書き換えましょう。

app/Main.hs
module Main (main) where

import System.Environment (getArgs)
import Minfree (minfree)

main :: IO ()
main = do
  [xs] <- getArgs
  print $ minfree $ read xs

このコマンドを実行するためには次のようにします。

minfreeプログラムの実行
$ stack build
$ stack exec -- PFAD-exe [0,1,2,3,5,6]
4

デフォルトの PFAD-exe という名前が気に入らない人は package.yamlexecutables を変更しましょう。

package.yaml
executables:
  minfree: # ここを変更します
    main:                Main.hs
    source-dirs:         app
    ghc-options:
    - -threaded
    - -rtsopts
    - -with-rtsopts=-N
    dependencies:
    - PFAD

ビルドしなおすと、指定したコマンド名で実行できるはずです。

名前を変更して実行
$ stack build
$ stack exec -- minfree [0,1,2,3,4,7]
5

minfree2 の作成

アプリケーションは複数作ることができます。例えば今回 minfree 関数とそれを改良した minfree' 関数がありました。別のアプリケーションとして minfree' を使った minfree2 を作ってみましょう。

まずは package.yamlexecutables に新しいアプリケーションの内容を追記します。また、複数のアプリケーションをビルドできるように source-dirs を削除し、 main の指定を app/Main.hs のようにします。

package.yaml
executables:
  minfree:
    main:                app/Main.hs  # 変更
    ghc-options:
    - -threaded
    - -rtsopts
    - -with-rtsopts=-N
    dependencies:
    - PFAD
  # ここから下の行を追記しました
  minfree2:
    main:                app/Main2.hs
    ghc-options:
    - -threaded
    - -rtsopts
    - -with-rtsopts=-N
    dependencies:
    - PFAD

先ほどのプログラムとほぼ同じですが、以下のように app/Main2.hs を作ってみましょう。

app/Main2.hs
module Main (main) where

import System.Environment (getArgs)
import Minfree (minfree')

main :: IO ()
main = do
  [xs] <- getArgs
  print $ minfree' $ read xs

実行してみます。

$ stack build
$ stack exec -- minfree2 [1,2,3,4,5]
0

ドキュメントの作成

Haddock の基礎知識

stack には Haddock 形式と呼ばれる形式でコメントを残すことで、そのコメントを自動的にドキュメントに変換する stack haddock コマンドがあります。

まずは特に何もせずに Haddock を生成してみましょう。

haddockの生成
$ stack haddock
...

Running Haddock on library for PFAD-0.1.0.0..
Haddock coverage:
   0% (  0 /  3) in 'Minfree'
  Missing documentation for:
    Module header
    minfree (src/Minfree.hs:6)
    minfree' (src/Minfree.hs:36)
Documentation created:
.stack-work/dist/x86_64-linux/Cabal-2.2.0.1/doc/html/PFAD/index.html,
.stack-work/dist/x86_64-linux/Cabal-2.2.0.1/doc/html/PFAD/PFAD.txt

...

これでドキュメントは生成されましたが、ログによると .stack-work/dist/x86_64-linux/Cabal-2.2.0.1/doc/html/PFAD/index.html に生成されているようです。

ちょっと確認しづらいので自動的にブラウザで開いてくれる --open オプションを使ってみましょう。

$ stack haddock --open

また、出力先のディレクトリを変更するためには --haddock-arguments --odir=haddock オプションを利用します。

キャッシュが残っているため上手く生成できていない
$ stack haddock --haddock-arguments --odir=haddock
$ tree -L 1
.
├── app
├── ChangeLog.md
├── LICENSE
├── package.yaml
├── PFAD.cabal
├── README.md
├── Setup.hs
├── src
├── stack.yaml
└── test

haddock ディレクトリができると思いきや、何も起こりませんね。これはビルドキャッシュが残っているためです。一度 stack clean をしてからもう一度実行してみましょう。

$ stack clean
$ stack haddock --haddock-arguments --odir=haddock
$ tree -L 1
.
├── app
├── ChangeLog.md
├── haddock       # 生成された
├── LICENSE
├── package.yaml
├── PFAD.cabal
├── README.md
├── Setup.hs
├── src
├── stack.yaml
└── test

haddock/index.html をブラウザで確認すると、こんな感じで Hackage と同じようなドキュメントが生成されているはずです。

page1.png
page2.png

ここまでで基本的なドキュメントの生成方法はわかりました。

気をつける点としては expose されている関数のみがドキュメント化されるという点です。Haddock 形式のコメントを使っていなくても expose されている関数や型は、自動的に情報が公開されることを理解しておきましょう。

次は Haddock コメントを追加してどんどんドキュメントをリッチにしていきましょう!

Haddock コメント形式

詳しい書式については以下のドキュメントを適宜参照してください。

Haddock 形式のコメントは非常に簡単です。

-- 通常のコメント
f = undefined

-- | Haddock 形式のコメント
--   直後の関数についてのコメント
g = undefined
-- ^ この形式も同様に Haddock 形式のコメント
--   直前の関数についてのコメント

つまり、-- |-- ^ で始まるコメントが Haddock コメントということになります。慣習的に関数のコメントには -- | を使い、型のフィールドについては -- ^ を使っているように思います。

以下は公式のドキュメントに載っている例をまとめたものです。

これだけ知っていれば Haddock が書けると言って良いでしょう。強調のためのキーワードやモジュールのコメントなどはここでは紹介しないのでドキュメントをご参照ください。

haddockの基本例
{-|
Module      : W
Description : Short description
Copyright   : (c) Some Guy, 2013
                  Someone Else, 2014
License     : GPL-3
Maintainer  : sample@email.com
Stability   : experimental
Portability : POSIX

Here is a longer description of this module, containing some
commentary with @some markup@.
-}
module W where

-- |The 'square' function squares an integer.
-- @since 1.0.0
square :: Int -> Int
square x = x * x

data T a b
  = C1 a b  -- ^ This is the documentation for the 'C1' constructor
  | C2 a b  -- ^ This is the documentation for the 'C2' constructor

data R a b =
  C { a :: a  -- ^ This is the documentation for the 'a' field
    , b :: b  -- ^ This is the documentation for the 'b' field
    }

f  :: Int      -- ^ The 'Int' argument
   -> Float    -- ^ The 'Float' argument
   -> IO ()    -- ^ The return value
f = undefined

@since メタデータは Yesod 関連のプロジェクトでは良く使われています。どのバージョンからこの関数が組み込まれたかをドキュメントに残すことができます。

実際に使ってみよう!

練習として minfree 関数に Haddock コメントをつけてみましょう。

src/Minfree.hs
-- |
-- 与えられた自然数のリストに含まれない最小の自然数を求める関数
-- 自然数は0を含む
-- 前提条件1: 与えられたリストには順序がついていない
-- 前提条件2: 要素は重複していない
minfree :: [Int] -> Int
minfree xs = head ([0..] \\ xs)

まぁまぁ良いでしょう!ではドキュメントを生成して確認してみましょう。

$ stack haddock --haddock-arguments --odir=haddock
$ firefox haddock/Minfree.html

スクリーンショット 2017-12-11 16.46.32.png

失敗しました。流石に1行はちょっと読みづらいので、修正します。

修正は簡単で、改行したい場所に空行を挟むだけです。

src
-- |
-- 与えられた自然数のリストに含まれない最小の自然数を求める関数
--
-- 自然数は0を含む
--
-- 前提条件1: 与えられたリストには順序がついていない
--
-- 前提条件2: 要素は重複していない
minfree :: [Int] -> Int
minfree xs = head ([0..] \\ xs)

スクリーンショット 2017-12-11 16.49.17.png

それっぽくなりましたね!

build の設定

ここまでである程度 Haddock に慣れてきたと思います!

しかし、毎回 stack haddock --haddock-arguments --odir=haddock を実行するのは面倒です。規模が小さいうちは処理もそんなに重たく無いのでビルドした際に一緒にドキュメントを生成したいと思うことがあるかもしれません。

そういった時は stack.yaml をカスタマイズしてみましょう!

stack.yaml のコメントを削除したものがこちらです。(慣れないうちはコメントが非常に有用なのですが、慣れてくると邪魔でしかないです。)

stack.yaml
resolver: lts-12.0
packages:
- .

ここで Haddock に関するデフォルトオプションを明示的に指定してみましょう。(デフォルト値を設定しているだけなので、何も無い場合と全く同じ動作をします)

stack.yaml
resolver: lts-12.0
packages:
- .
build:
  haddock: false
  haddock-arguments:
    haddock-args: []
  open-haddocks: false
  haddock-internal: false
  haddock-hyperlink-source: true

試しに stack haddock を実行して前と同じものが生成されていることを確認しましょう。

$ stack clean
$ stack haddock --haddock-arguments --odir=haddock

では、まずは --haddock-arguments --odir=haddock の指定を省略できるように、以下のように設定ファイルを書き換えましょう。

stack.yaml
resolver: lts-12.0
packages:
- .
build:
  haddock: false
  haddock-arguments:
    haddock-args:         # [] を削除
    - --odir=haddock      # 追記
  open-haddocks: false
  haddock-internal: false
  haddock-hyperlink-source: true

では確認してみましょう。

$ rm -rf ./haddock
$ stack clean
$ stack haddock

オプションを指定していませんが、期待通り haddock ディレクトリが生成されました!

では次に stack buildhaddock を生成するようにしてみましょう。

stack.yaml
resolver: lts-12.0
packages:
- .
build:
  haddock: true              # 変更
  haddock-arguments:
    haddock-args:
    - --odir=haddock
  open-haddocks: false
  haddock-internal: false
  haddock-hyperlink-source: true

確かめてみましょう。

$ rm -rf ./haddock
$ stack clean
$ stack build

ちゃんと動いていますね。これはどういうことかと言うと実は stack haddockstack build --haddock コマンドと同じです。--haddock オプションを渡すことで設定ファイルの haddock: true と同じ効果があります。

ちなみに stack test も同様に stack build --test と同じです。

これで stack build を行うだけでドキュメントの生成も自動的に行うようにカスタマイズすることができました。

open-haddocks オプション

このオプションはビルド完了時に HTML ファイルを自動的に開いてくれる設定です。

true にしてビルドすると自動的にブラウザが立ち上がり、ドキュメントを開くでしょう。その時のファイルは all/index.html を開くため、base パッケージのドキュメントなども含まれています。

--odir を指定しても、内部に生成された haddock を開いてしまうため、基本的にはあまり使いません。

最終的なファイル

コメントを以下のようにつけました。

src/Minfree.hs
module Minfree (minfree, minfree') where

import Data.Array (Array, elems, accumArray, assocs)
import Data.Array.ST (runSTArray, newArray, writeArray)

-- |
-- 与えられた自然数のリストに含まれない最小の自然数を求める関数
--
-- 自然数は0を含む
--
-- 前提条件1: 与えられたリストには順序がついていない
--
-- 前提条件2: 要素は重複していない
minfree :: [Int] -> Int
minfree xs = head ([0..] \\ xs)

-- | リスト us から vs に含まれる要素をすべて除いた残りの要素のリストを返す
(\\) :: Eq a => [a] -> [a] -> [a]
us \\ vs = filter (`notElem` vs) us

-- |
-- 引数として論理値の配列を取る
-- 論理値のリストに変換して True エントリーからなる最長先頭部分列の長さを返す
search :: Array Int Bool -> Int
search = length . takeWhile id . elems

-- | リストから配列への変換
checklist :: [Int] -> Array Int Bool
checklist xs = accumArray (||) False (0, n) (zip (filter (<= n) xs) (repeat True))
  where n = length xs

-- | checklist の置き換え
countlist :: [Int] -> Array Int Int
countlist xs = accumArray (+) 0 (0,n) (zip xs (repeat 1))
  where n = maximum xs

sort :: [Int] -> [Int]
sort xs = concat [replicate k x | (x,k) <- assocs (countlist xs)]

-- | Data.Array.ST モジュールを使った checklist
checklist' :: [Int] -> Array Int Bool
checklist' xs = runSTArray $ do
  a <- newArray (0, n) False
  sequence [writeArray a x True | x <- xs, x<=n]
  return a
  where n = length xs

-- | リストを述語 p を満たす要素と満たさない要素のリストに分割する
partition :: (Int -> Bool) -> [Int] -> ([Int], [Int])
partition p xs = (filter p xs, filter (not . p) xs)

-- | 最終的な minfree
minfree' :: [Int] -> Int
minfree' xs = minfrom 0 (length xs, xs)

-- | minfree の一般化
minfrom :: Int -> (Int, [Int]) -> Int
minfrom a (n, xs)
  | n == 0 = a
  | m == b - a = minfrom b (n-m, vs)
  | otherwise = minfrom a (m,us)
    where
      (us, vs) = partition (<b) xs
      b = a + 1 + n `div` 2
      m = length us

テストの作成

Haskell のテストパッケージの代表的なものとして hspec, tasty, doctest, QuickCheck があります。

  • hspectasty は関数の振る舞いをテスト (単体テスト) を記述するためのフレームワークです。
  • doctest ではドキュメントに記載されている内容をテストすることで、正しい内容であることを保証します。
  • QuickCheck は単体テストでは想定していなかったエッジケースをプログラムが生成するランダムな入力によってあぶり出すことができます。

また、関数をリファクタリングする際にリファクタリング前と後の関数が同一であるという性質をテストできるようになります。

例えば、 minfree と改良後の minfree' がランダムな入力に対して、同様の結果を返すことで、関数の性質をチェックします。

hspec

hspec-discover のインストール

hspec-discover を利用することで、それぞれのソースファイルと一対一に対応した Spec ファイルを自動的に読み込んでテストしてくれるようになります。

hspec-discoverのインストール
$ stack install hspec-discover

テストの作成

まずは hspec を使うために、package.yamltestshspec パッケージを追記します。

package.yaml
tests:
  PFAD-test:
    main:                Spec.hs
    source-dirs:         test
    ghc-options:
    - -threaded
    - -rtsopts
    - -with-rtsopts=-N
    dependencies:
    - PFAD
    - hspec # ここを追記

hspec-discover を利用するためには test/Spec.hs の内容を以下のように書き換えます。

test/Spec.hs
{-# OPTIONS_GHC -F -pgmF hspec-discover #-}

Spec ファイルの命名規則は、 src/Minfree.hs に対しては test/MinfreeSpec.hs という感じです。

test/MinfreeSpec.hs
module MinfreeSpec (spec) where

import Test.Hspec
import Minfree

spec :: Spec
spec = do
  describe "minfree" $ do
    it "本に載っている例" $ do
      minfree [8,23,9,0,12,11,1,10,13,7,41,4,14,21,5,17,3,19,2,6] `shouldBe` 15

  describe "minfree'" $ do
    it "本に載っている例" $ do
      minfree' [8,23,9,0,12,11,1,10,13,7,41,4,14,21,5,17,3,19,2,6] `shouldBe` 15

上記の書き方で minfree 関数と minfree' 関数の入力と出力の振る舞いがテストできるようになりました。

テストの実行

最後に以下のコマンドでテストを実行します。

テストの実行
$ stack test
Registering PFAD-0.1.0.0...
PFAD-0.1.0.0: test (suite: PFAD-test)

Progress: 1/2
Minfree
  minfree
    本に載っている例
  minfree'
    本に載っている例

Finished in 0.0003 seconds
2 examples, 0 failures

PFAD-0.1.0.0: Test suite PFAD-test passed
Completed 2 action(s).
ExitSuccess

QuickCheck

QuickCheck は凄く面白いので、Haskeller なら使いこなしたいところです。しかしながら、慣れるまでは結構難しいので実例を見ながら使い方を理解していきたいと思います。

パッケージのインストール

HSpec の時と同様に package.yamltestsQuickCheck パッケージを追記します。quickcheck では無いのでスペルミスに注意してください。

package.yaml
tests:
  PFAD-test:
    main:                Spec.hs
    source-dirs:         test
    ghc-options:
    - -threaded
    - -rtsopts
    - -with-rtsopts=-N
    dependencies:
    - PFAD
    - hspec
    - QuickCheck # この行を追記

QuickCheck パッケージは更新が頻繁に行われているのでバージョンごとに書き方が違う場合があります。今回明示的には指定していませんが、QuickCheck-2.11.3 として進めていこうと思います。

QuickCheck に慣れよう!

まずは QuickCheck が生成するランダムな値について理解を深めたいと思います。

この sample 関数を使うことによって、どんな値が生成されるのかデバッグすることができます。型を見る通り Gen a の値を適用すれば良さそうに見えますが、ここが少し変わっているので注意してください。

対話環境での実行
$ stack ghci --no-load --package QuickCheck
> import Test.QuickCheck
> :t sample
sample :: Show a => Gen a -> IO ()

実際に値をいくつか生成してみます。

sample関数の使い方
> sample (arbitrary :: Gen [Int])
[]
[]
[-1,1,-1]
[5,-5]
[-4,-6,-7,-7,1,2,3,8]
[10,3,-2,1]
[11,-8,-11,12,-4,5,-10]
[-3,9,7,6,1]
[1,-12,11,3,8,11,-1,-16]
[-18,-9,-9,-18,9,-15,-3,15,-4,3,-10,8,13,8,15,6,13]
[-9,-15,-4,-2,-7,-1,-2]

> sample (arbitrary :: Gen [Int])
[]
[-2,-2]
[]
[-2]
[8,2,6,-7,7,7,-3,2]
[-6,2,4,-6]
[-5,6,6]
[]
[-6,4,4,1,5,-5,13,-2]
[-1,-17,16]
[-8,17,15,13]

> sample (arbitrary :: [Int])
<interactive>:6:9: error:
     Couldn't match expected type [Int] with actual type Gen a0
     In the first argument of sample, namely (arbitrary :: [Int])
      In the expression: sample (arbitrary :: [Int])
      In an equation for it: it = sample (arbitrary :: [Int])

<interactive>:6:9: error:
     Couldn't match expected type Gen () with actual type [Int]
     In the first argument of sample, namely (arbitrary :: [Int])
      In the expression: sample (arbitrary :: [Int])
      In an equation for it: it = sample (arbitrary :: [Int])

ここで重要な点は2つです。

  • arbitrary :: Gen [Int]
  • 生成される値は実行のたびにランダムに変化する

面白いので他にも生成してみます。1行で表示させるために sample' を利用することにします。

sample'の使い方
> sample' (arbitrary :: Gen Int)
[0,-2,-2,6,6,-2,12,9,-12,-16,5]

> sample' (arbitrary :: Gen [Int])
[[],[0],[2],[-4,1,-2,3,-1],[-6,-3,-3,3,8,-2,-6,8],[9,-1,2,10,1],[-1,1,7,-11,-5,-5,1,-8],[7,-8,2,11,9,-10,2,-5,2],[-6,-13,-13,15,5,8,11,5,3,-9,-14,-8,-11,-13,4,-15],[-10,-11,11,-9,13,-15,14,-14,1,9],[1,-13,-11,0,-16,7,17,-12,-6,-13,-11,-12,9,11]]

> sample' (arbitrary :: Gen Bool)
[True,False,True,True,True,True,False,False,True,False,True]

> sample' (arbitrary :: Gen (Maybe Bool))
[Just True,Just True,Just False,Nothing,Nothing,Just False,Nothing,Just False,Nothing,Just True,Just False]

> sample' (arbitrary :: Gen [a])
<interactive>:12:10: error:
     No instance for (Arbitrary a1) arising from a use of arbitrary
      Possible fix:
        add (Arbitrary a1) to the context of
          an expression type signature:
            forall a1. Gen [a1]
     In the first argument of sample', namely
        (arbitrary :: Gen [a])
      In the expression: sample' (arbitrary :: Gen [a])
      In an equation for it: it = sample' (arbitrary :: Gen [a])

このように、生成したい値の型を Gen aa に指定してあげることでランダムな値を生成できることがわかりました。また、多相型についてはエラーになります。

色々な関数

他にも、QuickCheck モジュールではいくつか便利な関数を提供しています。

choose
-- 与えられた範囲でランダムな値を生成する
choose :: Random a => (a, a) -> Gen a
> sample' (choose (1,10))
[1,6,10,1,3,1,9,1,1,10,5]
elements
-- 与えられたリストの中からランダムに値を生成する
elements :: [a] -> Gen a
> sample' (elements ["patek", "omega", "seiko"])
["omega","patek","omega","omega","patek","patek","omega","omega","patek","omega","patek"]
oneof
-- 与えられたジェネレータのリストの中からランダムに選択し、それを利用してランダムな値を生成する
> let genHighPriceWatch = elements ["patek", "ap", "vc"]
> let genMiddlePriceWatch = elements ["seiko", "omega", "rolex"]
> sample' (oneof [genHighPriceWatch, genMiddlePriceWatch])
["vc","omega","seiko","patek","patek","rolex","rolex","rolex","seiko","omega","seiko"]
frequency
-- 出現頻度を指定してランダムな値を生成する (この例では1/100,99/100の出現確率に設定)
frequency :: [(Int, Gen a)] -> Gen a
> let genHighPriceWatchWithFreq = (1, genHighPriceWatch)
> let genMiddlePriceWatchWithFreq = (99, genMiddlePriceWatch)
> sample' (frequency [genHighPriceWatchWithFreq, genMiddlePriceWatchWithFreq])
["seiko","rolex","seiko","omega","seiko","seiko","omega","rolex","omega","omega","omega"]
suchThat
-- ランダムに生成された値に対して、与えられた条件満たす値のみを生成する
suchThat :: suchThat :: Gen a -> (a -> Bool) -> Gen a
> sample' ((arbitrary :: Gen Int) `suchThat` even)
[-2,0,-4,2,4,6,-10,-10,-8,4,6]

> sample' ((arbitrary :: Gen Int) `suchThat` (>0))
[3,1,5,1,1,10,3,14,11,10,21]
listOf
-- ランダムに生成された値のリストを生成する
> sample' (listOf (arbitrary:: Gen Int))
[[],[1,2],[-1,2,-4,2],[5,-1],[6,-3,6,4,0],[-10,8,-10,-5,-2,0,2,-10],[5,5,9,-7,-8,-8,7,9,-6,11],[],[4,14,13,0,13,5],[0,9],[17,-12,12,-3,-7,-13,-1,-1,-19,10,11,16,2,-20,-5,-4,0,12]]
listOf1
-- 空リストを除く
> sample' (listOf1 (arbitrary:: Gen Int))
[[0],[-1,-1],[4,1,4,0],[2,0],[1,-1,-7,8,-2,1,2,7],[-2,-6,-6,2],[11,-3,-5,4,1,0,-3,9,-10],[2,2,-9,-14,5,13,-13,11,14,12,-9,12,13],[7,10,5,-12,-4],[8,-16],[3,14,2,-17,3,-18,-4,17,16,-2,8,-11,14,20,-1,-10,2]]
vectorOf
-- 長さを指定してランダムな値のリストを生成する
> sample' (vectorOf 3 (arbitrary:: Gen Int))
[[0,0,0],[2,1,1],[3,-1,-2],[5,-2,-5],[2,1,5],[5,-6,4],[2,-12,8],[4,-2,-1],[2,9,-6],[-11,18,-6],[17,-7,-15]]
shuffle
-- 与えられたリストをシャッフルしたランダムな値のリストを生成する
> sample' (shuffle [1..5])
[[2,5,1,3,4],[3,4,2,1,5],[1,4,5,2,3],[4,1,2,5,3],[5,2,4,1,3],[3,1,4,2,5],[1,3,5,4,2],[1,4,2,3,5],[5,2,3,1,4],[1,4,5,2,3],[4,3,2,1,5]]

結構たくさんあるので、基本的なテストであればこれらの関数で十分対応可能です。

vector
> sample' $ (vector 3 :: Gen [Int])
[[0,0,0],[2,1,1],[4,-2,2],[3,3,3],[-8,4,5],[5,-10,10],[11,10,4],[-12,9,7],[8,-8,-5],[-15,-3,7],[12,-9,1]]
orderedList
> sample' (orderedList :: Gen [Int])
[[],[],[1,3],[-4,-2],[-6,-6,6],[-4,-2,0,5,6,8,9,9,10],[-12,-11,-3,1,2,4,5],[-12,-12,-10,-7,9,10,10,11,14],[11],[-17,-16,-11,-8,-3,14],[-19,-19,-15,-14,-12,-1,0,1,4,5,14,18,20]]

ここまで具体例をいくつか見てきたので、QuickCheck を使う際には arbitrary に具体的な型を指定してあげれば良さそうだということがわかってきました。

また、arbitraryArbitrary 型クラスのメソッドとなっているため、適切にインスタンスを定義してしまえば、自分で定義した型の値をランダムに生成することも可能です。

実際に QuickCheck のテストを書いてみる

Peals の問題は関数に制約をつけていることが多いため、良い練習になりそうです。

今回の制約は次の通りです。

  • 自然数
  • 重複しない

上記を満たすリストが入力値となります。

素朴に思いつく定義

慣習として property のための関数の接頭辞には prop をつけます。つけなくても問題は無いです。

test/MinfreeSpec.hs
module MinfreeSpec (spec) where

import Test.Hspec
import Minfree
import Test.Hspec.QuickCheck (prop)
import Test.QuickCheck

spec :: Spec
spec = do
  describe "minfree" $ do
    it "書籍の例" $ do
      minfree [8, 23, 9, 0, 12, 11, 1, 10, 13, 7, 41, 4, 14, 21, 5, 17, 3, 19, 2, 6] `shouldBe` 15
  describe "minfree'" $ do
    it "書籍の例" $ do
      minfree' [8, 23, 9, 0, 12, 11, 1, 10, 13, 7, 41, 4, 14, 21, 5, 17, 3, 19, 2, 6] `shouldBe` 15
  describe "minfree == minfree'" $ do
    prop "minfree == minfree'" prop_Minfree

prop_Minfree :: [Int] -> Bool
prop_Minfree xs = minfree xs == minfree' xs

このコードに対してテストを実行すると次のエラーが表示されます。

$ stack test
...

Minfree
  minfree
    書籍の例
  minfree'
    書籍の例
  minfree == minfree'
    minfree == minfree' FAILED [1]

Failures:

  test/MinfreeSpec.hs:17:5:
  1) Minfree, minfree == minfree', minfree == minfree'
       Falsifiable (after 3 tests and 2 shrinks):
         [-1]

  To rerun use: --match "/Minfree/minfree == minfree'/minfree == minfree'/"

Randomized with seed 901529074

Finished in 0.0013 seconds
3 examples, 1 failure
...

エラーメッセージから、どうやらランダムに生成された値に [-1] が含まれてたようです。まずはこれを改良してみます。

また、ランダムテストなのでもう一度同じテストを再現したい場合は Randomized with seed 901529074 のシードを与えれば良いです。

自然数に限定する

やり方は色々とあると思いますが、今回は Positive 型を利用することにします。

test/Minfree.hs
prop_Minfree :: [Positive Int] -> Bool
prop_Minfree xs = minfree ns == minfree' ns
  where
    ns = map getPositive xs

この結果、また別のエラーが出るようになりました。

$ stack test
...
Failures:

  test/MinfreeSpec.hs:17:5:
  1) Minfree, minfree == minfree', minfree == minfree'
       Falsifiable (after 5 tests and 2 shrinks):
         [Positive {getPositive = 1},Positive {getPositive = 1}]

  To rerun use: --match "/Minfree/minfree == minfree'/minfree == minfree'/"

Randomized with seed 173572505

Finished in 0.0014 seconds
3 examples, 1 failure
...

今回のエラーでは [1,1] のような重複した値の場合にテストが失敗しています。これも修正しましょう。

重複をなくす

(==>) を使って minfree に適用する前に事前条件を設定しておくことにします。

test/Minfree.hs
-- import 文を追記
import Data.List (nub)

prop_Minfree :: [Positive Int] -> Property
prop_Minfree xs = preCondition ==> minfree ns == minfree' ns
  where
    ns = map getPositive xs
    preCondition = length (nub xs) == length xs

これで QuciCheck を記述することができました。

QuickCheck を適用した最終的なコードは以下の通りです。

test/MinfreeSpec.hs
module MinfreeSpec (spec) where

import Test.Hspec
import Minfree
import Test.Hspec.QuickCheck (prop)
import Test.QuickCheck
import Data.List (nub)

spec :: Spec
spec = do
  describe "minfree" $ do
    it "本に載っている例" $ do
      minfree [8, 23, 9, 0, 12, 11, 1, 10, 13, 7, 41, 4, 14, 21, 5, 17, 3, 19, 2, 6] `shouldBe` 15
  describe "minfree'" $ do
    it "本に載っている例" $ do
      minfree' [8, 23, 9, 0, 12, 11, 1, 10, 13, 7, 41, 4, 14, 21, 5, 17, 3, 19, 2, 6] `shouldBe` 15
  describe "minfree == minfree'" $ do
    prop "minfree == minfree'" prop_Minfree

prop_Minfree :: [Positive Int] -> Property
prop_Minfree xs = preCondition ==> minfree ns == minfree' ns
  where
    ns = map getPositive xs
    preCondition = length (nub xs) == length xs

QuickCheck は本当に優秀で、自分の書いたコードに安心感をあたえてくれます。しかし、導入までのハードルが少し高いと思うので、日本語による実践的なチュートリアルがもう少し増えて欲しいところです。

doctest

doctest は一言で言えば、Haddock にテストを埋め込んだものです。

例として、以下のような関数とコメントがあった場合に

-- 与えられた価格の消費税を計算する
-- calcSalesTax 100.0 == 8.0
calcSalesTax :: Num a => a -> a
calcSalesTax = (*0.08)

消費税が10%に変更になったとしたらプログラムは以下のように修正されるでしょう。

-- 与えられた価格の消費税を計算する
-- calcSalesTax 100.0 == 8.0
calcSalesTax :: Num a => a -> a
calcSalesTax = (*0.1)

この時、ドキュメントも同様に修正されるべきですが、その保証はどこにもありません。この問題に対する有効な解決策が doctest です。

doctest の準備

まずは package.yamldoctest のための記述を追加しましょう。

package.yaml
tests:
  PFAD-test:
    main:                Spec.hs
    source-dirs:         test
    ghc-options:
    - -threaded
    - -rtsopts
    - -with-rtsopts=-N
    dependencies:
    - PFAD
    - hspec
    - QuickCheck
  # ここから下の行を追記
  PFAD-doctest:
    main: test/doctests.hs
    ghc-options:
    - -threaded
    - -rtsopts
    - -with-rtsopts=-N
    dependencies:
    - PFAD
    - doctest

このままでは doctests.hs が見つからずにエラーになるため、以下のファイルを作成します。このファイルに doctest の対象ファイルを記述します。

test/doctests.hs
module Main (main) where

import Test.DocTest

main :: IO ()
main = doctest ["-isrc", "src/Minfree.hs"]

これで準備は完了です。

doctest を実行するためには以下のように stack test コマンドを実行します。

$ stack clean
$ stack test

この場合、さきほど定義した hspecQuickCheck のテストも実行されてしまいます。

以下のように個別に実行することもできます。

$ stack test PFAD:test:PFAD-doctest
...
PFAD-0.1.0.0: test (suite: PFAD-doctest)

Progress 1/2: PFAD-0.1.0.0Examples: 0  Tried: 0  Errors: 0  Failures: 0

PFAD-0.1.0.0: Test suite PFAD-doctest passed
...

現時点では doctest を書いていないため、何も起きません。

doctest の書き方

doctest は以下のように、非常に直感的に記述することができます。

doctest>>> に続く文字列が ghci によって処理され、その下の行の結果と等しいかどうかをテストするだけです。

-- |
-- >>> add 2 3
-- 5
add x y = x + y

それでは minfree 関数に doctest を記述してみましょう。また、最初なので、間違ったテスト結果を書いてみます。

src/Minfree.hs
-- |
-- 与えられた自然数のリストに含まれない最小の自然数を求める関数
--
-- 自然数は0を含む
--
-- 前提条件1: 与えられたリストには順序がついていない
--
-- 前提条件2: 要素は重複していない
--
-- >>> minfree [8, 23, 9, 0, 12, 11, 1, 10, 13, 7, 41, 4, 14, 21, 5, 17, 3, 19, 2, 6]
-- "abcde"
minfree :: [Int] -> Int
minfree xs = head ([0..] \\ xs)

本当にテストが失敗するか確認します。

$ stack test PFAD:test:PFAD-doctest
...
PFAD-0.1.0.0: test (suite: PFAD-doctest)

src/Minfree.hs:15: failure in expression `minfree [8, 23, 9, 0, 12, 11, 1, 10, 13, 7, 41, 4, 14, 21, 5, 17, 3, 19, 2, 6]'
expected: "abcde"
 but got: 15

Examples: 1  Tried: 1  Errors: 0  Failures: 1

PFAD-0.1.0.0: Test suite PFAD-doctest failed
...

確かに失敗していることが確認できました。では、正しいテスト結果を記述してみましょう。

src/Minfree.hs
-- |
-- 与えられた自然数のリストに含まれない最小の自然数を求める関数
--
-- 自然数は0を含む
--
-- 前提条件1: 与えられたリストには順序がついていない
--
-- 前提条件2: 要素は重複していない
--
-- >>> minfree [8, 23, 9, 0, 12, 11, 1, 10, 13, 7, 41, 4, 14, 21, 5, 17, 3, 19, 2, 6]
-- 15
minfree :: [Int] -> Int
minfree xs = head ([0..] \\ xs)

もう一度テストしてみます。

$ stack test PFAD:test:PFAD-doctest
...

PFAD-0.1.0.0: test (suite: PFAD-doctest)

Examples: 1  Tried: 1  Errors: 0  Failures: 0 0  Errors: 0  Failures: 0

PFAD-0.1.0.0: Test suite PFAD-doctest passed
$ firefox haddock/Minfree.html

今度はちゃんとテストをパスし、以下のようなドキュメントが生成されると思います。

スクリーンショット 2017-12-11 18.27.52.png

また QuichCheck を使った書き方もできます。その場合は >>>prop> にするだけです。

src/Minfree.hs
-- Haddock に表示させたいのでエクスポートしています
module Minfree (minfree, minfree', (\\)) where

...

-- | リスト us から vs に含まれる要素をすべて除いた残りの要素のリストを返す
--
-- prop> (as ++ bs) \\ cs == (as \\ cs) ++ (bs \\ bs)
(\\) :: Eq a => [a] -> [a] -> [a]
us \\ vs = filter (`notElem` vs) us

この性質は満たされないためテストに失敗します。

$ stack test PFAD:test:PFAD-doctest
PFAD-0.1.0.0: test (suite: PFAD-doctest)

src/Minfree.hs:22: failure in expression `(as ++ bs) \\ cs == (as \\ cs) ++ (bs \\ bs)'
*** Failed! Falsifiable (after 3 tests and 1 shrink):
[]
[0]
[]

Examples: 2  Tried: 2  Errors: 0  Failures: 1

PFAD-0.1.0.0: Test suite PFAD-doctest failed

正しく書き換えた場合はテストに通ります。ついでに、残りのプロパティテストも追加しておきます。

src/Minfree.hs
-- | リスト us から vs に含まれる要素をすべて除いた残りの要素のリストを返す
--
-- prop> (as ++ bs) \\ cs == (as \\ cs) ++ (bs \\ cs)
--
-- prop> as \\ (bs ++ cs) == (as \\ bs) \\ cs
--
-- prop> (as \\ bs) \\ cs == (as \\ cs) \\ bs
(\\) :: Eq a => [a] -> [a] -> [a]
us \\ vs = filter (`notElem` vs) us

実行するとちゃんとテストをパスしてドキュメントを生成していると思います。

$ stack test PFAD:test:PFAD-doctest
...
PFAD-0.1.0.0: test (suite: PFAD-doctest)

Examples: 4  Tried: 4  Errors: 0  Failures: 0 0  Errors: 0  Failures: 0

PFAD-0.1.0.0: Test suite PFAD-doctest passed
...

スクリーンショット 2017-12-11 18.57.01.png

なんかかっこいい感じのドキュメントになってきました!

こんな感じでドキュメントも手軽にテストできるので、ぜひアプリケーションを開発する際に利用してください。

プログラムの配布

docker integration

stack を利用するメリットの1つに docker integration があります。

これは、stack build を行なった際に、Docker image 内で自動的にビルドを行うという優れものです。

Docker についての解説は公式のドキュメントなどをご確認ください。

stack docker ビルド

自分のプロジェクトを Docker でビルドするためには --docker オプションを追加します。


$ stack build --docker
Error: No such object: fpco/stack-build:lts-12.0
Received ExitFailure 1 when running
Raw command: /usr/bin/docker inspect fpco/stack-build:lts-12.0
Standard output:

[]

エラーになってしまいました。これはベースイメージに fpco/stack-build:lts-12.0 を指定したけど、イメージが見つからなかったのでビルドできなかったよ。というエラーです。

この場合 stack docker pull コマンドでイメージを pull します。今回は lts-12.0 のタグがついていますが、利用している resolver によって変わります。

ベースイメージのプル
$ stack docker pull
Pulling image from registry: 'fpco/stack-build:lts-12.0'
lts-12.0: Pulling from fpco/stack-build
b234f539f7a1: Pull complete
55172d420b43: Pull complete
5ba5bbeb6b91: Pull complete
43ae2841ad7a: Pull complete
f6c9c6de4190: Pull complete
52b26204f9af: Pull complete
916b6abf261c: Pull complete
048ddb5a0825: Pull complete
db66cbec7a84: Pull complete
7b52cdfbea02: Pull complete
Digest: sha256:fdbabc6df1135ab640041c966a2f8ced3bdaff7226de4a41f52d8c08fc9d64c7
Status: Downloaded newer image for fpco/stack-build:lts-12.0

では、もう一度ビルドを試してみましょう。

$ stack build --docker
...

今度は成功しました!しかし、Docker 環境内でビルドできて何が嬉しいの?と思うかもしれません。

では、次に stack image container コマンドを紹介します。このコマンドによって Docker イメージを作ることができます。

実行ファイルが入った Docker イメージを作る

まずは stack.yaml に以下の設定を追記しましょう。

imageセクションの追加
image:
  container:
    name: pfad
    base: "fpco/stack-run"

以下のコマンドで Docker イメージが作成されます。

$ stack image container --docker
...
Sending build context to Docker daemon  2.014MB
Step 1/2 : FROM fpco/stack-run
 ---> cdabad604832
Step 2/2 : ADD ./ /
 ---> 33e6900547e1
Successfully built 33e6900547e1
Successfully tagged pfad:latest

一応出来上がったイメージを確認してみます。

$ docker images pfad
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
pfad                latest              33e6900547e1        25 seconds ago      1.52GB

イメージのサイズが結構やばいですが、これは stack.yamlbase: "fpco/stack-run" として fpco/stack-run イメージを指定したからです。基本的には自分でカスタマイズしたイメージを利用することになります。

生成されたバイナリファイルはデフォルトでは /usr/local/bin に保存されます。

$ docker run --rm -it pfad ls -l /usr/local/bin
total 1968
-rwxr-xr-x 1 root root 1004528 Jul 16 08:28 minfree
-rwxr-xr-x 1 root root 1004528 Jul 16 08:28 minfree2

これで Docker イメージ内にバイナリファイルがあるので普通にコマンドを実行できます。

$ docker run --rm -it pfad minfree [0,1,3]
2

$ docker run --rm -it pfad minfree2 [0,1,3]
2

あとは、この Docker イメージを Docker Hub なりで公開すれば、誰でもすぐに実行できます。

Haskell 製の web アプリケーション をこのような方法で Docker イメージにして kubernetes にデプロイするスタイルにすると、めちゃめちゃ楽ですよ。

stack script

この方法は厳密には配布とは言えないかもしれませんが、とても小さな規模で、Qiita やブログに載せるようなコードの場合は stack script で実行してもらいましょう。

似たものに Script interpreter 形式もありますが、コードに余分な情報を追加しないといけないという点と、実行時にパッケージの指定をしなければならないという点であまりおすすめしていません。

具体的には以下のように MinfreeScript.hs を新しく定義したとしましょう。

MinfreeScript.hs
module MinfreeScript (main) where

import Data.Array (Array, elems, accumArray, assocs)
import Data.Array.ST (runSTArray, newArray, writeArray)
import System.Environment (getArgs)

main :: IO ()
main = do
  [xs] <- getArgs
  print $ minfree $ read xs

minfree :: [Int] -> Int
minfree xs = head ([0..] \\ xs)

(\\) :: Eq a => [a] -> [a] -> [a]
us \\ vs = filter (`notElem` vs) us

search :: Array Int Bool -> Int
search = length . takeWhile id . elems

checklist :: [Int] -> Array Int Bool
checklist xs = accumArray (||) False (0, n) (zip (filter (<= n) xs) (repeat True))
  where n = length xs

countlist :: [Int] -> Array Int Int
countlist xs = accumArray (+) 0 (0,n) (zip xs (repeat 1))
  where n = maximum xs

sort :: [Int] -> [Int]
sort xs = concat [replicate k x | (x,k) <- assocs (countlist xs)]

checklist' :: [Int] -> Array Int Bool
checklist' xs = runSTArray $ do
  a <- newArray (0, n) False
  sequence_ [writeArray a x True | x <- xs, x<=n]
  return a
  where n = length xs

partition :: (Int -> Bool) -> [Int] -> ([Int], [Int])
partition p xs = (filter p xs, filter (not . p) xs)

minfree' :: [Int] -> Int
minfree' xs = minfrom 0 (length xs, xs)

minfrom :: Int -> (Int, [Int]) -> Int
minfrom a (n, xs)
  | n == 0 = a
  | m == b - a = minfrom b (n-m, vs)
  | otherwise = minfrom a (m,us)
    where
      (us, vs) = partition (<b) xs
      b = a + 1 + n `div` 2
      m = length us

これを実行してみます。

$ stack script -- MinfreeScript.hs [0,1,2,3]
When using the script command, you must provide a resolver argument

謎のエラーが返ってきました。これは stack script コマンドを実行する際には必ず resolver を指定する必要があるためです。

$ stack script --resolver lts-12.0 -- MinfreeScript.hs [0,1,2,3]
Using resolver: lts-12.0 specified on command line
4

このように resolver でスナップショットを明示的に指定することで、いつ実行しても同じ結果になります。また、依存しているパッケージなどは自動的に解決してくれるため、とても便利です。

script interpreter + stack script でスクリプティング! にも使い方をまとめてあります。

HLint

hlint は自分のコードをより良くしてくれる静的解析ツールです。hlint の指摘に従うことでより Haskell らしい書き方が身につきます。

より詳細な内容は以下の記事をご確認ください。

HLint のインストール

stack があればインストールは簡単です。

HLintのインストール
$ stack install hlint
$ hlint --version
HLint v2.1.8, (C) Neil Mitchell 2006-2018

使い方もとても簡単です。検査したいディレクトリを指定するだけで再帰的にチェックしてくれます。

$ hlint app
No hints

$ hlint src
src/Minfree.hs:53:3: Warning: Use sequence_
Found:
  sequence [writeArray a x True | x <- xs, x <= n]
Why not:
  sequence_ [writeArray a x True | x <- xs, x <= n]

1 hint

上記のように、ヒントを表示してくれるため、これ通りに書き換えていくことで独学でも良いコードを書くことができます。

このヒントを修正することはとても簡単です。src/Minfree.hs:53:3 を指摘通りに修正するだけです。

src/Minfree.hs
-- | Data.Array.ST モジュールを使った checklist
checklist' :: [Int] -> Array Int Bool
checklist' xs = runSTArray $ do
  a <- newArray (0, n) False
  -- 修正
  sequence_ [writeArray a x True | x <- xs, x<=n]
  return a
  where n = length xs

このように修正したら HLint でもう一度確認してみましょう。

$ hlint src
No hints

プロジェクト全体をチェックしたい場合

$ hlint .
...
5 hints

test ディレクトリ以下のコードでまだ 5 件の修正が可能ですが、今回はこれで良いことにしましょう。

独自ルールの追加や、既存ルールの警告をOFFにすることもできるので、travis-cicircle ci などの継続的インテグレーションツールを使って hlint をビルドのたびに実行するとコードの品質をある程度保つことができると思います。

また ghc-modhaskell-ide-engine などと組み合わせ使うことで hlint を自動的に実行しながらコードを書くこともできます。

フォーマッター

Haskell にはソースコードフォーマットツールがいくつかあります。

どれを使うかは、好みの問題ですが、ここでは stylish-haskell を紹介します。

stylish-haskell

stylish-haskell はかなり控えめなフォーマットを行います。

stylish-haskellのインストール
$ stack install stylish-haskell
$ stylish-haskell --version
stylish-haskell 0.9.0.2

stylish-haskell をプロジェクトのコードに対して適用するためには以下のようなワンライナーを使います。

$ find . -type f -name "*hs" -not -path '.git' -not -path '*.stack-work*' -exec stylish-haskell -i {} \;

適用前

stylish-haskell適用前
module Minfree (minfree, minfree', (\\)) where

import Data.Array (Array, elems, accumArray, assocs)
import Data.Array.ST (runSTArray, newArray, writeArray)

-- |
-- 与えられた自然数のリストに含まれない最小の自然数を求める関数
--
-- 自然数は0を含む
--
-- 前提条件1: 与えられたリストには順序がついていない
--
-- 前提条件2: 要素は重複していない
--
-- >>> minfree [8, 23, 9, 0, 12, 11, 1, 10, 13, 7, 41, 4, 14, 21, 5, 17, 3, 19, 2, 6]
-- 15
minfree :: [Int] -> Int
minfree xs = head ([0..] \\ xs)

-- | リスト us から vs に含まれる要素をすべて除いた残りの要素のリストを返す
--
-- prop> (as ++ bs) \\ cs == (as \\ cs) ++ (bs \\ cs)
--
-- prop> as \\ (bs ++ cs) == (as \\ bs) \\ cs
--
-- prop> (as \\ bs) \\ cs == (as \\ cs) \\ bs
(\\) :: Eq a => [a] -> [a] -> [a]
us \\ vs = filter (`notElem` vs) us

-- |
-- 引数として論理値の配列を取る
-- 論理値のリストに変換して True エントリーからなる最長先頭部分列の長さを返す
search :: Array Int Bool -> Int
search = length . takeWhile id . elems

-- | リストから配列への変換
checklist :: [Int] -> Array Int Bool
checklist xs = accumArray (||) False (0, n) (zip (filter (<= n) xs) (repeat True))
  where n = length xs

-- | checklist の置き換え
countlist :: [Int] -> Array Int Int
countlist xs = accumArray (+) 0 (0,n) (zip xs (repeat 1))
  where n = maximum xs

sort :: [Int] -> [Int]
sort xs = concat [replicate k x | (x,k) <- assocs (countlist xs)]

-- | Data.Array.ST モジュールを使った checklist
checklist' :: [Int] -> Array Int Bool
checklist' xs = runSTArray $ do
  a <- newArray (0, n) False
  sequence_ [writeArray a x True | x <- xs, x<=n]
  return a
  where n = length xs

-- | リストを述語 p を満たす要素と満たさない要素のリストに分割する
partition :: (Int -> Bool) -> [Int] -> ([Int], [Int])
partition p xs = (filter p xs, filter (not . p) xs)

-- | 最終的な minfree
minfree' :: [Int] -> Int
minfree' xs = minfrom 0 (length xs, xs)

-- | minfree の一般化
minfrom :: Int -> (Int, [Int]) -> Int
minfrom a (n, xs)
  | n == 0 = a
  | m == b - a = minfrom b (n-m, vs)
  | otherwise = minfrom a (m,us)
    where
      (us, vs) = partition (<b) xs
      b = a + 1 + n `div` 2
      m = length us

適用後

stylish-haskell適用後
module Minfree (minfree, minfree', (\\)) where

import           Data.Array    (Array, accumArray, assocs, elems)
import           Data.Array.ST (newArray, runSTArray, writeArray)

-- |
-- 与えられた自然数のリストに含まれない最小の自然数を求める関数
--
-- 自然数は0を含む
--
-- 前提条件1: 与えられたリストには順序がついていない
--
-- 前提条件2: 要素は重複していない
--
-- >>> minfree [8, 23, 9, 0, 12, 11, 1, 10, 13, 7, 41, 4, 14, 21, 5, 17, 3, 19, 2, 6]
-- 15
minfree :: [Int] -> Int
minfree xs = head ([0..] \\ xs)

-- | リスト us から vs に含まれる要素をすべて除いた残りの要素のリストを返す
--
-- prop> (as ++ bs) \\ cs == (as \\ cs) ++ (bs \\ cs)
--
-- prop> as \\ (bs ++ cs) == (as \\ bs) \\ cs
--
-- prop> (as \\ bs) \\ cs == (as \\ cs) \\ bs
(\\) :: Eq a => [a] -> [a] -> [a]
us \\ vs = filter (`notElem` vs) us

-- |
-- 引数として論理値の配列を取る
-- 論理値のリストに変換して True エントリーからなる最長先頭部分列の長さを返す
search :: Array Int Bool -> Int
search = length . takeWhile id . elems

-- | リストから配列への変換
checklist :: [Int] -> Array Int Bool
checklist xs = accumArray (||) False (0, n) (zip (filter (<= n) xs) (repeat True))
  where n = length xs

-- | checklist の置き換え
countlist :: [Int] -> Array Int Int
countlist xs = accumArray (+) 0 (0,n) (zip xs (repeat 1))
  where n = maximum xs

sort :: [Int] -> [Int]
sort xs = concat [replicate k x | (x,k) <- assocs (countlist xs)]

-- | Data.Array.ST モジュールを使った checklist
checklist' :: [Int] -> Array Int Bool
checklist' xs = runSTArray $ do
  a <- newArray (0, n) False
  sequence_ [writeArray a x True | x <- xs, x<=n]
  return a
  where n = length xs

-- | リストを述語 p を満たす要素と満たさない要素のリストに分割する
partition :: (Int -> Bool) -> [Int] -> ([Int], [Int])
partition p xs = (filter p xs, filter (not . p) xs)

-- | 最終的な minfree
minfree' :: [Int] -> Int
minfree' xs = minfrom 0 (length xs, xs)

-- | minfree の一般化
minfrom :: Int -> (Int, [Int]) -> Int
minfrom a (n, xs)
  | n == 0 = a
  | m == b - a = minfrom b (n-m, vs)
  | otherwise = minfrom a (m,us)
    where
      (us, vs) = partition (<b) xs
      b = a + 1 + n `div` 2
      m = length us

こんな感じで、デフォルトで自分のコードがあまり変更されないため、個人的には結構好きです。

stylish-haskellにも少しまとめてあります。

hoogle

開発を進めて行くとこんなことを良く思います。

  • 型で関数を検索できないかな?
  • 関数名の一部は覚えているんだけど、全部の名前は忘れた
  • コードに書かれている関数がどのモジュールで定義されているか知りたい

Haskell に詳しい方であれば hayoohoogle を使って上記の問題を解決していると思います。個人的には hayoo が好きです。

これらのツールで基本的には事足りるのですが、stack hoogle を使うと以下のようなメリットがあります。

  • 現在指定しているプロジェクトの lts のバージョンに基づいて hoogle 検索が可能になる
  • 自分で定義した関数も hoogle 検索の対象となる (エクスポートしていれば)
  • オフラインで利用できる

実際に使う際も非常に簡単です。stack hoogle --setup をしていない場合は初回実行時に少し時間がかかります。

グローバルデータベースの作成
$ stack exec -- hoogle generate
...
プロジェクトデータベースの作成
$ stack hoogle
...
hoogleの利用例
$ stack hoogle "con map"
Prelude concatMap :: Foldable t => (a -> [b]) -> t a -> [b]
Data.List concatMap :: Foldable t => (a -> [b]) -> t a -> [b]
Data.Foldable concatMap :: Foldable t => (a -> [b]) -> t a -> [b]
GHC.OldList concatMap :: (a -> [b]) -> [a] -> [b]
...

$ stack hoogle "minfree"
Ch01.Minfree minfree :: [Int] -> Int
module Ch01.Minfree
Ch01.Minfree minfree' :: [Int] -> Int

$ stack hoogle "a -> a"
Prelude id :: a -> a
Data.Function id :: a -> a
GHC.Exts breakpoint :: a -> a
GHC.Exts lazy :: a -> a
...

$ stack hoogle -- "a -> a" --count=20
Prelude id :: a -> a
Data.Function id :: a -> a
GHC.Exts breakpoint :: a -> a
GHC.Exts lazy :: a -> a
...

プロファイリング

$ stack build --profile
$ stack exec -- <bin_name> +RTS -p -hb

# テストのプロファイリング
$ stack test --profile --test-arguments "+RTS -hm"

$ hp2ps -e8in -c <proj_name>.hp

コマンドレファレンス

stack build

並列ビルドについて

stack では自動的にビルドが並列化される (現在のCPUのコア数を自動的に設定する) ため、明示的に -j オプションを渡す必要はありません。(むしろ、現状は間違ったコア数を渡してしまうとパフォーマスに悪影響を及ぼすバグがあるため、非推奨です。)

並列化オプションとして -jN が提供されていますが、以下のようにコア数を抑制したい場合にのみ、利用した方が良いでしょう。(-j はエラーになります)

# 自動的に並列化してビルド
$ stack build

# コア数1でビルド
$ stack build -j1

stack.yaml の書式。

jobs: 1

stack hoogle

# グローバルデータベースの生成
$ stack exec -- hoogle generate
# プロジェクトデータベースの生成
$ stack hoogle
# 検索
$ stack hoogle "<search_text>"

stack clean

ビルド結果はキャッシュされてしまうので、警告とか見たい時の強制リビルトとかで良く使う。

通常は .stack-work/dist 以下のパッケージディレクトリを削除します。パッケージを指定して個別に削除することも可能です。また、--full オプションをつけると .stack-work ディレクトリが丸ごと削除されます。

$ stack clean
$ stack clean <package>
$ stack clean --full

stack exec

stack exec は通常、ビルドしたバイナリを実行するために利用します。

$ stack build
$ stack exec -- <binary>

また、あまり知られていませんが stack exec コマンドは shell で利用できるコマンドがそのまま使えます。

$ stack exec -- ls
$ stack exec -- env

これを少し応用すると、ビルドしたバイナリのパスを簡単に取得することができます。

$ stack exec -- which <binary>

デプロイスクリプトなどで使うと便利です。

stack path

基本的にはあまり使いません。
stack で問題が発生した際、この内容も教えてあげると解決しやすくなります。

$ stack path
....

stack unpack

Hackage に登録されているパッケージをローカルに保存します。

$ stack unpack <package>

git にソースコードがあれば git clone で良いのですが、古いパッケージだと Hackage に登録されているソースコードしかない場合もあるため、そのような場合に使います。

自分で Hackage からパッケージをダウンロードして tar.gz を展開することと同じですが、こっちの方が楽です。

stack test

stack test --test-arguments

テスト時に引数を渡したい場合に使います。
プロファイルのオプションを指定する際や、 tasty-html などで使う場合があります。

# 書式
$ stack test --test-arguments="<option>"

# 実際の使い方
$ stack test --profile --test-arguments "+RTS -hm"
$ stack test --test-arguments="--html results.html"

stack templates

利用可能なプロジェクトテンプレートの一覧をリストアップするコマンド。

$ stack templates
Template                    Description
chrisdone
foundation                - Project based on an alternative prelude with batteries and no dependencies.
franklinchen
ghcjs                     - Haskell to JavaScript compiler, based on GHC
ghcjs-old-base
hakyll-template           - a static website compiler library
haskeleton                - a project skeleton for Haskell packages
hspec                     - a testing framework for Haskell inspired by the Ruby library RSpec
new-template
protolude                 - Project using a custom Prelude based on the Protolude library
quickcheck-test-framework - a library for random testing of program properties
readme-lhs                - small scale, quick start, literate haskell projects
rubik
scotty-hello-world
scotty-hspec-wai
servant                   - a set of packages for declaring web APIs at the type-level
servant-docker
simple
simple-hpack
simple-library
spock                     - a lightweight web framework
tasty-discover            - a project with tasty-discover with setup
tasty-travis
unicode-syntax-exe
unicode-syntax-lib
yesod-minimal
yesod-mongo
yesod-mysql
yesod-postgres
yesod-postgres-fay
yesod-simple
yesod-sqlite

参考

stack

doctest

hspec

QuickCheck

tools

hoogle

hpack

haddock

GHC

Glasgow Haskell Compiler User's Guide