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

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

今後は上記サイトに加筆・修正を行うため、本記事はメンテナンスされません。ご注意ください。

また、記事の内容があまりに古くなった場合は削除する予定です。


TODO: docker integration, stack image container, stylish-haskell, hindent, liquidhaskell, profiling, travis について追記する。

Pearls of Functional Algorithm Design (訳本: 関数プログラミング 珠玉のアルゴリズムデザイン) の各章をそれぞれ1つのプロジェクトとし、全体を stack で管理する場合、どのようにしたら良いか考えたいと思います。

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

version 1.6.1 がリリースされました。

  • Cabal-2.0 に対応したので、アップデート推奨です。(逆にアップデートしないと今後 Stackage のパッケージをビルドできなくなる可能性があります)
  • 依存関係のビルドがかなり速くなりました
  • extensible snapshot が実装されました
  • hpack-0.20.0 がバンドルされました
    • cabal ファイルを手で書き換えた時の変更検知機能が追加されています
  • --install-ghc がデフォルトで有効になりました (stack setup が不必要になります)

詳細は以下のドキュメントをご参照ください。

stack の更新

$ stack upgrade

$ stack --version
Version 1.6.1, Git revision f25811329bbc40b0c21053a8160c56f923e1201b (5435 commits) x86_64 hpack-0.20.0

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

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

また stack upgrade を行うと、既存とは異なるパスにインストールされる点に注意してください。

$ curl -sSL https://get.haskellstack.org/ | sh
$ which stack
/usr/local/bin/stack

$ stack upgrade
$ which stack
/home/bm12/.local/bin/stack

# 基本的にこの設定をしておいた方が良い
# export PATH=$PATH:~/.local/bin とすると、古い stack を認識するので注意
$ export PATH=~/.local/bin:$PATH

なぜ 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-8.0 → lts-9.0 API の破壊的変更, パッケージの追加, パッケージの削除が行われる
マイナーバージョン 1週間に一度 (主に日曜日) lts-9.8 → lts-9.9 互換性のある API の変更, パッケージの追加が行われる
nightly 日々 nightly-2017-10-14 → nightly-2017-10-15 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 のスナップショットに lts-9.17 を指定して ghci を起動
$ stack ghci --resolver lts-9.8

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

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

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

はじめに

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

stack のインストール

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

$ 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/
/home/bm12/.stack/
|-- config.yaml
`-- indices
    `-- Hackage
        |-- 00-index.tar
        |-- 00-index.tar.gz
        |-- 00-index.tar.idx
        |-- 01-index.tar
        |-- mirrors.json
        |-- root.json
        |-- snapshot.json
        `-- timestamp.json

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

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

default-template: new-template
templates:
  scm-init: git
  params:
    author-name: <author>
    author-email: <email>
    copyright: 'Copyright (c) 2017 <author>'
    github-username: <github_username>

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

hpack について

hpackpackage.yaml が存在する場合のみ package.yaml から .cabal ファイルを生成するためのツールです。stack にバンドルされているため、別途 hpack をインストールする必要はありませんが、最新版の hpack を利用したい場合は --with-hpack=<PATH> オプションを利用する必要があるのでご注意ください。

個人的には yaml 形式なので、読みやすいというのと other-modules を明示的に記載しなくても良いという点に惹かれて導入しました。今後より便利になっていくと思っています。stackhpack に移行しましたので普通の人は使っても大丈夫だと思います。

既存のプロジェクトを hpack に変換するための便利ツールとして hpack-convert があります。stack はこのツールを使って hpack に移行しました。僕が使った時は少し変換がおかしい部分もあったので、ご注意ください。

また、stack new した際のデフォルトテンプレートも hpack に切り替わりました。(Switch new-template to use hpack #112)。利用可能なテンプレートは stack templates コマンドで確認できます。

本チュートリアルも hpack を利用します。

Bash Auto-completion

設定しておくと、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 new PFAD
$ cd PFAD
$ stack build

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

# ラフに開発する時はこっちを使ってます
$ stack test --fast --file-watch

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

オプション 意味
--fast 最適化を無効にする (-O0)
--file-watch ファイルの変更を検知するとリビルドする
--pedantic -Wall による警告をエラーとして扱う

準備

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

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

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

$ rm src/Lib.hs

そうすると当然 Lib.hs が無いのでコンパイルエラーになってしまいます。とりあえず現状は src/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
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
Using main module: 1. Package `PFAD' component exe:PFAD-exe with main-is file: /home/bm12/Desktop/PFAD/app/Main.hs
GHCi, version 8.0.2: http://www.haskell.org/ghc/  :? for help
[1 of 2] Compiling Minfree          ( /home/bm12/Desktop/PFAD/src/Minfree.hs, interpreted )

/home/bm12/Desktop/PFAD/src/Minfree.hs:3:1: error:
    Failed to load interface for ‘Data.Array’
    It is a member of the hidden package ‘array-0.5.1.1’.
    Use -v to see a list of the files searched for.

/home/bm12/Desktop/PFAD/src/Minfree.hs:4:1: error:
    Failed to load interface for ‘Data.Array.ST’
    It is a member of the hidden package ‘array-0.5.1.1’.
    Use -v to see a list of the files searched for.
Failed, modules loaded: none.

<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/ghci308/ghci-script

このエラーメッセージでは、Data.ArrayData.Array.ST がインポートできていないため、エラーとなってしまったことを教えてくれています。また、array-0.5.1.1 を利用したら良いこともこのエラーメッセージから読み取ることができます。

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

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 ファイルの librarydependencies を追記します。

