Edited at

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

技術書典7 に参加します。(Haskell を作る本とGHC APIを出す予定です)


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


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

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


TODO: profiling, default-extension, ghc-option, package.yamlstack.yaml の関係, について追記する。

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

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

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


エディタ (IDE) について

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

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

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


stack のバージョンについて

2019年9月14日現在の stack の最新バージョンは 2.1.3 です。

stack のバージョンを確認するためには --version オプションを利用します。


バージョンの確認

$ stack --version

Version 2.1.3, Git revision 636e3a759d51127df2b62f90772def126cdf6d1f (7735 commits) x86_64 hpack-0.31.2

上記の結果から、以下のバージョンを利用していることがわかります。

_
バージョン

stack
2.1.3

hpack
0.31.2


stack, hpack のバージョンのみを取得する

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


stackのバージョン確認

$ stack --numeric-version

2.1.3


hpackのバージョン確認

$ stack --hpack-numeric-version

0.31.2


stack コマンドの更新


stackコマンドの更新

$ stack upgrade


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


stack について

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

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

特に初心者にとって非常に使いやすいツールになっています。


なぜ stack を使うのか?

stack を使わずに Haskell で本格的なアプリケーションを開発することはとても大変です。仮に開発ができたとしても、規模が大きくなるにつれ、開発者しかビルドできなくなります。

stack が登場する以前の Haskell 界隈では、この問題は特に珍しいことではありませんでした。(現在は Nix, cabal (Nix-style Local Build) などの選択肢もあります)

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

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

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

これから業務なり、勉強なりで最初に Haskell を使おうと思っている人は必ず stack を使ってください。

現在 github などで見かけるプロジェクトは stack, cabal, nix のどれかで管理されています。


Stackage とは何か?

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

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


  • 長期サポートの Long Term Support (lts)

  • 日々のスナップショット nightly

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

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

バージョン
タイミング

備考

メジャーバージョン
3 ~ 6 ヶ月に一度
lts-13.0 → lts-14.0
API の破壊的変更, パッケージの追加, パッケージの削除が行われる

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

nightly
ほぼ毎日
nightly-2019-07-26 → nightly-2019-07-27
API の変更, パッケージの追加

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

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

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

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

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



  1. Hackage にパッケージをアップロードする


  2. Stackage に自動的に反映される

  3. パッケージ追加の申請を行い、nightly に追加される


  4. lts のマイナーバージョンアップデートで lts に追加される

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


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

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

$ stack ghci --resolver lts-14.5

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

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

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


stack 以外の選択肢

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


cabal

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

その理由としては


  • cabal の記法が独特

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

  • よくわからないタイミングでビルドできなくなることがある (例えば --max-backjumps のオプション等)

  • エラーメッセージがわかりづらい

ただ、現在実装が進んでいる 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 2.1.3, Git revision 636e3a759d51127df2b62f90772def126cdf6d1f (7735 commits) x86_64 hpack-0.31.2

また、このタイミングで ~/.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/
~/.stack/
├── config.yaml
├── pantry
│   ├── hackage
│   │   ├── 00-index.tar
│   │   ├── 00-index.tar.gz
│   │   ├── 00-index.tar.idx
│   │   ├── hackage-security-lock
│   │   ├── mirrors.json
│   │   ├── root.json
│   │   ├── snapshot.json
│   │   └── timestamp.json
│   ├── pantry.sqlite3
│   └── pantry.sqlite3.pantry-write-lock
├── stack.sqlite3
└── stack.sqlite3.pantry-write-lock

pantry フォルダや stack.sqlite3, stack.sqlite3.pantry-write-lock ファイルを触ることは基本的にありません。

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 ファイルを自動生成するためのツールです。stackhpack のライブラリを利用するため、別途 hpack をインストールする必要はありません。

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



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


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

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

ただ、最近では hpack を使わないプロジェクトも多いので、慣れてきたら package.yaml を削除して cabal だけでも良いかもしれません。


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

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


コマンドの補完

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


