モナディックなループ

しょーもないイディオムの話。モナディックなループを書くとき、だいたい3つくらい宗派があるように思う。

{-# LANGUAGE ViewPatterns #-}
module Loop where
import Control.Monad (when)
import Data.Function (fix)
import System.Environment (getArgs)

f :: IO ()
f = do
  (read -> n):_ <- getArgs
  flip fix (0 :: Int) $ \loop i ->
    when (i < n) $ do
      print i
      loop $ i + 1

g :: IO ()
g = do
  (read -> n):_ <- getArgs
  let loop i = when (i < n) $ do
        print i
        loop $ i + 1
  loop (0 :: Int)

h :: IO ()
h = do
  (read -> n):_ <- getArgs
  loop n (0 :: Int)
  where
    loop n i = when (i < n) $ do
      print i
      loop n $ i + 1

普通のpureなコードを書いているときは基本的にはwhereを使うのだけど、この例で言うnのようにbindした値を後で使いたいときは不便に感じる。会社のコードでは大きいコードはhで小さなループはg、たまにfのコードを見かける*1感じ。

個人的にはletの中身が長くなるとlet前後のつながりがわかりにくく感じるのでfixの方が好き。ただ引数が増えるとflip (flip fix 0) "foo"とかfix `flip` 0 `flip` "foo"とかもあんまり見やすくない。fixの良い感じのinfix operatorがあると良いのかもしれないけど、良い記号も浮かばない。

*1:Coreレベルではfもgも同じ。hだけ2引数の関数の再帰になる。

詳解bytestring

というタイトルでbytestringパッケージの包括的ガイドを書こうと思ったけど、まさにそんな内容のスライドをbytestringのBuilderを書いた人が公開していたのを見つけてしまったので、是非そちらを見て欲しい。

A guided tour through the bytestring library

以上、Haskell Advent Calender 2013 22日目の記事でした。


Read more

GREEのCUFPでのHaskell事例紹介を見た

社内Haskellチュートリアルのススメ | GREE Engineers' Blogという記事からたどって、GREEの中の人がCUFPでHaskellの事例紹介をした動画を見た。


CUFP 2013: Yasuaki Takebe: A Mobile Gaming Platform Case Study

スライドはCommon Pitfalls of Functional Programming and How to Avoid Them: A Mobile Gaming Platform Case Study (PDF)

GREEでは

  • 内製のKVSデータベースの管理
  • 画像サーバのフロントエンド

みたいな用途でHaskellを使っているらしい。

プレゼンは(タイトルではFunctional Programmingとあるけど、実際のところは)Haskellでよくある落とし穴を紹介して、それに対するworkaroundを紹介している。例として

  • 遅延評価によるスペースリーク
  • 非同期例外による競合状態
  • 外部ライブラリの変更に追従できなかったために起こる不具合

の3つが取り上げられている。

遅延評価によるスペースリーク

これはHaskellをreal worldで使うときの障壁としてよく取り上げられる。例としてこんなコードが取り上げられている。

modifyTVar' requestThreads $ \threads -> filter (tid /=) threads

modifyTVar'を使うことで[ThreadId]に対するfilterがstrictに適用されていることを期待しているのだけど、実際にはWHNFまでしか評価されないのでthunkが積み上がっていく。あるあるだと思う。

この問題に対するworkaroundとして、プレゼンではseqとlengthを使ってspine strictなリストにしている。spine strictにしたいだけなのにlengthをして長さを求めるのはイヤだ、本質的なこと以外はしたくない、と言う場合はparallelパッケージのControl.Seqが使える。

modifyTVar' requestThreads $ \threads ->
  filter (tid /=) threads `using` seqList r0

実際にどのように評価されるかはghciで:printすれば確認できる。

Prelude> :set -XBangPatterns 
Prelude> let !a = filter (/= 5) [1..10]
a :: [Integer]
Prelude> let !b = let xs = filter (/= 5) [1..10] in length xs `seq` xs
b :: [Integer]
Prelude> let !c = filter (/= 5) [1..10] `using` seqList r0
c :: [Integer]
Prelude> :print a
a = 1 : (_t2::[Integer])
Prelude> :print b
b = [1,2,3,4,6,7,8,9,10]
Prelude> :print c
c = [1,2,3,4,6,7,8,9,10]

ここら辺のテクニックはSimon Marlowの並列平行本にまとまっているのでおすすめ。

Parallel and Concurrent Programming in Haskell: Techniques for Multicore and Multithreaded Programming

Parallel and Concurrent Programming in Haskell: Techniques for Multicore and Multithreaded Programming

非同期例外による競合状態

次に取り上げられるのは、STMなqueueであるTQueueからタイムアウト付きでpopしたときに、稀にデータがなくなるという話。これは文字通り受け取ってHaskellは安全じゃないという話とは考えて欲しくないのだけど、確かに非同期例外は難しい。

実際の問題のコードはスライド中には出てこないのだけれど、たぶんこういうことだろう。

readRequest :: TQueue Request -> IO (Maybe Request)
readRequest q = timeout 10 $ atomically $ readTQueue q

ここで、Control.Timeout.timeoutはスレッドを立ち上げ、指定時間経過した後に元のスレッドに非同期例外を投げる。問題はatomically $ readTQueue qというSTMのトランザクションを抜けた直後にタイムアウトして非同期例外が投げられた場合である。この場合、全体ではreadTQueueで取り出した値ではなくNothingが返り、取り出された値は失われる。

スライドではこんなworkaroundが示される。

readRequest q = do
  mRequest <- timeout 10 $ atomically $ do
    request <- peekTQueue q
    return request
  case mRequest of
    Just _ -> atomically $ tryReadTQueue q
    Nothing -> return Nothing

このコードは意図したコードなのかちょっとわからない。readRequestが複数のスレッドから呼ばれる場合を考えると、スレッドAがタイムアウト前にpeekTQueueで要素があることを確認したあと、スレッドBがtryReadTQueueで要素を取り出してしまった場合、スレッドAではあるはずだった要素がなくなってNothingが返る。

ちゃんとしたtimeoutを実装するにはControl.Concurrent.STM.TVar.registerDelayが使える。

readRequest q = do
  timer <- registerDelay 10
  atomically $ asum
    [ Just <$> peekTQueue
    , Nothing <$ waitTimeout timer
    ]
  where
    waitTimeout = readTVar >=> check

非同期例外の取り扱いの難しさはHaskellの鬼門の一つで、これもまたSimon Marlowの並列並行本に詳しくまとまっているのでおすすめ。

外部ライブラリの変更に追従できなかったために起こる不具合

これは簡単にまとめると、http-conduitの仕様変更で挙動が変わったのだけどコンパイルは通り、それに気がつかず放置していたらスレッドが沢山できて性能劣化するバグとなってしまったという話。これはHaskellに限らず、外部ライブラリを使うあらゆるコードで発生しうる現象だと思う。

Tsuru Capitalでは外部ライブラリはgitで管理された自家製Haskell Platformみたいなものか、コードのリポジトリにチェックインして使うため、自動で依存ライブラリがアップデートされて挙動がおかしくなることがないようにしている。アップデートする際はおかしな挙動や性能劣化などが起きていないかテストやベンチマークの結果を確認してから反映する。

Haskellは型の表現力が高いとはいえ、コンパイルをすり抜けるバグはいくらでもあるので、テストやベンチマークはやはり欠かせない。

まとめ

Simon Marlowの並列並行本のステマみたいになったけど、実際良い本なのでHaskellを実務で使う人は是非読んでみると良いと思う。ちなみにこの本はAmazonで買わなくとも、オンラインで読める

今後もHaskellを採用する会社が増えると僕が食いっぱぐれたときに救われるかもしれないのでみんなHaskellを使おう。

Sensuについて

最近Sensuというモニタリングフレームワークを試している。見ての通り公式はオサレで今時な感じで、Nagiosのような古くささやZabbixのようなエンタープライズ臭はない。

Sensuの特徴は何かと考えると、こんな感じのことが浮かぶ。

  • 監視とメトリクス収集を一つの仕組みで行える。
    • ただし収集したデータの可視化はしない。
  • 設定がlightweight
    • 所定のディレクトリにJSONファイルをつっこんでおくと勝手にdeep mergeされるのでinclude指定とかいらないし、ポチポチやってデータベースに設定値を入れる必要もない。
      • キーがかぶったときにどうなるかとかは知らないので、設定がシンプルとは言わない。
      • JSONなのでコメントは書けない。コメントはChefのレシピ側に書こうという発想。
    • クライアント一覧は自動的に作られるので自分で能動的に登録する必要もない。
  • Chefで設定するためのcookbookが整備されてる。
    • 適当にattributesを設定して適用すればサーバを作れる。
  • Nagiosプラグインがそのまま使えるし、sensu-community-pluginsにもたくさんプラグインがある。
  • まともなAPIがついてる

サーバはRedisとRabbitMQを使っていてセットアップが大変かと思っていたけど、Chefレシピがちゃんと動いたのであっという間だった。

フロントエンドを外部に任せてモニタリングに集中したのは、餅は餅屋にという意味で良い決断だと思う。その分ユーザが設定すべきことは増えるけどChefとの連携とlightweightな設定で有る程度緩和されてる。

せっかく監視とメトリクス収集を同じ仕組みでできるようにしたのに、Nagios pluginと同じプロトコルを採用したために一つのチェック実行で両方が終わるようにならないのはもったいない気もする。

Graphiteにデータを送る際にgraphite mutatorを使うと、あっという間にサーバのCPUが100%になるので気をつけよう。extensionを使って書き直したものをPRしたので取り込まれればこっちを使った方が良いと思う。

それと、コミュニティの規模はまだだいぶ小さい。#sensuは140人弱くらいで発言数はかなり少ない。

persistentでEntityのJSONフォーマットを変更するには

Yesod界隈でよく使われているpersistentは、v1.2.0になったタイミングで、Entity a全体に対するToJSONとFromJSONクラスのインスタンスを提供するようになった。

このインスタンスのJSONフォーマットがちょっと変わっていて、

{"key": 1, "value": {"name": "Taro", "age": 20}}

という形式を想定している。

別のフォーマットを使うようにしたかったら、Haskellで同じ型に別のインスタンスを実装したいときにおなじみのnewtype wrapperを使うことになる。例えばよく使われる

{"id": 1, "name": "Taro", "age": 20}

にするならこんな感じ。

import Control.Lens ((&), (%~))
import Control.Lens.Aeson (_Object)
import Data.Aeson
import Database.Persist

newtype SaneJSON a = SaneJSON (Entity a) deriving Show

instance ToJSON a => ToJSON (SaneJSON a) where
    toJSON (SaneJSON (Entity key val)) =
        toJSON val & _Object %~ HM.insert "id" (toJSON key)

毎度これを定義するのは面倒な上に、うっかりtoJSONする前にSaneJSONでくるむのを忘れると、insaneなJSONが出てきてしまう。

やだなーと思っていた折、githubにissueが登録されたので、Entity aに対するToJSON/FromJSONをなくす方向に誘導してみた。無事#181がmasterにmergeされたので、次のリリースからは

import Database.Persist
import Database.Persist.TH

settings :: MkPersistSettings
settings = sqlSettings
  { mpsEntityJSON = EntityJSON
    { entityToJSON = 'entityIdToJSON
    , entityFromJSON = 'entityIdFromJSON
    }
  }

share [mkPersist settings] [persistLowerCase|
User json
  name Text
  age Int
  deriving Show Eq
|]

みたいな感じで、MkPersistSettingsのmpsEntityJSONに自分の好きなtoJSON/parseJSONをquoteして渡してやることで差し替えられるようになった。entityIdToJSON/entityIdFromJSONは新しくpersistentパッケージで定義されたpredefinedなtoJSON/parseJSON実装の一つ。stage restrictionさえ避ければ、自分の好きな実装に入れ替えられる。

HaskellでWebAppの開発に必要なN個のこと

あるプログラミング言語で実際にWebAppを開発できるようになるまで、何が必要だろうか。言語仕様の習得は終えているとしよう。おそらく、最低限以下のような知識が必要だと思われる。とりあえずHaskellについて知っていることを書いた。 ← ここまで引用。

パッケージマネージャ

Cabal 1.18を使おう。以上。

アプリケーションサーバ

WSGIとかRackとかの流れでHaskellでもwebアプリのサーバインタフェースを統一化する動きがいくつかあった。その中で一番市民権を得たのはwaiと呼ばれるものだ。

ただ、残念なことにHaskell界でここ数年ずっと続いているI/Oストリーミングライブラリ戦争の決着がついていないため、統一化の状況は思わしくない。waiはconduitというライブラリに依存しているが、フレームワークによっては別のI/Oストリーミングライブラリを基盤にしている。

現状の3大フレームワークで次のような状況である。

  • Yesod
    • インタフェースはwai。標準的なwebサーバはwarp
    • I/Oはconduitを使う。
  • Snap
    • インタフェースは独自のものでsnap-serverを使う。
    • I/Oはio-streamsを使う。
      • 追記: I/Oはまだenumeratorを使っていて、今io-streamsに移行準備中だそうです。*1
  • Happstack
    • インタフェースは独自のものでhappstack-serverを使う。
    • happstack-waiというライブラリが有るみたいだけど動くのかは知らないし、Hackageにもあがってない。
    • I/OはlazyByteStringを使っているみたい。将来はpipesに移行するらしい。

要するにほとんど統一化されておらず、各々が好きなインタフェースを実装して使っていると考えてよい。個人的にはこの状況はもうしばらくの間、続くのではないかと思っている。

リクエストパラメータの処理・ルーティング

基本的にフレームワークについてくるものを使う。

データベース

Yesodやwai陣営は基本的にpersitentSQL方面で拡張したesqueletoを使う。

Snapではフレームワーク自体にはデータベースアクセスのサポートはないが、アドオン(snaplet)の形式でいくつかサポートされているようで、persistentを使うこともできるらしい。

HappstackはなんとRDBMSではなくてacid-stateを押しているので、フレームワークとしてのサポートは特にないのではないかと想像できる。

いずれのフレームワークを使っていても、もっとローレベルなライブラリを直接叩く人もいるのではないかと思う。

ビューのレンダリング

たいていのケースでフレームワークに付属しているものを使う。MustacheのHaskell実装であるhustacheを使う人もいるかもしれない。

HTTPクライアント

Haskell PlatformにHTTPパッケージが入っているけどAPIがひどいので使っている人は少なそうな印象。

この分野もI/Oストリーミングライブラリの影響を多分に受けるのでフレームワークによっておすすめが変わると思う。

あたりがメジャーどころ。

テストフレームワーク

QuickCheckとHUnitを書くことになるが、それを補助するライブラリがいくつかある。

前2つはQuickCheckテストをまとめてくれるもの。並列実行などもサポートしている。

hspecはrspecみたいな感じにテストを書けるらしいけど使ったことがない。

doctestはテストをコメントとして書くことで、ドキュメントにもなるというもの。

hspecとdoctestはHaskellの単体テスト最前線 - あどけない話を参照されたい。

Webアプリケーションフレームワーク

上に出てきたとおり。YesodとSnap、Happstackが三強。

比較はWeb/Comparison of Happstack, Snap and Yesod - HaskellWikiにまとまっているみたい。

とりあえずフォーマットは埋めたので、思いつきベースで追記するかもしれない。何か言いたいことがある人は追記するのでコメントで教えて欲しい。

*1:fujimuraさん、ありがとうございます。

2013年8月現在のHaskell開発環境

以前はHaskell Platformを使っていたのだけど、Cabalの依存関係ソルバがあまり賢くなかったこともあり、いわゆるdependency hellに陥ることが多かった。それからというものHaskell Platformを使わない環境を使っている。OS Xでは公式のインストーラやらHomebrewやMac Portsなど幾つかの方法が用意されているが、こんな方法もあるよということで紹介してみよう。

GHCのインストール

GHCのリリース版は自分でビルドしない。時間が掛かるから。インストーラは使わずにtarball版を使う。これはインストール先を自分で指定したいため。

tar zxvf ~/Downloads/ghc-7.6.3-x86_64-apple-darwin.tar.bz2
cd ghc-7.6.3/
./configure --prefix=/usr/local/ghc/ghc-7.6.3
make install

こんな感じで/usr/local/ghc/ghc-x.y.zにインストールする。今はこんな感じ。7.2.xがないのに特に深い意味はない。

[maoe@maoe:~]
% ls /usr/local/ghc/   
ghc-7.0.4 ghc-7.4.2 ghc-7.6.3 ghc-HEAD

HEADが欲しい場合は自分でビルドする。使いたくなったタイミングで更新してる。インストール先は/usr/local/ghc/ghc-HEAD。

Haskell Platformを使わない利点は、バージョンが固定されるパッケージが少ないので依存関係でこけることが減ること。逆に欠点はプロジェクトごとに必要なパッケージをビルド・インストールするのでインストール時間と容量を食うこと。気になる人はHaskell Platformを同じディレクトリにインストールすればいいのかもしれないが、試したことはない。

GHCの切り替え

基本的にはshellから単にPATHを切り替えるようにしている。会社で使われていたスクリプトを少しだけ弄って使っている*1

maoe/dotfilesにあるprepathpreghcをbashrcやzshrc的なものにコピーすれば使えると思う。使い方は簡単で、

preghc

とだけ打てば最新のリリース版に、

preghc 7.4
preghc HEAD

などで特定バージョンに切り替えられる。

エディタからGHCを呼びたい場合はシェルスクリプトの関数を呼ぶとトラブルの元になりかねないのでバージョン固定でPATHを指定した方がよいかもしれない。

cabal sandbox

ライブラリを入れる際にcabal installするのはやめよう。cabal install yesod-platformなんてした日にはきっとあなたの.cabalは悲惨なことになる。これまでcabal-devを使っていたけど、最近cabalコマンド自体にsandbox機能やsandboxからGHCiを起動する機能が実装されたので、githubのmasterブランチを取ってきてビルドして使い始めた。これがなかなか快適なのでおすすめ。

そのうちHackageに上がると思うけど先取りして紹介しよう。自分のプロジェクトのルートディレクトリにいって

cabal sandbox init

とすれば.cabal-sandboxディレクトリが作られる。後はいつも通りにコマンドを打てば依存しているパッケージはすべて.cabal-sandbox配下にインストールされ、グローバルあるいはユーザレベルのパッケージデータベースを壊すことはなくなる。

cabal install --only-dependencies # .cabal-sandboxに依存ライブラリをインストール
cabal configure
cabal build
cabal test
cabal bench

GHCiを立ち上げたかったら

cabal repl

とする。あなたのプロジェクトがlibraryであれば上のコマンドでライブラリのモジュールがロードされる。

例えばtest suiteを弄っているときにGHCiが欲しくなったとする。Cabalファイルに

test-suite regression-tests
  hs-source-dirs: tests
  -- ...

と書いてあったとすると

cabal repl regression-tests

とすればライブラリの代わりにそのtest suiteのモジュールがロードされる。

それから、もしあなたのプロジェクトが公開されていないライブラリに依存しているなら、cabal-devと同じようにadd-sourceコマンドが使える。ただし、cabal-devとは異なり*2tarballではなくディレクトリを指定する。

cabal sandbox add-source path/to/your/library/directory

*1:会社で使われているスクリプトもコレに差し替えた

*2:cabal-devではtarballもディレクトリでもOKだそうです。 @notogawa さんありがとうございます。