package.yaml
library:
  source-dirs: src
  dependencies:
  - 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

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

$ 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 exec -- minfree [0,1,2,3,4,7]
5

minfree2 の作成

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

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

package.yaml
executables:
  minfree:
    main:                Main.hs
    source-dirs:         app
    ghc-options:
    - -threaded
    - -rtsopts
    - -with-rtsopts=-N
    dependencies:
    - PFAD
  # ここから下の行を追記しました
  minfree2:
    main:                Main.hs
    source-dirs:         app
    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 exec -- minfree2 [1,2,3,4,5]
0

ドキュメントの作成

Haddock の基礎知識

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

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

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 haddock
...
/home/bm12/.stack/snapshots/x86_64-linux-nopie/lts-9.17/8.0.2/doc/index.html

ドキュメントが生成されるパスはデフォルトでは .stack 以下になっているため、ちょっと確認しづらいです。

--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 が書けると言って良いでしょう。強調のためのキーワードやモジュールのコメントなどはここでは紹介しないのでドキュメントをご参照ください。

{-|
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)
minfree :: [Int] -> Int
minfree xs = head ([0..] \\ xs)

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

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

スクリーンショット 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-9.17
packages:
- .

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

stack.yaml
resolver: lts-9.17
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-9.17
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-9.17
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 を使うために、package.yamltestshspec パッケージを追記します。

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

ここで、test/Spec.hs の内容を以下のように書き換えます。hspec-discover を利用することで、それぞれのソースファイルと一対一に対応した Spec ファイルを自動的に読み込んでテストしてくれるようになります。

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

QuickCheck に慣れよう!

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

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

$ stack ghci --no-load --package QuickCheck
> import Test.QuickCheck
> :t sample
sample :: Show a => Gen a -> IO ()

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

> 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>:5: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>:5: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' (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>:20: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:
            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 :: Random a => (a, a) -> Gen a
> sample' (choose (1,10))
[1,6,10,1,3,1,9,1,1,10,5]


-- 与えられたリストの中からランダムに値を生成する
elements :: [a] -> Gen a
> sample' (elements ["patek", "omega", "seiko"])
["omega","patek","omega","omega","patek","patek","omega","omega","patek","omega","patek"]


-- 与えられたジェネレータのリストの中からランダムに選択し、それを利用してランダムな値を生成する
> 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"]


-- 出現頻度を指定してランダムな値を生成する (この例では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 :: 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]

-- ランダムに生成された値のリストを生成する
> 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]]

-- 空リストを除く
> 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]]

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

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

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

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

> 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

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

Registering PFAD-0.1.0.0...
PFAD-0.1.0.0: test (suite: PFAD-test)


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

Failures:

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

Randomized with seed 1211983148

Finished in 0.0007 seconds
3 examples, 1 failure

PFAD-0.1.0.0: Test suite PFAD-test failed
Completed 2 action(s).
Test suite failure for package PFAD-0.1.0.0
    PFAD-test:  exited with: ExitFailure 1
Logs printed to console

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

自然数に限定する

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

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

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

Registering PFAD-0.1.0.0...
PFAD-0.1.0.0: test (suite: PFAD-test)


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

Failures:

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

Randomized with seed 398651692

Finished in 0.0028 seconds
3 examples, 1 failure

PFAD-0.1.0.0: Test suite PFAD-test failed
Completed 2 action(s).
Test suite failure for package PFAD-0.1.0.0
    PFAD-test:  exited with: ExitFailure 1
Logs printed to console

今回のエラーでは [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 にテストを埋め込んだものです。

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

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

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

-- 与えられた価格の消費税を計算する
-- calcSalseTax 100.0 == 8.0
calcSalseTax :: Num a => a -> a
calcSalseTax = (*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: doctests.hs
    source-dirs: test
    ghc-options:
    - -threaded
    - -rtsopts
    - -with-rtsopts=-N
    dependencies:
    - PFAD
    - doctest

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

test/doctests.hs
import Test.DocTest
main = doctest ["-isrc", "src/Minfree.hs"]

これで準備は完了です。

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

$ stack clean
$ stack test

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-0.1.0.0: Test suite PFAD-test passed
Completed 2 action(s).
Test suite failure for package PFAD-0.1.0.0
    PFAD-doctest:  exited with: ExitFailure 1
Logs printed to console

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

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

$ stack haddock

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

スクリーンショット 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
### Failure in src/Minfree.hs:21: expression `(as ++ bs) \\ cs == (as \\ cs) ++ (bs \\ bs)'
*** Failed! Falsifiable (after 3 tests and 3 shrinks):
[]
[0]
[]

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

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

...

-- | リスト 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

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

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

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

プログラムの配布

stack script

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

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

今回の src/Minfree.hs で実際に使い方を見てみましょう。stack script 形式では必ず main 関数が必要になります (実行するので当たり前と言えば当たり前)。ライブラリであれば stack ghci で事足りる場面も多いかもしれません。

Minfree.hs
-- main をエクスポートするようにした
module Minfree (main) where

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

-- app/Main.hs から持ってきた --
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 -- Test.hs [0,1,2,3]
When using the script command, you must provide a resolver argument

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

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

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

HLint

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

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

$ stack install hlint
$ hlint --version
HLint v2.0.11, (C) Neil Mitchell 2006-2017

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

$ 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

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

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

formatter

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

どれを使うかは、好みの問題ですが、いくつか紹介しておきます。

hindent

stylish-haskell

brittany

hoogle

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

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

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

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

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

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

# グローバルデータベースの作成
$ stack exec -- hoogle generate
# プロジェクトデータベースの作成
$ stack 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

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.