Haskell
stack

Haskellのstackによるプロジェクトについて

この記事について

一、二ヵ月ほど前にHaskellに入門したばかりの著者です。そのためHaskellを学ぶためにいろいろな本を読んだりサイトを見たりして学んでいるのですが、どうもHaskellの文法や関数型、モナドについてばかりの情報がほとんどでプロジェクトを作成した後についての操作法がなかなか見つかりません。そこで少しの情報を基にいろいろ試行錯誤しました。それでわかってきたことを初心者ながらに説明しようと思い、これを書いています。そしてこの記事はHaskell (その4) Advent Calendar 2017の16日目の記事です。

プロジェクトの作成

まずプロジェクトを作ります。そのためにコマンドプロンプト上でプロジェクトを作りたいディレクトリの直下に移動してください。そこでstack new <projectname> [template] コマンドを使います。(この記事では"<>"で囲まれたものを使用者毎に入力してもらうコマンドとし、"[]"で囲まれたものを省略可能なコマンドとして扱うことにします。)ただしprojectnameには作成したいプロジェクト名、templateは使用するテンプレートです。このテンプレートはstack templatesとすると確認することができるテンプレートの中から選択できます。デフォルトではsimpleを使用することになっています。今回はstack new testProjectコマンドによりプロジェクトを作成してこれを基に説明していきます。
コマンドの実行後、次のようなファイル群が作られているのが確認できます。

  • testProject
    • app
      • Main.hs
    • src
      • Lib.hs
    • test
      • Spec.hs
    • .gitignore
    • ChangeLog.md
    • LICENSE
    • package.yaml
    • README.md
    • Setup.hs
    • stack.yaml
    • testProject.cabal

これらのうち.gitignoreChangeLog.mdの二つはgitを使い始めたら出てくるようになり、package.yamlは最近突然に生成されるようになっていたので出ない人もいるかも入れません。また~/sr/config.yaml

config.yaml
templates:
  scm-init: git
  params:
    author-name: <Your Name>
    author-email: <youremail@example.com>
    category: <Your Projects Category>
    copyright: <'Copyright (c) 2017 Your Name'>
    github-username: <yourusername>

ここを参考に編集すると.gitファイルも同時に生成してくれるようになりGitをすぐに使えるようになります。

生成されたファイルやフォルダの紹介

appフォルダ

このフォルダは実行用のファイルを入れておくところです。なので生成されていたMain.hsは実行用のファイルです。実際にmain関数が書いてあります。中身を確認してみます。

Main.hs
module Main where

import Lib

main :: IO ()
main = someFunc

まずMainモジュールを宣言していますね。(なぜここでモジュールとして扱うように宣言しているのか著者にはわかりません)そしてsrcフォルダにあるLibモジュールを使うように宣言しています。(ここで注意したいのはLib.hsファイルを使うという宣言ではなくLibモジュールを使うという宣言なのでsrc/Lib.hsファイルのことだと勘違いしないこと。著者は最初勘違いして少し泥沼にはまった。)このLibモジュールはLib.hsファイルにて宣言されています。
次にmain関数の型を宣言してから本体を宣言しています。Haskellには強力な型推論が備わっているので型の宣言は特に必要ないのですが、トップレベルだからあった方がいいということでmain関数ですが宣言されているようです。そしてこのmain関数で使うsomeFuncの実装はLibモジュールに書かれています。そのモジュールについて書いてあるLib.hsファイルで説明するのでここでは説明しません。

srcフォルダ

このフォルダはappフォルダ内で使う「道具」としての実装を記述したファイルを入れておくところです。主にモジュールを置くことになると思います。なのでLib.hsのようにじかにモジュールとしてファイルが置いてあったり、新しくDataフォルダを作ってその下にモジュールを作って階層分けして置いてあったりして使われます。このようなことからmain関数が書かれているファイルが存在しないのはすぐにわかります。(逆に考えるとmain関数があるファイルだけでプロジェクトを作るならこのsrcフォルダは使わないことになる。)
では、srcフォルダ内に生成されたLib.hsファイルを見てみましょう。

