cabalを使ってHaskellのプログラムを作り始める時のメモ

公開しようがしまいが、Haskellでプログラムを作る時はcabalを全面的に使うといろいろメンテしやすい。しかしその使い方(特に.cabalファイルの書き方)は自明ではないということで、いろいろ調べたのでメモ。

.cabalファイルには、とりあえずlibraryセクションは書いておく

executableセクションがあろうがなかろうが、自分で書いたモジュールのほぼ全てをlibraryセクションに突っ込んでおくのがよいようだ。executableセクションやtest-suiteセクションで自分の書いたモジュールを参照するためには、各セクションのbuild-dependsに自分自身のパッケージ名を書いておく。

なお、executableパッケージを作る場合、libraryセクションがなくてもtest-suiteのhs-source-dirsにexecutable用のコードを入れたディレクトリを指定すればテストを書くことができる。しかしこれには以下の欠点がある。

  • executable用のコードはexecutableのビルド時とtest-suiteのビルド時の2回、コンパイルされてしまう。
  • test-suiteのbuild-dependsにはテストコードと参照するexecutable用のコードの両方の依存パッケージを書く必要がある。

両方共DRY原則に激しく反するため、精神衛生上よろしくない。libraryセクションを使うべきである。

参考:

テストで使うモジュールは全てexposeする

上記のようにlibraryセクションを設けてtest-suiteでそれを参照する場合、テストコードから参照できるのはlibraryセクションのexposed-moduleに書いたモジュールのみである。しかし(あまりよいことではないかもしれないが)、ユーザーに見せないプライベートな関数をテストしたり、テスト中で内部ユーティリティを活用したりしたいことはしばしばある。

そのような、「テストでは使うがユーザーには見せない」モジュールをうまいこと.cabalで指定する方法は今のところないらしい。そういったものも全てexposeする必要がある。exposeしてはいるがユーザー向けではないモジュールには名前に"Internal"を含めたりするのが慣習となっている。

参考:

doctestとhspecでテストを記述する

参考:

cabal initやhiでひな形を生成する