# 必要に応じて .bashrc などに追記しておくと便利
$ echo 'eval "$(stack --bash-completion-script stack)"' >> ~/.bashrc


zshの場合

zsh の場合は .zshrc に以下のように追記します。

autoload -U +X compinit && compinit

autoload -U +X bashcompinit && bashcompinit
eval "$(stack --bash-completion-script stack)"

詳しくは別途マニュアルを参照してください。


完全なリビルド

通常 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

...
All done.

$ 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

bench
stack bench
デフォルトでは存在しない

haddock
stack haddock
デフォルトでは存在しない

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

-- import Lib は削除

main :: IO ()
main = print "Hello World"


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


ビルドに成功

$ stack build

Building all executables for `PFAD' once. After a successful build of all of them, only specified executables will be rebuilt.
PFAD> 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
[2 of 2] Compiling Paths_PFAD
Linking .stack-work/dist/x86_64-linux/Cabal-2.4.0.1/build/PFAD-exe/PFAD-exe ...
PFAD> copy/register
Installing library in /PFAD/.stack-work/install/x86_64-linux/754da54955212e5178bdb2a3208393df962e4e4e4998fe9886c862a9c58273a0/8.6.5/lib/x86_64-linux-ghc-8.6.5/PFAD-0.1.0.0-B1WTd1uDmyhGTpZWzSKOj5
Installing executable PFAD-exe in /PFAD/.stack-work/install/x86_64-linux/754da54955212e5178bdb2a3208393df962e4e4e4998fe9886c862a9c58273a0/8.6.5/bin
Registering library for PFAD-0.1.0.0..


stack.yaml.lock ファイル

stack v2.1.3 から stack.yaml.lock ファイルというものが生成されるようになりました。

例えば、現在の stack.yaml.lock ファイルの中身は以下のようになっていると思います。


stack.yaml.lock

# This file was autogenerated by Stack.

# You should not edit this file by hand.
# For more information, please see the documentation at:
# https://docs.haskellstack.org/en/stable/lock_files

packages: []
snapshots:
- completed:
size: 500539
url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/13/29.yaml
sha256: 006398c5e92d1d64737b7e98ae4d63987c36808814504d1451f56ebd98093f75
original: lts-13.29


このファイルはビルドの再現性のため導入されました。

詳細については以下を参照してください。

ここでの問題はこのファイルをリポジトリに含めるかどうかですが、今の所は .gitignore に追記しておいて良いです。

たぶん今後、パッケージの場合は .gitignore に含め、end product の場合はリポジトリに含めておくという方針になると思います。


ライブラリの作成

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



関数のエクスポート

Haskell ではエクスポートリストを明記することで、外部に公開する関数を制限することができます。

今回の例では以下の部分が該当します。

module Minfree (minfree, minfree') where

この場合 minfreeminfree' 関数を外部に公開することになります。

仮に、以下のように省略した場合は全ての関数が外部に公開されます。


エクスポートリストを省略した形式

module Minfree where



プログラムの実行 (repl 環境)

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


replを使った実行

$ stack ghci

Using main module: 1. Package `PFAD' component PFAD:exe:PFAD-exe with main-is file: /PFAD/app/Main.hs
PFAD> configure (lib + exe)
Configuring PFAD-0.1.0.0...
PFAD> 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.5: 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/35ac6387/ghci-script


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


エラーその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.
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)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^


エラーその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.
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)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

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


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

  • エラーの原因 (import しようと思ったモジュールが見つからなかった)

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

さきほどのエラーメッセージから上記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.


パッケージの指定

stack ghci コマンドを実行した場合 base パッケージが読み込まれます。そのため、それ以外のパッケージに含まれるモジュールを利用するためには明示的にパッケージを指定する必要があります。

パッケージを追加して起動する場合は --package オプションを使います。


arrayパッケージを指定してghciを起動

$ stack ghci --package array

...
*Main Minfree>

別のやり方として ghci を起動した後に :set -package コマンドを実行する方法があります。


