Haskell
stack

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

この記事は定期的にメンテナンス(加筆・修正)されています。

そのため、記事に追加して欲しい内容があればコメントをお願いします。できるだけメンテナンスの際に反映させたいと思います。


本記事のコードについては waddlaw/Qiita-stack に置いてありますので、適宜ご活用ください。

Haskell の開発方法については以下のサイトに色々とまとめていますので、こちらもご参照ください。


TODO: liquidhaskell, profiling, circle ci, gauge, default-extension, ghc-option, tasty について追記する。

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

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

Ubuntu 18.04 LTS or Mac で動作確認を行なっています。


エディタ (IDE) について

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

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

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


stack のバージョンについて

2019年1月6日現在の stack の最新バージョンは 1.9.3 です。

--numeric-version, --hpack-numeric-version オプションを使うことで、stack, hpack それぞれのバージョン情報が表示されます。


stackのバージョン確認

$ stack --numeric-version

1.9.3


hpackのバージョン確認

$ stack --hpack-numeric-version

0.31.1


stack コマンドの更新


stackコマンドの更新

$ stack upgrade


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

stack upgrade と良く似たコマンドに stack update というものがありますが、こちらはほぼ利用しません。なぜなら stack update はパッケージインデックス (利用可能なパッケージのリスト) の更新を明示的に行うコマンドですが、必要であれば stack の内部で自動的に stack update が実行されるためです。

=> How do I update my package index?

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


stack について

stackHaskell で開発を効率良く行うためのビルドツールです。コンパイラ (GHC) のインストールやパッケージ管理などを行います。

また、stack は今まで全く 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-12.0 → lts-13.0
API の破壊的変更, パッケージの追加, パッケージの削除が行われる

マイナーバージョン
1週間に一度 (主に日曜日)
lts-13.0 → lts-13.1
互換性のある API の変更, パッケージの追加が行われる

nightly
ほぼ毎日
nightly-2019-01-05 → nightly-2019-01-06
API の変更, パッケージの追加

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

Decision Tree

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 のバージョンを指定したスナップショットもあります)


stackのオプションとしてresolverを指定する方法

# resolver のスナップショットに lts-13.1 を指定して ghci を起動

$ stack ghci --resolver lts-13.1

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

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

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


stack 以外の選択肢

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


cabal

Haskell を始めたばかりの初学者には今の所おすすめしていません。

その理由としては


  • cabal の記法が独特

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


  • cabal new-* 系のコマンドに関する情報が少ない

ただ、現在実装が進んでいる 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

これで stack が使えるようになったと思います。以下のようにバージョン情報が表示されれば大丈夫です。


stackのインストールチェック

$ stack --version

Version 1.9.3, Git revision 40cf7b37526b86d1676da82167ea8758a854953b (6211 commits) x86_64 hpack-0.31.1

また、このタイミングで ~/.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

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

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


hpack について

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

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



  • yaml 記法なので読みやすい


  • other-modulesLICENSE など、自動的に推論してくれる

  • 共通する dependencies などをまとめて書くことができる

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


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

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


コマンドの補完

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


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

fish ユーザ


完全なリビルド

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

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


キャッシュの削除

$ stack clean

$ stack build

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

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


stack のアンインストール

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


stackのアンインストール

# stack が自動的にダウンロードしたパッケージやGHCは以下のディレクトリに保存されます。

$ rm -rf ~/.stack/

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

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

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


プロジェクトの新規作成


プロジェクトの新規作成

$ stack new PFAD

$ cd PFAD

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


プロジェクトの初期構造

$ tree .

.
|-- ChangeLog.md
|-- LICENSE
|-- PFAD.cabal
|-- README.md
|-- Setup.hs
|-- app
| `-- Main.hs
|-- package.yaml
|-- src
| `-- Lib.hs
|-- stack.yaml
`-- test
`-- Spec.hs


おすすめの開発方法

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


おすすめの開発方法

$ stack test --fast --file-watch --no-run-tests


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

オプション
意味

--fast
最適化を無効にする (-O0)

--file-watch
ファイルの変更を検知すると自動的にリビルドする