Lib.hs
module Lib
    ( someFunc
    ) where

someFunc :: IO ()
someFunc = putStrLn "someFunc"

まず私はLibモジュールでsomeFunc関数だけ外に見せると宣言しています。そしてsomeFunc関数の型宣言をしたのち実装を定義しています。自動生成直後ではただ「someFunc」と表示するだけみたいです。

testフォルダ

このフォルダはプロジェクトのテスト関係のファイルを置くところです。著者がHaskellのテストについてまだ何もわからないので説明はできません。なので説明は省略させていただきます。

LICENSE / README.md / Setup.hs / stack.yamlファイル

LICENSEREADME.mdファイルは名前の通りのファイルです。LICENSEファイルはある方法でどんなライセンスを使うか変更できるようです。(おそらく後で紹介するpackage.yamlに記述するものかと思われる。)
Setup.hsファイルはstack newコマンドによってどんなプロジェクトが作られたのかの情報が書かれています。stack.yamlファイルにはそのプロジェクト固有のオプションが書かれている(?)みたいです。正直Haskell初心者である著者には何もわかりませんが小さいプロジェクトを作って実行するまでいじる必要はないものだと思われるので無視しても構いません。

package.yaml / testProject.cabalファイル

package.yamlファイルは Hpack(?)が使用可能なときに生成されるらしいファイルで、ビルド時にtestProject.cabalを自動生成してくれるパッケージ管理ファイルです。どちらのファイルも「#」以降の同行の文字列はコメントになります。先に同じパッケージ管理ファイルのtestProject.cabalについて説明します。

testProject.cabalについて

正直前半はプロジェクトに関する説明なので、見れば何を意味しているのか分かるので省略します。このcabalファイルでは複数指定する場合は「,」で区切ります。
以下に後半のlibraryからを表示します。

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

executable testProject-exe
  main-is: Main.hs
  hs-source-dirs:
      app
  ghc-options: -threaded -rtsopts -with-rtsopts=-N
  build-depends:
      base >= 4.7 && < 5
    , testProject
  default-language: Haskell2010

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

まずlibraryに関しての説明をします。これはsrcフォルダと関係しています。何を言ってるかわからないと思うので説明の為、上から見ていこうと思います。
まずhs-source-dirsは自身の作成するHaskellのソースコードを置いておくところです。これによりsrcフォルダが認識されその中のLib.hsファイルも認識できるようになります。
次にbuild-dependsについてです。これは以下でも何回も出てくる設定です。文字通り「ビルド時に依存するパッケージ」についての設定となります。今回はlibraryでの設定なのでsrcの下にあるファイルが使うモジュールをここに記述していきます。例えばCotrol.Lensを使うときはlensを追記します。(ここで指定するのはパッケージ名)また、最初から入力されているbase <= 4.7 && < 5はHaskellのどのベースパッケージを使うかを意味しています。これによりパッケージの依存関係の解消時に使用できるスナップショットの範囲が大体決まります。パッケージが古いものの場合baseがあっていないこともあるかもしれないのでその時はHackageでパッケージを検索して確認してみてください。
次はexposed-modulesについてです。ここでは外部にさらされる(exposed)とき、すなわちappフォルダやsrcフォルダ内のファイルが同フォルダ内のモジュールを使用するときに使いたいモジュール名を書きます。生成直後ではappフォルダ内のMain.hsLibモジュールを使用するのでLibが書かれています。
最後のdefault-languageについてですがHaskellの歴史上HaskellにはHaskell 1.0, Haskell 98, Haskell′, Haskell 2010の四つのバージョン名がついているのでどのHaskellを使うかについて書くところになります。著者の環境ではHasell 2010で生成されており最新ですね。
以下の説明ではかぶるところは適宜省略していきます。
次にexecutableについて説明します。基本同じですね。しかしlibraryとは違いすぐ横にtestProject-exeと書かれています。これはビルドしたときに出来上がる実行ファイル名を意味しています。つまりこのままビルドするとtestProject-exe.exe(著者はWindowsを使っているのでexeファイルとしている。)が出来上がることになります。(いやなら好きなように変えてしまいましょう。)この関係は後で説明するtest-suitetestProject-testでも(著者はHaskellのテスト関係は全然わからない為)同じだと思われます。
そしてmain-isについてですが名前からするとmain関数を持つファイル名の一覧か最初に実行するファイルの名前です。(著者は複数の実行用ファイルを使ったことがないのでわかりません。)しかし、初心者時点では単にmain関数があるファイルの名前だと思っても差し支えなさそうです。そしてそのファイルが書かれているフォルダを指定するのが次のhs-source-dirsです。
次のghc-optionsではstackでstack buildコマンドを実行時に使用するghcのオプションとして設定するオプションを指定できます。
次はbuild-dependsについて説明します。ここに書くのはlibraryで書くのと考え方は基本同じ(つまりapp以下で使用する外部パッケージについて書く)ですが、一つだけ違います。それはtestProjectの記述です。これによりlibraryで記述したexposed-modulesオプションにおけるモジュール(今でいうとLibモジュール)が使用可能になります。
最後にtest-suiteについて説明します。と言いたいところですが、type以外は他と同じようなものであり、その唯一違うtypeについて著者はわからないので説明できません。