setコマンドを使ってarrayパッケージを読み込む

$ stack ghci

...
*Main> :set -package array
Prelude> import Data.Array

Prelude Data.Array>


この場合 ghci が完全に初期化されるため、プロンプトの表示が *Main Minfree> ではなく Prelude> になります。そのため、実際のプロジェクトではあまり使わないかもしれません。


--no-load オプション

プロジェクト内で stack ghci を実行すると、プロジェクトのモジュールが読み込まれてしまいます。

場合によっては、初期状態の ghci を起動したい時があるかもしれません。その場合は --no-load オプションを使います。


モジュールを読み込まずにghciを起動

$ stack ghci --no-load

Prelude>


プロンプト文字列の変更

stack ghci を起動した時のプロンプトの表示名は Prelude> になっています。(何かモジュールを読み込んだ場合は、そのモジュール名が表示されます)


モジュールを読み込まずにghciを起動

$ stack ghci --no-load

Prelude>

ghci で色々と試していると、モジュールをいくつも import したくなります。そうすると、プロンプトの表示名はインポートしたモジュール名が連なり、非常に使いづらいものになってしまいます。


モジュールをいくつも読み込んだ状態

Prelude> import Data.List

Prelude Data.List> import Data.Function
Prelude Data.List Data.Function> import Control.Monad
Prelude Data.List Data.Function Control.Monad> import Data.Functor
Prelude Data.List Data.Function Control.Monad Data.Functor> 1+1
2
Prelude Data.List Data.Function Control.Monad Data.Functor>

この問題を解決するためには :set prompto コマンドを使います。


プロンプトの文字列を変更

Prelude Data.List Data.Function Control.Monad Data.Functor> :set prompt "> "

> 1+1
2
> import Data.Bool
> :set prompt "λ "
λ 1+1
2
λ

こんな感じでスッキリさせることができます。

特に大きなプロジェクトを stack ghci で読み込むとプロンプトがひどいことになってしまので、このようにして対処すると良いでしょう。


.ghci ファイル

プロンプトの表示を毎回自分で変更するのが面倒な人は .ghci ファイルを作成しましょう。

このファイルは ghci を起動した直後に実行するコマンドを並べたものです。このファイルはプロジェクトのルートに作成します。


.ghci

:set prompt "λ "


実際に実行してみます。


ghciの起動

$ stack ghci

Loaded GHCi configuration from /PFAD/.ghci
[1 of 2] Compiling Main ( /PFAD/app/Main.hs, interpreted )
[2 of 2] Compiling Minfree ( /PFAD/src/Minfree.hs, interpreted )
Ok, two modules loaded.
Loaded GHCi configuration from /private/var/folders/p9/ln28v7zs7_53lz3ln4xyq2g00000gn/T/haskell-stack-ghci/52a58e18/ghci-script
λ

Loaded GHCi configuration from /PFAD/.ghci のような文字列が表示されれば、ちゃんと設定ファイルが読み込まれています。もし、設定ファイルの記述内容が間違っていた場合は Some flags have not been recognized のようなエラーが表示されるはずです。


promptとtypoしてしまった場合のエラーメッセージ

$ stack ghci

...
Some flags have not been recognized: prompto, λ
Loaded GHCi configuration from /PFAD/.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, パッケージ, バージョン, モジュール, 関数の関係について

用語
対象

モジュール
関数やデータコンストラクタなどの集まり

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

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

パッケージアーカイブ
パッケージの集まり

具体例はこんな感じです。

用語
具体例
対象の具体例

モジュール
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, Stackage
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

新しく追加した array パッケージのバージョンは 0.5.3.0 が使われているようですね。


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

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

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


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

$ cat stack.yaml

resolver: lts-13.29
packages:
- .

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

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

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


extra-deps の利用方法

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


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


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

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

lts-13.29 には array-0.5.3.0 しか含まれていないため、スナップショットに array-0.5.2.0 を追加する必要があります。

そのためには extra-deps を指定します。


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