--no-run-tests
テストを実行せず、ビルドだけ行う


準備

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

フォルダ
関連するコマンド

app
stack exec, stack run

src
stack build

test
stack test

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


利用しないファイルの削除

$ rm src/Lib.hs


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


ファイルを削除したことによって生じたコンパイルエラー

$ stack build

... 初回のみコンパイラのインストールが行われます ...

/PFAD/app/Main.hs:3:1: error:
Could not load module `Lib'
It is a member of the hidden package `libiserv-8.6.3'.
Perhaps you need to add `libiserv' to the build-depends in your .cabal file.
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 = return ()


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


ビルドに成功した結果

$ stack build

Building all executables for `PFAD' once. After a successful build of all of them, only specified executables will be rebuilt.
PFAD-0.1.0.0: build (lib + exe)
Preprocessing library for PFAD-0.1.0.0..
Building library for PFAD-0.1.0.0..
Preprocessing executable 'PFAD-exe' for PFAD-0.1.0.0..
Building executable 'PFAD-exe' for PFAD-0.1.0.0..
[1 of 2] Compiling Main ( app/Main.hs, .stack-work/dist/x86_64-linux/Cabal-2.4.0.1/build/PFAD-exe/PFAD-exe-tmp/Main.o )
[2 of 2] Compiling Paths_PFAD ( .stack-work/dist/x86_64-linux/Cabal-2.4.0.1/build/PFAD-exe/autogen/Paths_PFAD.hs, .stack-work/dist/x86_64-linux/Cabal-2.4.0.1/build/PFAD-exe/PFAD-exe-tmp/Paths_PFAD.o )
Linking .stack-work/dist/x86_64-linux/Cabal-2.4.0.1/build/PFAD-exe/PFAD-exe ...
PFAD-0.1.0.0: copy/register
Installing library in /PFAD/.stack-work/install/x86_64-linux/lts-13.1/8.6.3/lib/x86_64-linux-ghc-8.6.3/PFAD-0.1.0.0-B1WTd1uDmyhGTpZWzSKOj5
Installing executable PFAD-exe in /PFAD/.stack-work/install/x86_64-linux/lts-13.1/8.6.3/bin
Registering library for PFAD-0.1.0.0..


ライブラリの作成

それでは書籍のコードを 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



プログラムの実行

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


replを使った実行

$ stack ghci

Using main module: 1. Package `PFAD' component exe:PFAD-exe with main-is file: /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.6.3: http://www.haskell.org/ghc/ :? for help
[1 of 2] Compiling Main ( /PFAD/app/Main.hs, interpreted )
[2 of 2] Compiling Minfree ( /PFAD/src/Minfree.hs, interpreted )

/PFAD/src/Minfree.hs:3:1: error:
Could not load module `Data.Array'
It is a member of the hidden package `array-0.5.3.0'.
You can run `:set -package array' to expose it.
(Note: this unloads all the modules in the current scope.)
Use -v to see a list of the files searched for.
|
3 | import Data.Array (Array, elems, accumArray, assocs)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

