砂場遊びは地獄のかほり

この記事は Haskell Advent Calendar 2013 1日目前半の記事です.後半の記事もよろしく.


cabal sandboxは便利だけどdep hellが無くなったわけではない.ほとんどにわかに見えなくなっただけだ.むしろsandboxに甘えて針穴通すようなbuild-dependsを書いてしまうようでは,すぐにまた地獄は-よりおぞましくなって-我々の前にその姿を現すだろう.しかも,そうなったときは最早避けようもない.みんなgems/bundlerやらcpan/cpanmやら先達のアレコレ見てきてイロイロと「こうなってるとマズい」という反省も貯まっているんじゃないのかねぇ?

特に,Debianなど全体で一貫した依存関係を目指し,かなりの気合いを入れてhackageも取り込んでメンテナンスしてくれてるディストリビューションにおいて

  • dep hellに引き摺り込むようなhackageが忌避されパッケージングされない/外される
  • dep hellを避けるために妙に古いバージョンのものが使われ,更新できない

といった事態を発生させるかもしれない.これは大きなマイナスだ.そんなことになるようだと,いくらHaskell良い言語でみんな使えばと勧めたところで使う気が起きなくもなるだろう.いくら「成功を回避してきた」とか言っても限度はあるだろうし,関数はコンポーザビリティが高いんだよーとか言い放ちつつライブラリ同士はdep hellのせいでコンポーザビリティが無くなってしまったんだなどとなったらジョークにしても全然笑えない.

現実的には特にWeb系のパッケージは更新やインターフェースの変更やたら多くて頻繁だし*1,難しい面もあるにはある.が,地獄のような未来を回避し,あかるくケンゼンなHaskell界隈を守るために,何かLibraryだのExecutableだのを公開する際には,当然だけど,

  • できるだけdependencyをスリムにし,いたずらに多くのhackageに依存させない
  • できるだけdependencyを広いバージョンに対応させ,いたずらに狭いバージョンのみに依存させない
  • 実際に広いバージョンやいろいろな環境でテストしておく

といったことに気を配る必要がある.今回は,そのための小技をいくつか記しておきたい.


Since *.*.*.*

これは,言語の機能でもcabalの機能でもなんでもないが,使っているライブラリのどのバージョンに対応しているかをcabalファイルに記述する際に「メッチャたすかるわー」と思うhaddock記述における慣習で,主にConduit系というか,まぁMichael Snoyman系?のhackageでよくやってるなーという印象.こういうやつだ.つまり,そのインターフェースはそのライブラリのどのバージョンから存在しているかをSinceなんちゃらという形でドキュメントに残すされるようにしてある.もし同じ名前でもインターフェース(型)が変われば,Sinceは更新される.

こうなってくれてると,自分がcabalファイルでbuild-dependsに依存バージョンのレンジを記述する際に,対応している下限がどれになるのがとても調べ易い.最新のhaddockだけ見ればいいからだ.そのライブラリの古いバージョンのhaddockをバイナリサーチで虱潰しに探す作業をしなくていいのである.とりあえずみんなこれやるべき.

本当は,パッケージ合わせるのも型でなんとかなればいいと思うし,なんかそういうことしようとしてる/してたみたいな話も聞くには聞くので,そういう未来も良いかもしれない.どうみても私はよい子全一なのでサンタさんにおねがいしておいた.


asTypeOfで助えるdependencyがあるかもしれない

Aに依存したBを使うとき,Bのインターフェースを使うためにAもbuild-dependsに入れなきゃいけない事態って割とある.特にAの関数としては何も使わないのに,Bの関数を使うときにAに定義された型を明示しなきゃならないとかそういうケースだ.そんなとき,自分が作ってるものをCとすると,既にA <- Bという依存があってAが入ってるのは確定なのに,必要としているB <- Cの他にA <- Cとという依存も明示的に付けなきゃならなくなって,Aの動向も追わなきゃならなくなる.これはどうにも無駄ではないか.