resolver: lts-13.29

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

これだけです。


確認

$ stack build

array> configure
array> Configuring array-0.5.2.0...
array> build
array> Preprocessing library for array-0.5.2.0..
array> Building library for array-0.5.2.0..
....

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


array のバージョンが 0.5.2.0 に変化したことを確認できました。


extra-deps の注意点

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

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

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

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


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

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

具体例として waddlaw/single-repo を使ってみましょう。

今回のために作ったサンプルパッケージなので、Example モジュールに singleExample 関数しか定義していません。

module Example where

singleExample :: String
singleExample = "more single repo"

リポジトリのURLは https://github.com/waddlaw/single-repo になります。また、どのコミットハッシュを利用するか指定する必要があります。

ここでは一番最初のコミットハッシュ 9af5f4c6cdc03deb59bba03407321490f0a2aa41 を指定します。


single-repoを使うようにstack.yamlを変更した

resolver: lts-13.29

packages:
- .
extra-deps:
- array-0.5.2.0
- git: https://github.com/waddlaw/single-repo # この行を追加
commit: 9af5f4c6cdc03deb59bba03407321490f0a2aa41 # この行を追加

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

$ stack build

Cloning 9af5f4c6cdc03deb59bba03407321490f0a2aa41 from https://github.com/waddlaw/single-repo
PFAD> configure (lib + exe)
Configuring PFAD-0.1.0.0...
PFAD> build (lib + exe)
Preprocessing library for PFAD-0.1.0.0..
Building library for PFAD-0.1.0.0..
[2 of 2] Compiling Paths_PFAD
Preprocessing executable 'PFAD-exe' for PFAD-0.1.0.0..
Building executable 'PFAD-exe' for PFAD-0.1.0.0..
[2 of 2] Compiling Paths_PFAD
Linking .stack-work/dist/x86_64-linux/Cabal-2.4.0.1/build/PFAD-exe/PFAD-exe ...
PFAD> copy/register
Installing library in /PFAD/.stack-work/install/x86_64-linux/e45e24d661a5523830acd90eb01354f05f6c2b4a1af4a66fed71976eed516247/8.6.5/lib/x86_64-linux-ghc-8.6.5/PFAD-0.1.0.0-JMjJGAZykCn4AzXbJSCMlb
Installing executable PFAD-exe in /PFAD/.stack-work/install/x86_64-linux/e45e24d661a5523830acd90eb01354f05f6c2b4a1af4a66fed71976eed516247/8.6.5/bin
Registering library for PFAD-0.1.0.0..

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


package.yamlにsignle-repoの依存関係を追加

...

dependencies:
- base >= 4.7 && < 5
- array
- single-repo # ここを追加
...


依存関係チェック

$ 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
single-repo 0.1.0.0

実際に stack ghci で使ってみましょう。

$ stack ghci --no-load

λ import Example
λ singleExample
"single repo"

今度はコミットハッシュ値を変更して、もう一度実行してみます。そのために、stack.yamlextra-deps を編集します。

github を参照する場合は git の代わりに github を使えば、省略形が利用できるのでオススメです。コミットのハッシュ値も更新します。


stack.yaml

extra-deps:

- array-0.5.2.0
- github: waddlaw/single-repo # この行を変更
commit: 16463931082172d6ad780034f6905e373dbc5139 # この行を変更

再び同じ手順で実行してみます。

$ stack ghci --no-load

...
λ import Example
λ singleExample
"more single repo"

ちゃんと異なるコミットのパッケージが利用できていますね。


リモートリポジトリの設定 (mega-repo)

amazonka など、いくつかのパッケージは単一のリポジトリで複数のパッケージを管理しています。そういったパッケージを extra-deps で使う場合は subdirs を指定する必要があります。

mega-repo 用のサンプルリポジトリを waddlaw/mega-repo に作りました。このリポジトリの内容は以下の通りです。


  • mega-repo1 パッケージがある

  • mega-repo2 パッケージがある