/PFAD/src/Minfree.hs:4:1: error:
Could not load module `Data.Array.ST'
It is a member of the hidden package `array-0.5.3.0'.
You can run `:set -package array' to expose it.
(Note: this unloads all the modules in the current scope.)
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/08a077bf/ghci-scrip


少し長いですが、このエラーメッセージのうち特に重要な部分を抜き出してみましょう。


エラーその1

/PFAD/src/Minfree.hs:3:1: error:

Could not load module `Data.Array'
It is a member of the hidden package `array-0.5.3.0'.

|
3 | import Data.Array (Array, elems, accumArray, assocs)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^



エラーその2

/PFAD/src/Minfree.hs:4:1: error:

Could not load module `Data.Array.ST'
It is a member of the hidden package `array-0.5.3.0'.

|
4 | import Data.Array.ST (runSTArray, newArray, writeArray)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^


このエラーメッセージから読み取って欲しい点は以下の3点です。


  • エラーが発生したソースコードの場所

  • エラーの原因 (今回は利用しようと思った外部パッケージのモジュールが見つからなかったためエラーになっています)

  • モジュールがどのパッケージで定義されているか

さきほどのエラーメッセージから上記3点を抜き出すと次のようになります。

エラーの内容
エラーその1
エラーその2

エラーの発生箇所
PFAD/src/Minfree.hs:3:1
PFAD/src/Minfree.hs:4:1

エラーの原因
Could not load module `Data.Array'
Could not load module `Data.Array.ST'

モジュールが定義されているパッケージ
array-0.5.3.0
array-0.5.3.0

これらの情報から、壊れたビルドの直し方がわかります。

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 以外を採用することは一般的に考えにくいです。


Hackage, パッケージ, バージョン, モジュール, 関数の関係について

 
集合の対象

モジュール
関数の集まり

パッケージ (バージョン付き)
モジュールの集まり

パッケージ
パッケージ (バージョン付き) の集まり

Hackage
パッケージの集まり

 
具体例

Prelude
map, id, filter, length, on, ...

base-4.12.0.0
Prelude, Control.Monad, Data.Function, ...

base
base-4.11.0.0, base-4.11.1.0, base-4.12.0.0, ...

Hackage
base, array, vector, ...


依存関係の追加

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

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

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


package.yaml

dependencies:

- base >= 4.7 && < 5
- array

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


プロジェクトのビルド

$ stack build


現在のプロジェクトで利用しているパッケージの依存関係をチェックするためには stack ls dependencies コマンドを利用します。


依存関係にarrayパッケージが追加されたことの確認

$ stack ls dependencies

PFAD 0.1.0.0
array 0.5.3.0
base 4.12.0.0
ghc-prim 0.5.3
integer-gmp 1.0.2.0
rts 1.0


パッケージのバージョンを省略できる理由

stack はどんな組み合わせでもビルドできるパッケージの組み合わせをスナップショットとして提供していることはすでに述べた通りです。

実際にプロジェクトを新規作成する際に stack.yaml ファイルの resolver フィールドにスナップショットが自動的に指定されます。


コメントを削除したstack.yaml

$ cat stack.yaml

resolver: lts-13.1
packages:
- .

resolver はプロジェクトを作成した時点の最新の LTS が指定されるので、気に入らない場合は変更しましょう。

この resolver が指定されているおかげで、明示的にバージョンを指定しなくてもスナップショットに含まれているバージョンが利用されます。スナップショットに登録されているパッケージ、およびバージョンは lts などで確認することができます。

また、スナップショットに含まれていないパッケージ (やバージョン) を利用したい場合も実際には良くあります。そのような場合は extra-deps という仕組みが用意されているので、そちらを利用します。


extra-deps の利用方法

例えば今回の例は、以下のような状態です。


  • スナップショットは lts-13.1 を利用


  • lts-13.1 に含まれている array パッケージのバージョンは 0.5.3.0 となっている

ここでは、例として1つ前のバージョン array-0.5.2.0 を利用してみましょう。


1つ前のバージョンのarrayを利用するためにextra-depsを指定

resolver: lts-13.1

packages:
- .
extra-deps:
- array-0.5.2.0

これだけです。


確認

$ stack build

PFAD-0.1.0.0: unregistering
array-0.5.2.0: download
array-0.5.2.0: configure
array-0.5.2.0: build
array-0.5.2.0: copy/register
....

Completed 2 action(s).

$ stack ls dependencies
PFAD 0.1.0.0
array 0.5.2.0
base 4.12.0.0
ghc-prim 0.5.3
integer-gmp 1.0.2.0
rts 1.0



extra-deps の注意点

extra-deps に追加した場合はビルドに失敗する可能性があります。今回はたまたま上手くいっただけです。

プロジェクトで利用しているパッケージの依存関係内において、バージョンの矛盾が起こらない限りビルド可能というのが extra-deps です。例えば過去の古い GHC に依存しているバージョンを指定した場合、他のパッケージが最新の GHC を要求していたら、ビルドできるはずありませんよね。

ただ、extra-deps を使って良くないことが起きるということは無いため、積極的に使ってみてください。ビルドできれば何の問題もありません。

実際のアプリケーション開発では必須の機能です。


リモートリポジトリの指定

extra-deps が優秀なのは Hackage などに登録されていないけども github などにホスティングされている野良パッケージや、まだ Hackage に登録されていない開発バージョンの利用がとても簡単にできる点にあります。

具体例として array の最新版を使ってみましょう。リポジトリのURLは http://git.haskell.org/packages/array.git になります。また、どのリビジョンを利用するか指定する必要があります。


最新版のarrayを使うようにstack.yamlを変更した

resolver: lts-13.1

packages:
- .
extra-deps:
- git: http://git.haskell.org/packages/array.git
commit: 8593a10f65020da3854b1c8478082d454b416118

あとは普通にビルドするだけです。

$  stack build

Cloning 8593a10f65020da3854b1c8478082d454b416118 from http://git.haskell.org/packages/array.git
Cloning into '/PFAD/.stack-work/downloaded/b__kss5ijemv'...
remote: Counting objects: 10815, done.
remote: Compressing objects: 100% (3006/3006), done.
remote: Total 10815 (delta 7730), reused 10569 (delta 7582)
Receiving objects: 100% (10815/10815), 2.04 MiB | 617.00 KiB/s, done.
Resolving deltas: 100% (7730/7730), done.
PFAD-0.1.0.0: unregistering
array-0.5.2.0: unregistering
array-0.5.3.0: configure (lib)
array-0.5.3.0: build (lib)
array-0.5.3.0: copy/register
...

Completed 2 action(s).

ログを見ると、ちゃんとリポジトリをクローンしているようですね。依存関係もチェックしておきましょう。


依存関係チェック

$ stack ls dependencies

PFAD 0.1.0.0
array 0.5.3.0
base 4.12.0.0
ghc-prim 0.5.3
integer-gmp 1.0.2.0
rts 1.0

バージョン番号は Hackage に登録されている 0.5.3.0 のままですが、ちゃんと最新版の内容が利用できるようになっています。


アプリケーションの作成

stack.yaml は以下の内容で進めていきます。


stack.yamlの内容

resolver: lts-13.1

packages:
- .

ここまででライブラリ (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

また、stack 1.9.1 からは stack run というサブコマンドが実装されたため、build -> exec のステップが1ステップに簡略化されました。


stack-runコマンド

$ stack run [0,1,2,3,4,7]

5


minfree2 の作成

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

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


複数のアプリケーションをビルドするためにpackage.yamlを修正

executables:

minfree:
main: app/Main.hs # この行を変更
ghc-options:
- -threaded
- -rtsopts
- -with-rtsopts=-N
dependencies:
- PFAD

次に package.yamlexecutables に新しいアプリケーションの内容を追記します。


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


実行してみます。


minfree2の実行

$ stack build

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

この場合も同様に stack run が使えますが、アプリケーションが2つになったため明示的にアプリケーション名を指定する必要があります。


minfree2の実行(stack-run)

$ stack run minfree2 [1,2,3,4,5]

0

アプリケーション名が省略された場合は、package.yamlexecutables に書かれている一番上のアプリケーションが実行されます。


ドキュメントの作成


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.4.0.1/doc/html/PFAD/index.html,
.stack-work/dist/x86_64-linux/Cabal-2.4.0.1/doc/html/PFAD/PFAD.txt

...


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

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


自動的にブラウザが開く

$ stack haddock --open


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


キャッシュが残っているため上手く生成できていない

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

$ tree -L 1
.
|-- ChangeLog.md
|-- LICENSE
|-- PFAD.cabal
|-- README.md
|-- Setup.hs
|-- app
|-- package.yaml
|-- src
|-- stack.yaml
`-- test

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


