Creatable a => a -> IO b

Haskellと数学とちょびっと音楽

もうcabal hellは怖くない、Stackを使ってみるよ!

はいはいどうも、最近はずっとドラクエやってます。 ちゅーんさんです。

人生ではじめてプレイしたRPGってドラクエ6だった気がします。 スマフォ版で久々にプレイしたのですが、やっぱりアレです。
バーバラちゃん、いいですね。

あ、ひとまずラスボス倒したので、今は3やってます。
ポカパマズさああああああん!!!!

はい。

というわけで、 この記事はちゅーんさんがイマドキのHaskellのビルドツールである、 stackを2日くらいぐりぐりいじって覚えた使い方をまとめようと思います。

ざっくりと、stackってどんなもんよ、みたいな話は

http://qiita.com/tanakh/items/6866d0f570d0547df026

の前半を読むとだいたいわかりますので、 ここでは実用的な具体例はさておき、とにかく動かしてみたい人に向けて、 「ここに書かれてる通りに色々やったら、なんとなくstackの使い方が分かった気がするー。」 くらいの感じのチュートリアルになるように頑張ります。頑張りました。

2015/7/13 18:19 投稿当時、本記事でsolver内のパッケージを使用する際に、stack.yamlへの追記を行っている場所がありましたが、solverに含まれないパッケージを使用する場合にのみ設定が必要とのご指摘を頂きました。現在は修正済です。

インストール

ここ

https://github.com/commercialhaskell/stack

のHow to installを参照しましょう。

自分の場合、Ubuntu14.04なので、ターミナルから

$ wget -q -O- https://s3.amazonaws.com/download.fpcomplete.com/ubuntu/fpco.key | sudo apt-key add -
$ echo 'deb http://download.fpcomplete.com/ubuntu/trusty stable main'|sudo tee /etc/apt/sources.list.d/fpco.list
$ sudo apt-get update && sudo apt-get install stack -y

でなんか良くわからないけど入りました。てへぺろ

実行プログラムを作る

準備

まず、適当なフォルダを作って、次のような手順でstack newコマンドを実行します。

$ mkdir firstApp
$ cd firstApp
$ stack new

すると、以下のような最低限のファイルがひと通り揃ったプロジェクトファイルが出来上がります。

.
├── app
│   └── Main.hs
├── LICENSE
├── new-template.cabal
├── Setup.hs
├── src
│   └── Lib.hs
├── stack.yaml
└── test
    └── Spec.hs

3 directories, 7 files

基本的には、これをベースにいい感じに書き換えてやればOKみたいです。

まず、以下のファイルをリネームします。

  • new-template.cabal -> firstApp.cabal
  • Lib -> FirstApp.hs

で、この2ファイルとMain.hsを、それぞれ次のように書き換えます。

src/FirstApp.hs:

module FirstApp
 ( message
 ) where

message :: String
message = "Hello, Stack!"

app/Main.hs:

module Main where
import FirstApp

main :: IO ()
main = putStrLn message

firstApp.cabal:

name:                firstApp 
version:             0.1.0.0
synopsis:            Initial project template from stack
description:         Please see README.md
homepage:            http://github.com/name/project
license:             BSD3
license-file:        LICENSE
author:              Your name here
maintainer:          your.address@example.com
-- copyright:           
category:            Web
build-type:          Simple
-- extra-source-files:  
cabal-version:       >=1.10

library
  hs-source-dirs:      src
  exposed-modules:     FirstApp
  build-depends:       base >= 4.7 && < 5
  default-language:    Haskell2010

executable firstApp 
  hs-source-dirs:      app
  main-is:             Main.hs
  ghc-options:         -threaded -rtsopts -with-rtsopts=-N
  build-depends:       base
                     , firstApp
  default-language:    Haskell2010

test-suite firstApp-test
  type:                exitcode-stdio-1.0
  hs-source-dirs:      test
  main-is:             Spec.hs
  build-depends:       base
                     , firstApp
  ghc-options:         -threaded -rtsopts -with-rtsopts=-N
  default-language:    Haskell2010

source-repository head
  type:     git
  location: https://github.com/name/project

cabalファイルはちょっと解りづらいですが、パッケージ名やファイル名等をビルド出来るようにリネームしたので、 それに合わせてパッケージ名やモジュール名などを編集しただけです。

実際には、synopsisとかcategoryとか、開発するプロジェクトに合わせて色々設定しましょう。

stack build

stack buildは、プロジェクトをビルドするコマンドです。 これによって、インストールされていない依存パッケージ等が解決されます。 後述のstack ghci等を使う前に、一回は実行する必要があるみたいですので、とりあえず実行しましょう。

cabalファイルの設定に不備があれば、この段階で指摘されるので修正していきます。