それぞれ、こんな感じの関数が定義されています。


mega-repo1パッケージの中身

module MegaExample1 where

megaExample1 :: String
megaExample1 = "mega repo example1"



mega-repo2パッケージの中身

module MegaExample2 where

megaExample2 :: String
megaExample2 = "mega repo example2"


では、実際に使ってみましょう。extra-deps に今回利用するパッケージを追記します。


stack.yaml

extra-deps:

- array-0.5.2.0
- github: waddlaw/single-repo
commit: 16463931082172d6ad780034f6905e373dbc5139
- github: waddlaw/mega-repo # この行を追加
commit: 130b4a853a6cdd1e2f2090b9a9a755ce30ee50d7 # この行を追加

試しにこのままビルドしてみましょう。


失敗することを確かめる

$ stack build

No cabal file found for Archive from https://github.com/waddlaw/mega-repo/archive/130b4a853a6cdd1e2f2090b9a9a755ce30ee50d7.tar.gz

こんな感じでビルドに失敗します。

ビルドを直すためには subdirs でパッケージを指定するだけです。


stack.yaml

extra-deps:

- github: waddlaw/single-repo
commit: 16463931082172d6ad780034f6905e373dbc5139
- github: waddlaw/mega-repo
commit: 130b4a853a6cdd1e2f2090b9a9a755ce30ee50d7
subdirs: # この行を追加
- mega-repo1 # この行を追加

ghci で実際に動かしてみます。

$ stack ghci --package mega-repo1 --no-load

λ import MegaExample1
λ megaExample1
"mega repo example1"

現在 subdirs には mega-repo1 しか指定していないため、mega-repo2 パッケージは利用できません。


subdirsで指定していないためエラーになった

$ stack ghci --package mega-repo2 --no-load

Using main module: 1. Package `PFAD' component PFAD:exe:PFAD-exe with main-is file: /PFAD/app/Main.hs

Error: While constructing the build plan, the following exceptions were encountered:

Unknown package: mega-repo2

Some different approaches to resolving this:

Error: Plan construction failed.

Warning: Build failed, but trying to launch GHCi anyway
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.5: http://www.haskell.org/ghc/ :? for help
<command line>: cannot satisfy -package mega-repo2
(use -v for more information)


パッケージを複数指定する場合は、単純に subdirs を複数指定するだけです。


subdirsを複数指定する場合

extra-deps:

- github: waddlaw/single-repo
commit: 16463931082172d6ad780034f6905e373dbc5139
- github: waddlaw/mega-repo
commit: 130b4a853a6cdd1e2f2090b9a9a755ce30ee50d7
subdirs:
- mega-repo1
- mega-repo2 # この行を追加

$ stack ghci --package mega-repo1 --package mega-repo2

λ import MegaExample1
λ megaExample1
"mega repo example1"

λ import MegaExample2
λ megaExample2
"mega repo example2"


アプリケーションの作成

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


stack.yamlの内容

resolver: lts-13.29

packages:
- .

また package.yamldependencies は以下のようになっています。


package.yamlのdependencies

dependencies:

- base >= 4.7 && < 5
- array

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


fastMinfree の作成

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

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


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

executables:

minfree:
main: app/Main.hs # この行を変更
# source-dirs: app は削除します
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
# ここから下の行を追記しました
fastMinfree:
main: app/FastMinfreeApp.hs
ghc-options:
- -threaded
- -rtsopts
- -with-rtsopts=-N
dependencies:
- PFAD

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


app/FastMinfreeApp.hs

module Main (main) where

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

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


実行してみます。


fastMinfreeの実行

$ stack build

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

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


fastMinfreeの実行(stack-run)

$ stack run fastMinfree [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
├── stack.yaml.lock
└── 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
├── stack.yaml.lock
└── test

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

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

気をつける点としては 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

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

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


src/Minfree.hs

-- |

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

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


build の設定

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

しかし、毎回 stack haddock --haddock-arguments --odir=haddock を実行するのは面倒です。規模が