package.yaml について

testProject.cabalを自動生成する似たような書き方の package.yamlで、実際に書き方も似ています。ただ区切りが「,」ではなく「-」となることには注意しておいてください。
pacage.yamlファイルもtestProject.cabalファイル同様に前半はそのプロジェクトの情報なので省略し、後半だけ説明します。
後半を以下に表示します。

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

library:
  source-dirs: src

executables:
  testProject-exe:
    main:                Main.hs
    source-dirs:         app
    ghc-options:
    - -threaded
    - -rtsopts
    - -with-rtsopts=-N
    dependencies:
    - testProject

tests:
  testProject-test:
    main:                Spec.hs
    source-dirs:         test
    ghc-options:
    - -threaded
    - -rtsopts
    - -with-rtsopts=-N
    dependencies:
    - testProject

見てすぐにわかるようにexecutablestestsの内容はtestProject.cabalと同じです。また、名前が複数形になっておりインデントの深さからわかるように同様にtestProject-○○以下をそれぞれインデントの深さに気をつけて下につなげていけば複数いけるみたいです。(情報としては聞いたことがあるが著者はやったことがないため。)
さて、libraryにおいてexposed-modulesオプションがなくなっていることにお気づきでしょうか。これは自動でlibrarysource-dirsから探してくれるので不要になったようです。なお、同じく消えているlibrarydependenciesは、自動生成時には生成されたプロジェクトのファイルの都合上その時には必要ないので削除されているだけ見たいです。なので自分で新しくオプションとして書いても問題がないようです。(その証拠に下に記したビルドの例がある。)
最後に説明の都合上順番が逆になってしまいましたが、一番上にあるdependenciesについて説明します。これはlibraryexecuablestestsで共通して依存するものを書くときに使われます。自動生成時にはtestProject.cabalでも何回も書かれていたbaseについて書いてあることからもわかるかと思います。

プロジェクトのビルド

普通にビルドするだけではつまらない(新しめのstackでstack buildをすれば問題なく終わるため。)ので、少し編集してビルドしてみます。今回は以下のように編集してみることにしました。