stack ghci

無事ビルドが成功したらstack ghciを実行すると、FirstAppモジュールが読み込まれた状態でGHCiが起動します。

こういう時に実行されるGHCのバージョンとかがどうやって決まるのかは、今の所ちゃんと調べてません。solver(Stackageのスナップショット)の設定によるんだと思われます。

$ stack ghci
Configuring GHCi with the following packages: firstApp
GHCi, version 7.8.4: http://www.haskell.org/ghc/  :? for help
Loading package ghc-prim ... linking ... done.
Loading package integer-gmp ... linking ... done.
Loading package base ... linking ... done.
Loading package firstApp-0.1.0.0 ... linking ... done.
[1 of 1] Compiling FirstApp         ( /home/tune/Documents/Program/Haskell/StackTutorials/Tutorial/firstApp/src/FirstApp.hs, interpreted )
Ok, modules loaded: FirstApp.
*FirstApp> message
"Hello, Stack!"

尚、この時に対応するバージョンのGHCがインストールされていない場合はエラーになります。
その場合、stack setupするだけで対応するGHCが簡単にインストールされますが、 stack buildでも自動的に解決されますので、基本的にこのコマンドを使う事は無いでしょう。

うーん、階層の深い所にある、小さなモジュールだけGHCiでロードして動作確認したい場合ってどうするのが良いんですかね?

stack runghc

んで、mainを実行してみたいのであれば、stack runghcコマンドを使います。

$ stack runghc app/Main.hs
Hello, Stack!!

こんな感じで、小さい単位で実行出来るのはsrc配下のライブラリ部分だけなので、 app以下のフォルダは必然的に最小限になるようです。

stack install

で、出来上がったアプリケーションをインストールしたい場合は、stack installコマンドを使います。

$ stack install

 ...略...

Installed executables to /home/tune/.local/bin/:
- firstApp
$ firstApp
Hello, Stack!

実行ファイルにパスが通っていれば、普通にコマンドラインから実行できるようになります。

ライブラリを作る/使う

ライブラリを作る

ライブラリを作る場合も、基本的な考え方は同じで、 cabalファイルをライブラリ用に書き換えでやればOKな感じですね。

まず、firstAppと同じ感覚でプロジェクトを作成しましょう。

$ mkdir firstLib
$ cd firstLib
$ stack new

で、次のようにリネームしたり、appフォルダを削除したりしまして…

$ tree
.
├── firstLib.cabal
├── LICENSE
├── Setup.hs
├── src
│   └── FirstLib.hs
├── stack.yaml
└── test
    └── Spec.hs

各ファイルを以下のようにして書き換えます。

src/FirstLib:

module FirstLib
    ( firstLib
    ) where

firstLib :: IO ()
firstLib = putStrLn "`firstLib` called!"

firstLib.cabal

name:                firstLib
version:             0.1.0.0
synopsis:            Initial project template from stack
description:         Please see README.md
homepage:            http://github.com/name/project
license:             BSD3
license-file:        LICENSE
author:              Your name here
maintainer:          your.address@example.com
-- copyright:           
category:            Web
build-type:          Simple
-- extra-source-files:  
cabal-version:       >=1.10

library
  hs-source-dirs:      src
  exposed-modules:     FirstLib
  build-depends:       base >= 4.7 && < 5
  default-language:    Haskell2010

test-suite firstLib-test
  type:                exitcode-stdio-1.0
  hs-source-dirs:      test
  main-is:             Spec.hs
  build-depends:       base
                     , firstLib
  ghc-options:         -threaded -rtsopts -with-rtsopts=-N
  default-language:    Haskell2010

source-repository head
  type:     git
  location: https://github.com/name/project

後は、stack buildでビルドして、stack ghciで動作確認してみましょう。