そんなとき,場合によっては,普段あまり使われることが無いPreludeの関数第一位に燦然と輝くかもしれないasTypeOfによって,型が推論され得る値さえあれば,型は同じハズだけど推論が効いてない全然別の値に対し,型情報だけ伝搬させられるケースがある.上手くいけば,内々に型情報を伝搬できる設計にできて,明示的にAに依存させる必要がなくなるかもしれない.あくまでもかもしれない.


Travis CIのhaskell用モードを使わない

Travis CIでテストを流している場合,.travis.ymlに"language: haskell"を使うと,Haskellプロジェクトを簡単に扱える設定になる.参考はここここ.しかし,後者のページにある通り,

Haskell workers on travis-ci.org use Haskell Platform 2012.2.0.0 and GHC 7.4.1.

と,微妙に古いHaskell PlatformとGHCひとつだけでしか動作確認されずもったいない.どうせなんだし,いろんなバージョンのHaskell PlatformやGHCを同時に確認しておきたい.

ではどうするかというと,世の中には親切な人が親切をおすそわけしてくれている.スバラシイ.このmulti-ghc-travisを使うと複数バージョンのGHCやらHaskell Platformの設定で,Travis CIでテストをマトリクス実行することができ,古い環境に対する対応状況などを崩さないように,あるいは崩れたらそのことがわかるように開発を行うことができる.

もちろん,古い環境にどれだけ,そして,いつまで対応させるのかはライブラリ作者の判断次第だし,あまりにも古い環境に対して対応が切れたとしても何も言えはしないだろう.ただ,このようにTravis CIでいちどきに複数バージョンの動作確認が取れるのであれば,「テスト環境が用意できない」から考慮から外すという後ろ向きな対応を取る必要は薄くなってきてる気がする.

個人的な目安としては「現行Debian stableの古さ」まで可能ならばサポートし,それより古いのは切るくらいだと良心も痛まないかなと思っている.別にdebian使いではないが.


適切にラップしてバージョン間ギャップを吸収する

こんなのHaskellじゃなくたってあたりまえの話なんだけど,一応,基本的と思われる方法を書いておく.と言いつつGHC拡張になっちゃうんだけど仕方ないしまあいいよね.

GHCのCプリプロセッサ拡張を利用し,MIN_VERSION_*マクロなどを使う.たとえば,Network.Socket.sCloseなどは,network >=2.4ではdeprecatedになっており,かわりにNetwork.Socket.closeを使うようにとなっている.このギャップを吸収するNetwork.Socket.Wrapperモジュールを書くとなると次のようになる.gracefulパッケージより抜粋.

{-# LANGUAGE CPP #-}
module Network.Socket.Wrapper
    ( close
    , module Network.Socket
    ) where

import qualified Network.Socket as NS
#if MIN_VERSION_network(2,4,0)
import Network.Socket hiding ( close )
#else
import Network.Socket hiding ( sClose )
#endif

-- | wrap close/sClose
close :: Socket -> IO ()
#if MIN_VERSION_network(2,4,0)
close = NS.close
#else
close = NS.sClose
#endif

ビルド環境のnetworkのバージョンを見て,マクロが適切なものを残してくれるようにラップしている.他のモジュールでNetwork.Socketが必要な箇所では,変わりにNetwork.Socket.Wrapperをimportし,networkのバージョンが古い環境であってもラップされたcloseを使えばよい.

他にも,たとえばbytestringで0.10から入ってきたfromStrict/toStrictなんかを使ってる程度であれば,bytestring-0.9以前でも同様にラップしてあげた上でfromStrict/toStrictはカンタンなので自前で提供してあげれば,bytestring >= 0.10に制約する必要は特に無くもっと広く依存バージョンを取れる.こういうの結構あったりする.


まとめ

とにかくみんなもっとパッケージングには気を遣おう.いくら言語が型で守られていると言っても,エコシステムまで型が守ってくれているわけではないので,エコシステムを守るのはいまのところ個々人の良識+知識に依存している.コーディング時に型によって浮いた分の労力を,わずかでも上手いパッケージングに割いていただきたい.

*1:だから基本気にくわないのだが,Web自体がコギタナくてキレイに抽象化できてないため仕方無い部分もある