Lib.hs
{-# LANGUAGE TemplateHaskell #-}

module Lib
    ( someFunc
    ) where

import Control.Lens

someFunc :: IO ()
someFunc = putStrLn "someFunc"

標準に入っていないLensパッケージを使ってみることにしました。
まずこの状態でstack buildとコマンドプロンプトに入力してみましょう。多分コンパイラに「Control.Lensて何?知らないよ?」と怒られるでしょう。なので、stack install lensとしてパッケージlensをインストールしておきます。するとそのプロジェクト内でlensパッケージが使えるようになります。(ここでlensパッケージとしたのはControl.Lensモジュールが所属するパッケージ名がlensであるため(hackage参照))lensパッケージは大きいパッケージなので少し時間がかかるので少し待ちましょう。
インストール終了後、もう一度stack buildしてみましょう。すると先とは違ったエラーが出てきます。そこには「そのControl.lensモジュールが書かれたパッケージ見つかったはいいがtestProject.cabalファイルに記述されていない。 」というようなエラーのようです。ならば記述してやりましょう。今回は著者の環境ではtestProject.cabalが自動生成されて効果を確認できないためpackage.yamlに書きます。今回はsrcフォルダ内でしか使っていないのでlibraryオプションに書き、以下となりました。(dependencies- lensを追記しても問題なし。)このとき「-」の後には空白を開けておかないとパースエラーになってしまうので注意してください。

package.yaml
library:
  source-dirs: src
  dependencies:
    - lens

今回の例の作成時には著者の環境でstack buildしたときにlens-4.15.4がほしいといわれていました。ですが、package.yamlが問題が起きないように自動的にバージョンを指定してくれるのでバージョン指定をしなくても大丈夫です。
そして保存してからもう一度stack buildすると今度は問題なくビルドがとおって実行ファイルができます。
実はstack install lensコマンドを使わなくてもいきなりpackage.yamlに記述するとビルド時にインストールしてからビルドをしてくれます。便利です。

作成した実行ファイルを実行する

ビルドしたファイルは生成されている.stack-workフォルダ内の深いところにあります。これはビルド時のメッセージを読めば場所はすぐにわかるのですが、いちいち探すのもめんどくさいのでstackには実行に関する二つのコマンドがあります。ここではそのコマンドを説明します。
実行に関係するコマンドは二つあります。stac exec <fiileName>stack install <projectName>の二種類です。(余談 : 実行ファイルがもしファイルを作成したらファイルがどこにできるか、まだわからないのでわかる人教えてください)

stac exec <fiileName>

その場でfileNameから始まる実行ファイルを実行します。今回説明時に変更した以外のところを何も手を加えなければstack exec testProject-exesomeFuncと表示されます。

stack install <projectName>

stack install testProjectとコマンドを入力すると.stack-workフォルダ内からプロジェクトの成果物がインストールされます。場所はコマンドを実行したときに表示されるのですが、決まって同じところのようなのでパスを通しておくと、コマンド(例えばtestProject-exe.exe)ですぐに実行できるのでよさそうです。
さて、コピー先のフォルダを見てみるとtestProject-exeを名前に持つ実行ファイルが見つかります。なのでそれを実行してみましょう。(ダブルクリックや、コマンドプロンプト上でtestProject-exe.exeと打つかドラッグアンドドロップしてエンターして実行させましょう。)すると問題なくsomeFuncと表示されます。

締めの言葉

以上で説明は終わりになります。だらだら長いわかりにくい文章を最後まで読んでいただきありがとうございました。以下は雑談のような蛇足のようなものなので読んでいただかなくても構いません。

雑談のような何か

ごく最近Haskellに入門したばかりでCabalヘルなどを知らないし、Haskellの開発環境事情やGUI事情、テスト事情なども全くわからない著者です。(教えていただけるとありがたいです。)そんな著者ですが、この記事を書こうと思った原因があります。それはtwitterで「Haskellを広めたいならstack等の周辺ツールについての説明を増やすべき」と発言したことです。これが著者が思っていたよりも多くの賛同が得られてしまいました。なので「他人に命令するなら自分も何かしろ」ってことでこの記事を初心者ながら書いてみました。Haskellを広めたいと思っている人は案外こういったHaskellの周辺の方が需要あるんだよってことを頭の片隅に置いておいてくださるとありがたいです。共感した方はどうか周辺ツールについての説明をお願いします。