で、これをStackageに上げるためには、stack uploadを使うみたいなんですが、 この辺はまだ試していないので良くわからんです(`・ω・´)キリッ

作成したライブラリを使う

さて、今作ったfirstLibfirstAppから使う方法を説明します。
firstApp側のstack.yamlと、firstApp.cabalそれぞれに、firstLibに依存している事を記述します。

cabalファイルはご存知の通りプロジェクトをビルドするための情報ですが、stack.yamlはstackがパッケージの依存関係等を安全に解決するための設定ファイルです。

stack.yaml

flags: {}
packages:
- '.'
- '../firstLib/'
extra-deps: []
resolver: lts-2.17

firstApp.cabal

 ...略...

executable firstApp 
  hs-source-dirs:      app
  main-is:             Main.hs
  ghc-options:         -threaded -rtsopts -with-rtsopts=-N
  build-depends:       base
                     , firstApp
                     , firstLib
  default-language:    Haskell2010

 ...略...

で、app/Main.hsFirstLibimportしーの…

app/Main.hs

module Main where
import FirstApp
import FirstLib

main :: IO ()の
main = do
  putStrLn message
  firstLib

stack buildでビルドしーの、stack runghcで実行。

$ stack build
$ stack runghc app/Main.hs
Hello, Stack!
`firstLib` called!

良い感じですね。

Stackageのライブラリを使う

試しに、lensをfirstAppから使えるようにしてみましょう。

んで、最初solverのパッケージを使うためには単純にstack.yamlに記述が必要だと思っていたのですが、 どうやらcabalファイルにパッケージ名を追記すればOKなようです。

firstApp.cabal

 ...略...

executable firstApp 
  hs-source-dirs:      app
  main-is:             Main.hs
  ghc-options:         -threaded -rtsopts -with-rtsopts=-N
  build-depends:       base
                     , firstApp
                     , firstLib
                     , lens
  default-language:    Haskell2010

 ...略...

もう一度、現段階のstack.yamlの設定を見てみましょう。

stack.yaml

flags: {}
packages:
- '.'
- '../firstLib/'
extra-deps: []
resolver: lts-2.17

resolverの設定がlts-2.17となっていますが、これはこのプロジェクトと対応したsolverのバージョンです。

Stackageのサイト内にある、該当するsolverのHoogle検索でlensに関する演算子を検索してみましょう。

https://www.stackage.org/lts-2.17/hoogle?q=%28.~%29

ここから、LensのHaddockに飛ぶと、バージョンが4.7.0.1になっています。 このようにして、自動的にインストールされるパッケージのバージョンを知る事ができます。

んで、ビルドして実行。
勝手にlensをStakageからダウンロードして、インストールしてくれます。

$ stack build
$ stack runghc app/Main.hs
Hello, Stack!  `firstLib` called!
---------------------------
222
("Hoge","Stack","Fuga")

Lensは巨大なライブラリなのでビルドには時間がかかりますが、 同じsolverのライブラリ群はどのような組み合わせでビルドしても、依存関係で詰まる事は無い事がわかっているので、 コーヒー片手にすごいH本でも読みながらまったり待ちましょう。

テストを書く

そういえば、なんやかんや型のおかげであんまりテストを必要に感じる事がなくて cabal経由でテストを試してみた事が無かったわけですけど、 いつ必要になるかわかりませんし、stackの練習も兼ねて作ってみましょう。

今回テストを追加してみるのは、firstLibの方です。

そういえば、firstLibの関数をIOにしてしまってましたね…、 面倒くさいので、テストしやすいようにメッセージ部分を切り離します。

src/firstLib.hs

module FirstLib
  ( firstLib
  , libMessage
  ) where

firstLib :: IO ()
firstLib = putStrLn libMessage

libMessage :: String
libMessage = "`firstLib` called!"

で、意味の無いテストですが、HUnitを使ってこのメッセージの内容をチェックするテストを書いてみます。

test/Spec.hs

 ...略...

test-suite firstLib-test
  type:                exitcode-stdio-1.0
  hs-source-dirs:      test
  main-is:             Spec.hs
  build-depends:       base
                     , firstLib
                     , HUnit
  ghc-options:         -threaded -rtsopts -with-rtsopts=-N
  default-language:    Haskell2010

 ...略...

test/Spec.hs:

import Test.HUnit
import FirstLib

messageTest :: Test
messageTest = "MessageTest" ~: libMessage ~=? "`firstLib` called!"

main :: IO ()
main = runTestTT messageTest >>= print

これで、stack testを実行すれば、このテストを実行する事ができます。

$ stack test
firstLib-0.1.0.0: test (suite: firstLib-test)
Cases: 1  Tried: 1  Errors: 0  Failures: 0
Counts {cases = 1, tried = 1, errors = 0, failures = 0}

OKですね。

まとめ

とゆーわけで、ざっくりとstackの使い方をまとめてみました。

  • cabal sandboxよりはだいぶ使いやすい
  • ライブラリ間の依存関係に悩まされなくて済む
  • GHCのバージョンが上がってもプロジェクトが死なないで済む

とゆー感じで、ちょっと触った感触だと、かなり魅力的なビルドツールと言えます。

あと、既存のプロジェクトをstackで管理できるようにするstack initなんてコマンドもあるみたいですが、 これは近々に必要なので試してみようと思います。気が向いたらまた記事にするかもしれません。

そういえば、他の言語だと、こういう問題ってどうやって解決してるんでしょう?
それから、複数のsolverに対応したい場合とかって、どうすれば良いんですかねー(´・ω・`)

とかまぁ、色々気になる点はありますが、 慣れてくるとなかなか良い感じなので、愛用していこうと思いました。まる。

ではではノシノシ