cabal initコマンドを使うと、パッケージのひな形を作成することができる。しかしcabal initは今のところtest-suiteセクションやテストのエントリポイントを自動生成しないため、毎回手で{-# OPTIONS_GHC -F -pgmF hspec-discover #-}と書くことになって精神衛生上よろしくない。

そこでひな形の生成にはhiというコマンドを使うとよいようだ。このコマンドはデフォルトでHspecのエントリポイントを生成する。また、ひな形の実体はgitレポジトリであるため、カスタマイズしたい場合はgithub上でforkしていじればOK。

参考:

forkIOがmask状態を引き継ぐ意味が分からない

(注: この記事はghc 7.6.3, base-4.6.0.1で検証している)

HaskellのIOモナドには(フツーの命令型言語で言うところの)例外の仕組みが備わっているが、この例外には2種類がある。

  • 同期例外(synchronous exception): スレッド自身が行う処理によって発生する例外
  • 非同期例外(asynchronous exception): スレッドの外部から(throwTo関数などによって)送りつけられる例外

非同期例外はいつ発生するのか本当に予測不可能な例外であり、どちらかというとPOSIXのシグナルにイメージとしては近いと思う。

不用意なタイミングで非同期例外が発生するとプログラムの状態がおかしなことになってしまうため、Haskellには非同期例外の発生を一時的に抑制する仕組みがある。

mask_ :: IO a -> IO a

mask_関数は引数としてIOアクションをとり、非同期例外の発生を抑制した(マスクされた)アクションに変換して返す。

マスクされたアクションの実行中は、そのスレッドで非同期例外が発生することはない。その間、throwTo関数で非同期例外を送信したスレッドは、送信先スレッドのマスクが解除されて非同期例外がちゃんと発生するまでブロックする(これはghcに固有の動作かもしれない)。そのため、マスクされるアクションの範囲は必要最小限に抑えることが重要となる。

さて、Haskellではスレッドを作る時は一般的にforkIO関数を使う。

forkIO :: IO () -> IO ThreadId

forkIOは引数としてIOアクションをとり、それを別スレッドで実行し、呼び出し元にはスレッドIDを返す。

ここで重要なのは、forkIOで発生させたスレッドは親スレッドのマスク状態を引き継ぐという点である(これはghcに固有の動作かもしれない)。

そのため、親スレッドが非常に限定的な範囲でマスクしていたとしても、たまたまマスクした状態で子スレッドを作った場合、子スレッドは全体がマスクされることになる。

-: マスク解除状態の処理
*: マスク状態の処理

親スレッド:  -----------***------------------- ...
                         |
                      [forkIO]
                         |
子スレッド:              ********************* ...

さらに悪いことに、Haskellの例外処理関数の中には与えられたアクションをマスクして実行する類のものがある。

例えば、bracket関数の第1引数(リソース獲得アクション)と第2引数(リソース解放アクション)はマスクされる。これは以下のコードで検証できる。

import Control.Exception (bracket, getMaskingState)

showMask label = do
  state <- getMaskingState
  putStrLn (label ++ ": " ++ show state)

main = bracket (showMask "before") (const $ showMask "after") (const $ showMask "body")

-- before: MaskedInterruptible
-- body: Unmasked
-- after: MaskedInterruptible

したがって、例えばリソース獲得アクションとしてforkIOを書いてしまうと子スレッドは全体がマスクされる。

この事実はドキュメントには書かれていない。そもそもマスク状態は型に表れないので、ユーザは関数へ渡したアクションがマスクされるのかされないのか確実に見極めることはできない。

このように、マスク状態の引き継ぎは子スレッド全体が意図せずマスクされてしまう事態を引き起こすが、そもそもなぜforkIOの仕様がマスク状態を引き継ぐようになっているかが理解できない。非同期例外はスレッドごとに独立なのだからわざわざ引き継ぐ必要はないのではないか。

親スレッドのマスク状態に関わらず子スレッドのマスクを解除するには、とりあえず以下のようにすればよい・・・と思う。

{-# LANGUAGE CPP #-}
import qualified Control.Concurrent as CC
#if MIN_VERSION_base(4,3,0)
#else
import qualified Control.Exception as CE
#endif

forkIOUnmasked :: IO () -> IO CC.ThreadId
#if MIN_VERSION_base(4,4,0)
forkIOUnmasked action = CC.forkIOWithUnmask (\unmask -> unmask action)
#elif MIN_VERSION_base(4,3,0)
forkIOUnmasked = CC.forkIOUnmasked
#else
forkIOUnmasked action = if CE.blocked then CE.unblock $ CC.forkIO action else CC.forkIO action
#endif

マスク周りのAPI(以前は"ブロック(block)"と呼ばれていた)はいろいろと変更になっているため、古いbaseパッケージもサポートしようと思ったらバージョンごとに実装を変える必要がある。

正直、何か見落としている点があるかもしれないので上記のコードもそれほど自信はない。。

むつかしいことを考えずにHaskellでCSVをパースする

まともに使い物になるプログラムを作ろうとすると、プログラムの外から何らかのデータを取り込んでパースし、内部で使えるデータに落としこむ処理というのはほとんどの場合で必要になる処理だろう。

しかし、Haskellではこのパース処理を記述するのが意外なほど難しい、と自分は思う。というのも、パース処理というのは「外界から」のデータを「一つずつ」読み込み、しかもそのあらゆる過程で「失敗するかもしれない」処理だからだと思う。もうこの時点で複雑なモナドが必要な気がしてくる。

ということで、Haskellにはこういった面倒なパース処理をうまくやるためにParsecなどのライブラリがあるので、普通は素直にこれを使うのがよい。

しかし、初学者にとってはParsecの学習コストもバカにはならない。特に、パースしようとする対象がごく単純なCSVフォーマットだったりする場合はわざわざParsecの使い方を習得するのもおっくうになるというものである。

そこで、Parsecを使わずにざっくりとCSVフォーマットをパースする処理を書いてみた。

続きを読む

どれほどのHackageにChangelogがないのか調べてみたHaskellで

Haskellを勉強し始めて驚いたのが、あまりにも多くのHackage(Haskellにおけるライブラリパッケージ)にChangelogが存在しないことだ。

当たり前だが、Changelogがなければモジュールのバージョン間の変化を俯瞰することができない。Haskellの文化では互換性を破壊するモジュールの変更がある程度許容されるそうなので、このことは著しい不便を生むことがある。(ただし、Cabalパッケージ間の変化を調査するprecisというプログラムもあるようだ)

しかし、「ChangelogのないHackageが多い」というのはあくまで主観的な印象であり、そのようなHackageが正確にいくつあるのかは分からなかった。そこで、Haskellの練習がてらこれを調べてみた。

続きを読む

Haskellでの名前空間管理

ここ最近、性懲りもなくまたHaskellを書いてみている。少しずつコツがつかめてきたのか、以前よりストレスなく書けるようになってきたと思う。(Perlに比べると書くスピードは半分くらいだが)

ただ、ある程度の大きさのプログラムを1枚のファイルに書いていると、そのファイルの名前空間がどんどん散らかってきてキツいと感じるようになってきた。

続きを読む

HaskellのlensをPerlに移植したData::Focus 0.01をリリース

HaskellのlensライブラリのAPIと実装をてきとうにパクってきて、Data::FocusというPerlモジュールを作った。

現状、Haskellのコンパクトなlens実装であるlens-family-coreパッケージに近い作りになっている。lensパッケージは複雑すぎてまだ全容を捉えきれていない。

lensについて一旦忘れると、Data::Focusは複雑に入れ子になったデータ構造へアクセスするためのモジュールである。Data::Diverの親戚と言える。

use feature qw(say);
use Data::Focus qw(focus);

my $target = {
    foo => [0, 1, { bar => "buzz" }]
};

say focus($target)->get("foo", 2, "bar"); ## => buzz

この程度ならData::Diverと変わらない。実際、Data::Diverではこう書ける。

use Data::Diver qw(Dive);
say Dive($target, "foo", 2, "bar");

Data::Diverでは"foo"や2といったキー部分にundefやarray-refを与えることでもう少しトリッキーなデータアクセスも可能としている。

一方、Data::Focusはこの考え方をさらに発展させ、Data::Focus::Lensを継承するクラスのオブジェクトをキーとして与えると、そのLensの実装に応じてデータアクセスの方法を様々に変化させることができる。これにより、ハッシュや配列のtraverseを行ったり、ハッシュや配列以外のオブジェクトへアクセスしたりといったことが可能となる。

続きを読む

Prismメモ

前回の続きで、今回はPrismについて調べたのでそれについて記す。(lensパッケージ バージョン4.7に基づく)

前回書いたように、PrismはTraversalの一種であり、全体データの中の0個もしくは1個の部分データに着目する。また、Prismは「部分データから全体データを復元することができる」という、Lensにはない特徴を持つ。(そのため、Prismでは「全体」と「部分」という言い方は不適切かもしれない)

>>> (Right "hoge") ^. _Right
"hoge"
>>> "hoge" ^. re _Right
Right "hoge"

この、Prismの効果を「逆転」する関数であるreがどういう仕組みになっているか気になったので、lensのマニュアルソースコードを読んだ。

続きを読む