キャッシュがクリアされたため、上手くいった

$ stack clean

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

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

page1.png

page2.png

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

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

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


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)

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


haddockの生成

$ 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 を実行するのは面倒です。規模が小さいうちは haddock の生成処理も比較的短時間で終わるため、ビルド時に一緒にドキュメントも生成したいと思うことがあるかもしれません。

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

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


stack.yaml

resolver: lts-13.1

packages:
- .

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


stack.yaml

resolver: lts-13.1

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-13.21

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.22

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 installstack build --copy-bins です。

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


open-haddocks オプション

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

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, HUnit があります。



  • hspectasty は関数の振る舞いをテストを記述するためのテストフレームワークです。


  • HUnit は単体テストを記述するためのパッケージです。


  • QuickCheck は単体テストでは想定していなかったエッジケースをプログラムが生成するランダムな入力によってあぶり出すことができます。


  • 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 パッケージは更新が頻繁に行われているのでバージョンごとに書き方が違う場合があります。今回明示的には指定していませんが、lts-13.1 に含まれている QuickCheck-2.12.6.1 を利用しています。


QuickCheck に慣れよう!

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

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


対話環境での実行

$ stack ghci --no-load --package QuickCheck

Prelude> import Test.QuickCheck
Prelude Test.QuickCheck> :set prompto "> "
> :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 に指定してあげることでランダムな値を生成できることがわかりました。また、多相型についてはエラーになります。


色々な関数

他にも、Test.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

-- ランダムに生成された値のリストを生成する

listOf :: Gen a -> Gen [a]
> 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

-- 空リストを除く

listOf1 :: Gen a -> Gen [a]
> 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

vectorOf :: Int -> Gen a -> Gen [a]

-- 長さを指定してランダムな値のリストを生成する
> 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

shuffle :: [a] -> Gen [a]

-- 与えられたリストをシャッフルしたランダムな値のリストを生成する
> 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

vector :: Arbitrary a => Int -> Gen [a]

> 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

orderedList :: (Ord a, Arbitrary a) => Gen [a]

> 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


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

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 コマンドを実行します。


doctestの実行

$ stack clean

$ stack test

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

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


doctestの実行(個別)

$ 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 を書いていないため、何も起きません。

また、環境によっては以下のビルドエラーが出る場合があります。


環境によっては以下のエラーが発生する

/usr/bin/ld.gold: error: cannot find -ltinfo

collect2: error: ld returned 1 exit status
`gcc' failed in phase `Linker'. (Exit code: 1

上記のエラーが出た人は apt-get install libtinfo-dev を実行してから、もう一度 stack test を行いましょう。


必要な依存関係をインストールして、もう一度テストを実行

$ apt-get install libtinfo-dev

$ stack test
...
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>>> に続く文字列が 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)

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


doctestが失敗することを確認

$ 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)

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


doctestが成功する例

$ 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


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


doctestが失敗する例

$ 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

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


doctestが成功する例

$ 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 オプションを追加します。


imageのpullに失敗してエラーになった例

$ stack build --docker

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

[]


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

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


イメージがまだ出来ていない時のエラー

Pulling image from registry: 'fpco/stack-build:lts-13.1'

Error response from daemon: manifest for fpco/stack-build:lts-13.1 not found
Could not pull Docker image:
fpco/stack-build:lts-13.1
There may not be an image on the registry for your resolver's LTS version in
your configuration file.

上記のエラーは最新のLTSを利用する際に起きることがあります。まだイメージの生成ができていないだけなので、待つしか無いです。だいたいLTSのリリースから2~3日ほどで新しいベースイメージができる印象です。


ベースイメージのプル

$ 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

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


dockerビルド

$ stack build --docker

...

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

では、次に stack image container コマンドを紹介します。このコマンドを使うことでビルドしたアプリケーション等を含む Docker イメージを作ることができます。


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

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


imageセクションの追加

image:

container:
name: pfad
base: "fpco/stack-run"

以下のコマンドで Docker イメージが作成されます。(--docker オプションは忘れやすいので注意が必要です)


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


これを実行してみます。


resolverオプションが無いのでエラーになった

$ 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-13.1 -- MinfreeScript.hs [0,1,2,3]

Using resolver: lts-13.1 specified on command line
4

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

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


HLint

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

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


HLint のインストール

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


HLintのインストール

$ stack install hlint --resolver lts

$ hlint --version
HLint v2.1.11, (C) Neil Mitchell 2006-2018

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


hlintの実行

$ 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の実行

$ hlint src

No hints


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


プロジェクト全体に対してhlintを実行

$ 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 --resolver lts

$ stylish-haskell --version
stylish-haskell 0.9.2.0

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


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