A Tour of Go in Haskellを作ったのと、GoとHaskellの比較
(この記事は Haskell (その2) Advent Calendar 2017 - Qiita の3日目の記事です)
A Tour of Go in Haskell というサイトを作りました。 英語版(開発中) もあります。
サイトのソースは GitHub で管理しています。
概要
今流行りの Go 言語は並行並列処理が簡単に書けることを1つの売りにしているようです。 Haskell も Go と同じく軽量スレッドやチャネルを利用することができ、並行並列が得意な言語の1つです。 そこで、A Tour of Go という Go の有名なチュートリアルの 並行性 の章を Haskell で書いてみることで、 Haskell と Go を並行並列処理の記述という観点で比べてみよう、というのが A Tour of Go in Haskell になります。
Go ユーザや Haskell に慣れ親しんでいない人が見ても雰囲気がつかめるコードを目標にしたので、あまり難しい書き方はしないようにしています。
たとえば、Haskeller にはよく知られているユーティリティ関数を使わなかったり、ポイントフリーにできるところをポイントフリーにしていなかったり、関数はあまり純粋にせず IO
にしていたりします。
また、Go との比較を重視したので、もっぱら async や stm を使い、EvalモナドやParモナドは説明していません。
Haskell に詳しい方はそのあたりご了承下さい。
なお、コードのレファレンス性のために import
はかなり明示的に書いています。
その場のコーディング規約にもよると思いますが本来はもっとワイルドカードな import
ができます。
サイトの作りとしては完全に静的サイトになっていて、残念ながら現状は本家のようにコードを実行する機能はありません。 一応作る気はあり、静的サイトホスティングとして Firebase Hosting を使っているのもいずれ GCE かなにかでコード実行サーバを立てたいというのがあるためです。 そういえば最近は他にも静的サイトホスティングの選択肢はあるようですが、Firebase Hosting は独自ドメインも使えるし SSL 証明書もついてくるし HTTP/2 で通信されるし、 結構便利な感じでした。
あと、Haskell は言語拡張を多用することに毀誉褒貶があったりしますが、A Tour of Go in Haskell は言語拡張を1つも使わずに書けました。 並行並列が Haskell (GHC) に早い段階から入っていた(枯れた)機能なことが分かります。
比較
いくつかの観点から、比較と言えるほど厳密ではないですが、感触の違いを述べます。 今回はパフォーマンスについては見ていません。
軽量スレッド
軽量スレッドというのはOSのスレッドとは違って言語のランタイムが管理するスレッド(でいいはず)。 軽量というだけあって軽くてたくさん(数千個とか)作ることができます。 どれくらい軽いかというと Haskell の場合はスレッド1つにつき18ワード+1K (pdf) しかメモリを消費しない(slackで教えていただきました m(_ _)m)。 よく並行処理がすごいと言われる Erlang の軽量プロセス(軽量スレッドとは違うのか?)が309ワード(リンク) なことを考えれば、Haskell はなかなかすごいと言えそうです。 Go の軽量スレッドのサイズは分かりませんでしたが、おそらく Haskell か Erlang くらいでしょう。
さて軽量スレッドについては Go と Haskell で使い勝手はほとんど変わらないと思います。
Go では go
とやると軽量スレッドになるところを、Haskell では async
とやると軽量スレッドになるというだけです。
(forkIO
でもいいが async
の方がよく使われていると思います)。
違いは go
が専用の構文なのに対して async
は普通の関数というところです。
あとたぶん goroutine というのは軽量スレッドという言葉を言い換えたものだと思うのですが、 別の名前をつけることで言語固有のすごい機能として見せるのはうまい手だなと思いました。
チャネル
チャネルはスレッド間で値をやり取りするのに使う First-In-First-Out キューという理解。
Go はかなりチャネルに特化して構文や機能を用意していて、チャネルに関しては Haskell より使いやすいと言えそうです。 チャネルに値を書き込む/取り出すための演算子のような構文があったり、for式でチャネルの要素をイテレーションできるなど、 ストレスフリーにチャネルを取り扱えるように考えられています。
Haskell でも真似できる部分は結構あり、例えば getChanContents
関数を使えば Go の range
句のようなことができます(Range and Close - A Tour of Go in Haskell)。
加えて Haskell は演算子が定義できるので工夫すれば Go のような書き味を実現できるかもしれません。
ただ、それはおそらくやりすぎなのでやらない方が良いでしょう。
A Tour of Go in Haskell では取り扱わなかったこととして、Haskell のより高速なチャネルの話題があります。 Hackage には Haskell 標準のチャネルより高性能な実装を目指したパッケージがいくつかあります。 もし標準チャネルの性能に不満があるなら検討してみるとよいかもしれません。
- unagi-chan: Fast concurrent queues with a Chan-like API, and more
- kazura-queue: Fast concurrent queues much inspired by unagi-chan (作者によるQiitaでの解説)
Mutex, STM
チャネルは便利ですが、スレッド間で変数を共有する方法としてそれだけで完結するというわけではありません。 たとえば int の変数を複数のスレッドで共有してコンフリクトを起こさないようにそれぞれ読み書きをするような場合です。
Go はそのような場合のために古典的な Mutex を用意しているようです。 しかし Mutex を直接的に使うことは変数の書き換えの前後でロック処理およびアンロック処理が明示的に必要で、 シンタクティックノイズなだけでなくデッドロックを生みやすくなってしまいます。 (これは私の憶測ですが、Goの設計はユーザにチャネル以外の共有変数を禁止しようとしているように見えます)。
Haskell ではそのような場合や、複数の共有変数が絡むようなより複雑な場合のために STM(ソフトウェア・トランザクショナル・メモリ)が用意されています。 STM では一連の不可分な共有変数の操作を「トランザクション」という単位でまとめることができます。 トランザクションの実行中に、利用したい共有変数の1つが他のスレッドによってすでに使われていることが分かったら、トランザクションは実行前の状態までロールバックし、変数がフリーになるのを監視します。 いけそうになったらトランザクションはリトライされ、ロールバックせず完遂できるまで繰り返します。
STM を使うと複雑な並行処理もうまく記述できることがあります。 まあ STM にも弱点はあり、ふつうのコードとの組み合わせに注意が必要なこと、ロールバック・リトライの回数やタイミングを人間が完全に予測することはほとんど不可能なことなどがあります。 もしトランザクション中にミサイルを発射する処理が入っていた場合、トランザクションが間違いでロールバックされることになってもミサイルの発射はロールバックできません。 そこまでいかなくても、トランザクション中に何かのインクリメント処理が入り込んでいて実行環境依存でリトライ回数が激増して int の最大値を超える、といったことは十分考えられます。 「ソフトウェア」とわざわざ付けているだけあって元は電子回路上のアルゴリズムなので、任意の処理が差し込めるようにはできていないのです。 Haskell の場合、STM は STM モナドとして IO モナドと分離することでこの問題を完全に回避しています。 もしトランザクション中に副作用のある処理を入れてしまったとしても、実行時ではなくコンパイル時にエラーになります。 IOに型が付くという Haskell の特徴はこういったところでもメリットを生んでいます。
Haskell の STM について詳しく知るには O'Reilly Japan - Haskellによる並列・並行プログラミング が最も良いと思います。 よく言われる「STMは遅いのか?」という話題も取り扱われています。
さて、Mutex のページは Mutex を使う A Tour of Go と STM を使う in Haskell で顕著な違いがある部分です。 ぜひ2つのコードを見比べてほしいと思います。
- sync.Mutex - A Tour of Go
- sync.Mutex - A Tour of Go in Haskell (
atomically
関数が STM のトランザクションを作ります)
(補足: GoにもSTMライブラリはあるようですが更新が止まっているようです GitHub - lukechampine/stm: Software Transactional Memory in Go)。
やり残したこと
- 本家のようにコード実行機能をつけたい。
- hint-server を使えば、Haskell コードを渡して動的ロードするサーバーが作れそう。
- あるいは直接 ghc コマンドを叩く。
- この部分は静的サイトにできないのでサーバ代がかかる……。(たぶんGCE Managed Instance Groupを使う←Twitterで教えていただきました m(_ _)m)
- 英語版の完成。
- CSSの改善。
おわりに
Go の軽量スレッドやチャネルの構文はとてもよく設計されていて、特にチャネルに的を絞って専用の構文をたくさん用意する設計選択にはすごいセンスを感じました。 ただ、チャネル以外の共有変数のサポートが薄いというのは、並行並列処理に関してちょっと強すぎる仮定な気もしました。 しかしもしかすると、Go の想定する適応領域(システムプログラミング?)ではスレッドとチャネル以外の並行並列ツールはほとんど必要ないのかもしれません。 そうだとすると Go の設計選択のセンスはやはりすごいということになる。
O'Reilly Japan - Haskellによる並列・並行プログラミング から少し長いですが引用します。
スレッドとロックは、並列画像処理から並行ウェブサーバまで、必要なものをすべて記述できるだけの表現力を有しています。 このような単一の汎用的なAPIが存在するのはたしかに利点ではあります。 しかしながら、並列・並行ソフトウェアの作成を容易にしたいのであれば、 異なる問題に対しては異なるツールを用いるという考えが、やはり重要になってきます。 一本のナイフではうまく切れないのです。
私個人としては1つのツールに寄り掛かるのは恐いので状況に応じたツール選択ができるようになっておきたいという思いがあります。
「Haskellによる並列・並行プログラミング」は A Tour of Go in Haskell で利用した async や stm だけでなく、純粋な関数での並列性、メッセージパッシング、そもそも並列と並行の違いとは、といった広範な話題を扱っていて、 Haskell に限らず並列・並行をやる人にとてもおすすめの本です。 A Tour of Go in Haskell を作るにあたっても大いに参考にしました。
GoのTickerみたいなやつをHaskellで作った
作った。
GoのTickerはとてもシンプルな関数で、 指定した周期でチャネルに値を送るスレッドを生成する。 一定間隔で何かの処理を行いたいときに利用する。
今回作ったHaskell版もだいたい同じようなものを提供する。
実際のところパッケージにするには小さすぎる気もするけど、 Haskellパッケージ製作の練習をしたかったのでHackageのアカウントを取ってHackageに上げた。 練習のため、HaddockによるAPIドキュメントおよびhspecとdoctestによるテストも書いてある。 あとは何かCIを導入してGithubのページにCIのステータスを表示するようなやつもやってみたい。
小さいわりにHaskellパッケージに求められる要素は結構入っているので、 Haskellパッケージ作ったことがない人が作り方の参考にすることができるかもしれない。
ちなみに言語拡張は一つも使っていない。
パッケージの説明
(Haddock を結構まじめに書いたのでこちらも参照されたい)
現在は2つの関数を提供している。
newTicker :: Int -- ^ ticker rate by micro sec -> IO (Chan (), IO ()) -- ^ ticker channel and ticker stopper withTicker :: Int -- ^ ticker rate by micro sec -> (Chan () -> IO a) -- ^ handler function -> IO a -- ^ result of handler
newTicker
のインターフェースはGoの time.NewTicker
に近づけている。
Haskellのチャネルは getChanContents
を使うとリストに化けさせることができる。
(Haskellのリストは遅延リストなのでこれが可能)。
「forループでチャネルの受信をする」というGoっぽい書き方もできる。
import Control.Concurrent.Ticker (newTicker) import Control.Concurrent.Chan (getChanContents) import Control.Monad (forM_) import System.Timeout (timeout) main :: IO () main = do (chan, cancelTicker) <- newTicker $ 10^3 * 100 chanStream <- getChanContents chan _ <- timeout (10^3 * 350) $ forM_ chanStream $ \_ -> do putStr "Tick!" -- Tick!Tick!Tick! cancelTicker
newTicker
はチャネルとともにTickerを止める関数を返す。
Tickerの受信者がいなくなったらこれを呼ばないとリークになる(たぶんGoも同じ)。
もう一つの関数 withTicker
はハンドラーが終わったら Ticker を停止するのでリークを気にする必要はない。
main :: IO () main = do withTicker (10^3 * 100) $ \chan -> do chanStream <- getChanContents chan _ <- timeout (10^3 * 350) $ forM_ chanStream $ \_ -> do putStr "Tick!" return ()
withTicker
の実装は超単純で newTicker
を bracket
でくるんでいるだけ。
withTicker microSec action = bracket (newTicker microSec) (\(_, cancelTicker) -> cancelTicker) (\(chan, _) -> action chan)
こういう必要な関数がぱっと出てくるので Haskell は好き。 ハンドラーが例外を投げたときでも Ticker は停止される。
既知の問題
Githubのissueにも書いたが、
newTicker
は「一定周期」の実現のためにスリープ関数だけを使っている。
当然 newTicker
のループの中にはスリープ関数以外の処理によって生じるオーバーヘッドがあり、
チャネルに値が積まれる速度は指定した時間よりわずかに長くなってしまうと思われる。
GoのTickerは現在時刻を毎ループで取得することで時間を厳守しているようだ。 でも現在時刻って NTP アクセスなどによって巻き戻ることがあると思うんだけど、 そこはどうしてるのだろう。
ともかくGoは詳しくないがHaskellでは getCPUTime
というCPU時間を取得する関数があり、
これを使えば巻き戻らない時間が取得できそうだ、
と最初は思ったのだが……。
試したところ getCPUTime
はどうやらスリープ中はカウントが止まってしまうようだった。
ループで何かを待つ場合、ふつうはビジーループを避けるために適当にスリープを入れると思うのだが、
スリープを入れるとめっちゃ時間が延びてしまった。
考えてみれば当たり前な気もするがCPUを使っていない時間はCPU時間として計測されないらしかった。
(おまけ)Hackageまわりの知見
- Hackageのアカウントは実名で取っている人が多いが実名である必要はない。
- .cabal ファイルの
description
はsynopsis
より長く書いておかないとアップロードのときに警告される(アップロードはできる)。 - Hackageにアップロードしたものは基本的に修正や削除はできない。
- 修正のためには新しいバージョンをアップロードする。
- ただし .cabal ファイルの軽微な修正ならバージョンを更新しなくても可能。
- HaddockでAPIに "Deprecated" の印をつけるにはHaskellのプラグマを用いる。
HaskellとJSON、そしてレコード型
HaskellのJSON周りについて、こうやるのがいいんじゃないかという私の現在のやり方を書きます。 題材としては、 Swagger Petstore に記されている REST API にリクエストを投げてレスポンスを取り出すというのをやります。 (Swagger ですが scaffold は使わず自分で HTTP クライアントライブラリを使います)。
基本方針は「出力は厳密に入力には寛容に」(出典失念) です。
もくじ
- JSONの前に: レコードのフィールドへのアクセス
- JSONの前に: レコードのデフォルト値
- Haskellのデータ型→JSON
- JSONデータを含むHTTPリクエスト
- レスポンスにおけるJSONの扱い
JSONの前に: レコードのフィールドへのアクセス
Haskellのレコード型って、フィールドのゲッターがふつうの関数なのでレコードから値を取り出そうとすると f3 $ f2 $ f1 record
みたいにフィールド名が左向きに繋がります。
そのくせレコードの更新構文は右向きだったりとか……。
そのあたりをいい感じにしてくれる lens というライブラリがあって (lens というと色々あるみたいですが以下これを単にlensと呼びます)、こんなことができます。
{-# LANGUAGE TemplateHaskell #-} import Control.Lens (makeLenses, (^.)) import Prelude hiding (id) -- idをフィールド名で使うため data User = User { _id :: Int, _name :: Name } data Name = Name { _firstName :: String, _familyName :: String } makeLenses ''User makeLenses ''Name main :: IO () main = do let user = User 1 (Name "Takiji" "Kobayashi") print $ user ^. id -- -> 1 print $ user ^. name . firstName -- -> "Takiji"
makeLenses
は TemplateHaskell によってコンパイル時に動く関数で、データ型の定義からいろいろなものを自動生成します。
すると (^.)
などの lens の演算子がデータ型に対して使えるようになり、フィールド名を右向きに繋げていけるようになります。
レコードの更新も同様に右方向でできます。
lens を紹介しているサイトでは大体この makeLenses
を紹介していると思います。
しかしレコード型をたくさん作っていると makeLenses
ではちょっと面倒になってきます。
なんでかと言うと Haskell のレコード型のフィールド名って別のレコード型と衝突するんですよね。
衝突を避けるためにフィールド名のプレフィクスとしてレコード名をつけたりします。
data User = User { _userId :: Int } data Pet = Pet { _petId :: Int } makeLenses ''User makeLenses ''Pet main :: IO () main = do print $ (User 1) ^. userId -- -> 1 print $ (Pet 1) ^. petId -- -> 1
そういう場合に便利なやつとして makeFields
というのが lens で提供されています。
data User = User { _userId :: Int } data Pet = Pet { _petId :: Int } makeFields ''User makeFields ''Pet main :: IO () main = do print $ (User 1) ^. id -- -> 1 print $ (Pet 1) ^. id -- -> 1
lens で利用するときにプレフィクスがいらなくなるわけですね。 この例だとまだありがたみが薄いですがレコードが増えてきてレコード名が長くなってくると大変便利です。 (というかないと破綻する)。
makeLenses
を紹介しているサイトが多いですが makeFields
便利なので使いましょう。
makeFields
でも不満がある場合は、lensライブラリにはフィールド名の扱いをより自由にカスタマイズする方法も提供されているみたいなのでそれを使うといいでしょう。
あと今回、lens だけでどうにかする方法を紹介しましたが、拡張可能レコードを使うのが今風でより良いかもしれません。 拡張可能レコードについては haskell/extensible-record.md at master · lotz84/haskell · GitHub がよくまとまっていると思います。
JSONの前に: レコードのデフォルト値
我々は怠惰なので複雑なレコード値を構築するのが面倒です。 レコード型にデフォルト値があって必要なフィールドだけを変更できると便利なことがあります。
そんなとき便利なのが data-default です。
Swagger Petstore の POST /store/order の入力値をこれを使って表現してみます。
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE DeriveGeneric #-} import GHC.Generics (Generic) import qualified Data.Text as T import Data.Default (Default(..)) instance Default Bool where def = False instance Default T.Text where def = "" data PostOrder = PostOrder { _postOrderId :: Int , _postOrderPetId :: Int , _postOrderQuantity :: Int , _postOrderShipDate :: T.Text , _postOrderStatus :: T.Text , _postOrderComplete :: Bool } deriving (Show, Generic) instance Default PostOrder main :: IO () main = print (def :: PostOrder) -- > PostOrder {_postOrderId = 0, _postOrderPetId = 0, _postOrderQuantity = 0, _postOrderShipDate = "", _postOrderStatus = "", _postOrderComplete = False}
型クラス Default
の関数 def
がその型のデフォルト値を意味します。
Int
などの基本的な型にはすでにインスタンスが用意されていますが、
- デフォルト値を定めづらい型:
Bool
など - 標準でないライブラリの型:
Text
など - 自作の型
についてはユーザがインスタンスを定義する必要があります。
def
は2つの方法で定義できます。
言語拡張 DeriveGeneric
は型についてある種のメタデータを生成することを可能にします。
def
はこのメタデータがあれば自動的に導出できるようになっているので、def
について言及しなくても PostOrder
が Default
のインスタンスであると示すだけで def
の内容が用意されます。
Haskellのデータ型→JSON
Haskell の JSON ライブラリといえば aeson ですが、今回はそれに加えて aeson-casing というのも使います。
PostOrder
の JSON エンコーディングを記述します。
(既に書いた分は省略)。
import qualified Data.Aeson as JSON import qualified Data.Aeson.Types as JSON import qualified Data.Aeson.Casing as JSON import qualified Data.Text as T import qualified Data.ByteString.Lazy as LBS instance JSON.ToJSON PostOrder where toJSON = JSON.genericToJSON JSON.defaultOptions { JSON.fieldLabelModifier = JSON.snakeCase . drop (T.length "_postOrder") } main :: IO () main = LBS.putStr $ JSON.encode $ (def :: PostOrder) -- > {"status":"","quantity":0,"pet_id":0,"id":0,"ship_date":"","complete":false}
aeson でも Generic
による自動導出ができて、実は toJSON
の定義を書かなくても ToJSON
のインスタンスはできます。
ただ今回は lens のところでも書いたようにフィールド名のプレフィクスを消したいので、
自動導出をカスタマイズする方法を採っています。
T.length
の部分はちょっと格好悪いしメンテナンス性を下げるのでいい方法がないか模索中です。
JSONデータを含むHTTPリクエスト
ここまで書いてきたすべてのものを使って、いよいよ HTTP リクエストを投げていきます。
HTTP クライアントライブラリには req を使います。 このジャンルでは wreq が有名ですが、req もいいライブラリだと思います。 req の README には開発のモチベーションや wreq 等の他のライブラリとの比較があります。
POST /store/order へのリクエストは req ではこのように書きます。
(既に書いた分と makeFields
は省略)。
import Network.HTTP.Req ( req , MonadHttp(..) , GET(..), POST(..) , http, https , (/:) , NoReqBody(..), ReqBodyJson(..) , jsonResponse , responseBody ) import Control.Exception (throwIO) import Control.Lens ((&), (.~)) instance MonadHttp IO where handleHttpException = throwIO postOrder :: (MonadHttp m) => PostOrder -> m JSON.Value postOrder model = responseBody <$> req POST -- HTTPメソッド: POST (http "petstore.swagger.io" /: "v2" /: "store" /: "order") -- URL: http://.../store/order (ReqBodyJson model) -- リクエストボディ: (ToJSONのインスタンスである)PosrOrderを -- JSON としてセット jsonResponse -- レスポンスの型: レスポンスは JSON としてパース mempty -- ヘッダなど: 今回はないので mempty main :: IO () main = do let order = (def :: PostOrder) & petId .~ 3 & quantity .~ 1 postOrder order
req の提供する req
が HTTP リクエストを投げる関数です。
req
は使用環境に MonadHttp
を要求します。
今回はシンプルに IO
の中で req
を使いたいので IO
を MonadHttp
のインスタンスにしていますが、
このやり方だと Orphan Instance の警告が出ます。
実は1週間ほど前にこの警告を回避する方法が提供され、最新の req 0.4 で利用可能になっています (詳細)。 まだ LTS Haskell には入っていないのでここでは紹介しませんが、警告が気になる場合は見てみるといいでしょう。
レスポンスにおけるJSONの扱い
これまでリクエストについては型を定義して名前が衝突しないように配慮してデフォルト値も設定して……と手塩にかけてやってきましたが、 レスポンスについてはそこまでしっかりやらなくてもいいと自分は思っています。 基本方針にも書きましたが「出力は厳密に入力には寛容に」という原則に従います。
具体的には lens-aeson というライブラリを使います。 lens-aeson を使って、Swagger Petstore の GET /pet/{:id} のレスポンスを処理してみます。
import Control.Lens ((^?)) import Data.Aeson.Lens (key, _String) getPetById :: (MonadHttp m) => T.Text -> m JSON.Value getPetById _petId = responseBody <$> req GET (http "petstore.swagger.io" /: "v2" /: "pet" /: _petId) NoReqBody jsonResponse mempty main :: IO () main = do pet1 <- getPetById "1" let pet1Name = pet1 ^? key "name" . _String print pet1Name -- > Just "some name"
レスポンスの型を特に指示していないのに値が取り出せてしまいます。
と言っても型がない訳ではなく、実際は関数によって型が与えられていて、しっかり静的にチェックされます。
ここでの key
は引数が JSON のオブジェクトに相当する型であることを仮定します。
同様に _String
は取り出した値が文字列であると仮定します。
その仮定が外れるような入力なら(または該当するキーがなかったら)単に Nothing
が返ります。
出典を忘れたのですが「優れた言語は型付けのレベルを柔軟に変更できる」という意見を見たことがあります。 今回紹介したやり方は結構型を柔軟に取り扱っている感じじゃないでしょうか。
Haskellは真面目なアプリケーション開発に向いている
↑の記事で(主題ではないと思うものの)Haskellの批判に結構な分量が割かれていて、その批判のなかに「ちょっと違うんじゃないかな」という点がいくつかあったので反論ぽいことを書きます。
"Haskell は真面目なアプリケーション開発には向いてない"について
これには多分いくつか事例を挙げればよくて、
- Facebook ではスパム等の攻撃と戦うためのシステムを Haskell で作っています。
Fighting spam with Haskell | Engineering Blog | Facebook Code | Facebook (この記事を書いている Simon Marlow 氏は Haskell および GHC の主要開発者)
- より「アプリケーション」な事例が必要なら、プレゼンツールの Prezi を挙げることができます。
- 日本での事例としては、ASAHIネットというプロバイダの認証サーバが Haskell で動いています。
-
もうひとつ日本での事例としては、Tsuru Capital という金融系の企業が(一部だけでなく)全面的に Haskell を利用しています。
このように Haskell は多方面の「真面目な」領域で用いられています。(特にプロバイダの認証サーバとか可用性への要求がやばそう)。
少なくとも "Haskell は真面目なアプリケーション開発に向いていない" とは言えないかなと思います。
~完~
各論への反論
これだけだとあれなので個別の批判点についても書きます。
ドキュメントがない?
Haskell の主要なライブラリホスティング場所である Hackage は、ライブラリをアップロードすると自動で Haddock という Javadoc 的な仕組みで API ドキュメントを生成して、ライブラリと一緒に公開します。その意味では Haskell のライブラリはほぼドキュメントが用意されているし、ドキュメントの形式にも統一感があって見やすいと言えます。
他の言語だとたまに20行くらいの Usage でドキュメントを済ませるようなやつとか、API ドキュメントらしきものがあっても引数と戻り値の型が書かれていないやつとか、引数と戻り値の型は書いてあるけど全部 Object 型のやつがあったりしますが、それらに比べると Haskell は最低限のドキュメントとそのアクセス経路が保証されている点でリードしています。
ただ事実としてドキュメントが十分に書かれていないライブラリがあることも確かです。しかし私の実感としては、ドキュメントが十分でないものの多くは未成熟なものや実験的なもののようです。基本的に Haskell のメジャーなライブラリは自動生成の API ドキュメントだけで済ませているということはあまりないと思います。
これは Haskell に限らないことですが、ライブラリのドキュメントが十分でないというのはライブラリが未成熟なサインのひとつです。実用する予定なら未成熟なライブラリをそもそも選ばないようにすべきでしょう。
(まあごく稀に有名なライブラリでもドキュメントが整っていないのがあったりはします。ekmettさんはもう少しlens以外のライブラリのドキュメントも書いてほしい)
あと有名だからとか話題になっているからと言ってそのライブラリが production ready であるとか battle tested であることは意味しません。(これは私もよく間違えてしまうところです)。実用するライブラリを選ぶなら、新奇なものと主流のものは見分けましょう。これは何の言語でも必要なスキルだと思います。
言語とライブラリの進化が速すぎる?
おそらく2011~2013年くらいのことかなと思うのですが、この時期は確かにいろいろと新しいものが出てきました。当該記事で挙げられている Iteratee/Enumerator/Conduit というのはどれもストリーム処理ライブラリで、より良いやり方があるのではないかと盛んに議論されていました。(現在も模索を続けている人たちもいます)。
元記事で腑に落ちていない部分があって "Iteratee/Enumerator/Conduit などを何度も調査し直すことになって全然実装が進まなかった" という部分です。私の記憶によれば当時これらは乱立していて API も洗練されていなかったものの、どれもストリーム処理ライブラリとしての最低限の機能はすでに有していました。開発しているアプリケーションにストリーム処理ライブラリが必要と分かったなら、一度調査してどれかに決定すればいいだけのはずで、何度も調査し直したというのは決定のフェーズを先延ばしにし続けてしまったということなのではないかと思います。これは例えば React で作り始めたウェブアプリを Vue がいいと聞いて Vue で作り直したり Angular がいいと聞いてまた作り直したりという、似た技術が並立する状況でありがちな悪循環にはまってしまったのではないでしょうか。Haskell のライブラリの進化が速いというところには問題の原因はないと思います。
実際、今では Conduit が主流なものの「Iteratee を採用したプロジェクトをメンテしている」という話を私は2016年にも聞きました。おそらくどれを選んでも前に進めたのではないでしょうか。間違ったものを選んでしまうのではないかという不安はもっともですし私もよく分かります。しかし決定はどこかでしないといけないのです。"私はアプリケーションを作りたいだけなのに" と言うならなおさらです。
また元記事では "Yesod は新しいライブラリを大量に使うのでそれに引っ張られた" という趣旨のことも書かれていますが、これは Yesod の問題であって Haskell の問題とは言えません。
互換性壊しすぎ?
これは当時のことをあまりよく覚えておらず自信がないのですが Yesod はともかく Haskell (GHC) が互換性を壊すということがそんなに頻繁にあったでしょうか? 自分にはあまりそんな記憶がないので、Yesod の問題が Haskell の問題と混同されているのではないかという気がしています。(ここはちょっと自信がないです)。
Yesod 以外でもライブラリが互換性を壊すということは当時は時折起こっていたことは覚えています。しかし元記事には Haskell と Yesod にしか言及がないのでその辺りのことは分かりません。
元記事の Yesod が自身のビルド方法を大転換したのについていけないと感じたという指摘は……やはり Haskell の問題ではなく Yesod の問題です。
そういえば最近、 別の言語のウェブフレームワークでビルド方法がdeprecatedになった という話がありましたね。現行verが 2.5 で、2.4 まではドキュメントに書かれている主要なビルド方法だったと記憶しているのですが混乱とかなかったのでしょうか。(追記: id:xuwei さんより、activatorは実態的にsbtとほぼ同じもので、終息による影響はあまりないとのご指摘を頂きました。ありがとうございます。)
全体的に
ここまで見てきたように、元記事は全体的に Yesod の問題であるところを Haskell の問題であるように肥大化して書かれています。Ruby on Rails の問題を Ruby の問題だと言っているようで、ちょっと誠実な態度ではないなと思います。
完全に推測ですが、元記事の筆者の方は、もともと Haskell に強い意欲のあった方なのだろうと思います。意欲があったからこそ Yesod という大規模なフレームワークを導入したり、ストリーム処理ライブラリについてのコミュニティの議論に耳を傾けたりしたのでしょう。そんな方がどこかの時点で可愛さ余って憎さ百倍となって Haskell を嫌いになってしまったということは、Haskell を今も好きでやっている者としては本当に悲しく思います。
しかしいくら Haskell が憎かったとしても、過剰なネガティブキャンペーンを張ることはやめてほしいのです。嘘の情報でせっかくの初学者の意欲を減退させてしまうようなことは、それこそ避けたいと思っています。
私がこういう風に言うのは、私が C++ に挫折して Haskell を始めた人間だからです。人間には向き不向きというのがあって、Haskell の水が合う人もいれば C++ が最高という人もいる。それだけのことではないでしょうか。
で、どうすれば真面目なアプリケーションを作れるの?
自分のユースケースに合致していてかつ成熟したライブラリを選定する方法や、依存関係地獄から自由になるための方法は、最近の Haskell では充実してきています。それらを少し紹介していこうと思います。ただしひとつ念を押しておきたいのは、ライブラリの選定とかビルド環境の構築というのはプログラマの責任範囲であるということです。わざわざ実験的ライブラリを選択して後で後悔することになってもプログラマの自己責任のうちです。これは Haskell に限らずそうです。
依存関係解決、ビルド環境
最近の Haskell には Stackage というサービスおよび Stack というツールがあります。Stackage というのは「依存関係問題を起こさないコンパイラ・ライブラリバージョンのセット」のようなもので、ここからライブラリを落としてくると「コンパイラ-ライブラリ間」および「ライブラリ-ライブラリ間」の依存関係で悩まされることはまずありません。Stackage は基本的に Stack から使います。Stack は Stackage や Cabal や GHC をひとまとめにしていい感じに実行してくれるツールで、stack コマンドを知っておけば ghc コマンドや cabal コマンドを意識しないで Haskell 開発ができます。
冒頭に挙げた Facebook, ASAHIネット, Tsuru Capital の Haskell 事例は Stackage 以前からあるものなので、Stackage が真面目な Haskell 開発に必須かと言われればそうではないのですが、我々としては Stackage を使ってカジュアルに楽をしてやっていけばいいのでないかと思います。
ライブラリの選定
いい感じにメジャーで成熟した Haskell ライブラリを選ぶための指標としては State of the Haskell ecosystem というのがあります。これは Haskell が応用される各分野における Haskell 環境の成熟度と、そこに関連する主要なライブラリ等をリストアップしたものです。ライブラリを選ぶときなどは参考にするとよいでしょう。
また手前味噌ですが、当ブログでも Haskellライブラリ所感2016 というのを書いています。恐縮ながら合わせてご参照下さい。
別の観点としては「(Hackageだけでなく)Stackageに上がっているライブラリは最近までメンテされていることが期待できる」という大雑把な指標もあります。
人間的サポート
もし本当にビジネスとして真面目に Haskell を採用するなら、Haskell のコンサルティング会社と契約するのもひとつの手だと思います。有名な会社としては FP Complete や Well-Typed があります。Well-Typed はヨーロッパの会社ですが日本にもクライアントがいると彼らのサイトに書いてあります。
ふつうはそこまで本格的なサポートを受けるのはハードルが高いと思います。そういう場合は 日本Haskellユーザーグループ にコンタクトを取るのが良いでしょう。個人であれば、開放されている Slack チームに参加して質問チャンネルで何か質問を投げると暇な Haskeller が答えてくれます。実際に業務で Haskell を利用している人も何人か参加しています。(追記: 日本Haskellユーザーグループのsubredditが作られました。またStackOverflowなどの質問が当該Slackに流れるようになりました。 Slackに参加するのは気が引けるという人はこれらを利用するのも良いでしょう)
追記 最近のYesodについて
読み返すと「Yesodが悪い」みたいな書き方になっていたのでフォローしておくと、最近の Yesod はかなり良くなっています。年月を経て API が洗練されてきたとかだけでなく、Stackage の影響を大きく受けて初期構築や依存解決がとても楽になっています。というか、Yesod の開発チームが Yesod の構築を楽にするために Stack および Stackage を作っているという関係のようです。(このあたりの関係はあまり詳しくないです)。
超技術書典でGHCJSの本を出します
【告知】ニコニコ超会議2017と併催される超技術書典Day1(4/29)にて、GHCJSの入門的な本を出します。@y_taka_23 さんのスペースに委託する形になります。Haskell×JavaScriptに興味のある方はぜひA-04まで。 #技術書典 #超技術書典 pic.twitter.com/zUhiVc3CyX
— しょしー 超技術書典A-04 (@syocy) 2017年4月25日
出します。
GHCJSの仕組みの話はほとんどなくて、あくまで使い方とか周辺事情の話になります。
GHCJSって昔は導入すること自体が難しくて、他のHaskell系AltJSに水をあけられている感があったのですが、 最近は(時間とディスク容量はかかるけど)簡単に導入できるようになりました。 またFFIを直接書くのではなく、EDSLとしてJavaScript処理を記述する方法も提供されるようになったので、かなり使いやすくなったと思います。
超技術書典Day1(2017-04-29)で id:y_taka_23 さんのスペースに委託して置かせてもらうことになっていますので、是非。
Haskellライブラリ所感2016
(これは Haskell Advent Calendar 2016 の7日目の記事です)
今年使ったり調べたりした Haskell ライブラリを広く紹介していく企画です。 あくまで今年使ったものなので新しいものばかりではないです。 また記事の性質上、紹介するものが偏っていてもご容赦ください。
Hackage にはすごい数のライブラリが登録されていて、 頼もしいことですが目が回りそうにもなってしまいます。 この記事が Haskell のライブラリを調べる上での指針になったら幸いです。
なおこの企画と方向性が似ているものとして State of the Haskell ecosystem ( 2016年2月版 ) があります(英語)。 これは Haskell を取り巻く環境を知る上で非常によいドキュメントです。 ただ各ライブラリについては名前を挙げるだけにとどめられています。 この記事ではもう少しライブラリの内容に踏み込んだ説明をしていきます。
目次
文字列
bytestring,
blaze-builder,
bytestring-conversion,
base64-bytestring
text,
text-format,
text-icu
utf8-string
shakespeare
Haskell の標準の String
型は Char
リストのエイリアスなので、
文字列として使うには効率が十分でないことが多いです。
そのため効率的な文字列型として text や bytestring があります。
かといってリストのメリットもなくしたくないので text と bytestring は
Lazy 版を提供しています。
まとめると
String = [Char]
: 文字のリスト(UCS-4 (UTF-32?))Data.Text
: 文字列 (UTF-16)Data.Text.Lazy
: 文字列のリスト (UTF-16)Data.ByteString
: 文字列 (バイト列)Data.ByteString.Lazy
: 文字列のリスト (バイト列)
上記はあくまで内部的な話で、それぞれのインターフェースはほぼ同じように設計されていてどれも文字列として扱えます。
また OverloadedStrings
拡張によってどれも文字列リテラルから生成することができます。
上記に加えて、text と bytestring は Builder
というのも提供しています。
イミュータブル文字列は連結を繰り返すと効率が悪いので専用の型があるわけです。
Java の StringBuilder
のようなものだと考えておけばいいと思います。
blaze-builder は bytestring 用のビルダーでしたが bytestring 自体がビルダーを提供するようになったために現在は積極的に開発されていないようです。 互換性のために残されています。
bytestring-conversion は自作の型に独自にシリアライズ・デシリアライズを実装するのに便利です。 普通は JSON とか既存のものを使えばいいですがたまに独自のシリアライズが必要になることもあります。
base64-bytestring は bytestring に Base64 エンコーディングを提供します。
text-format は文字列のフォーマット出力(%d
ではなく {}
を使う方)を提供します。
text-icu は ICU ライブラリへのバインディングです。 自分は正規表現の機能のために使っていました。 Haskell はパーサコンビネータが優秀なので正規表現が必要になることはあまりないのですが、 今年はアプリのインターフェースとして正規表現をユーザに提供するという機会がありました。
utf8-string は String
や bytestring に UTF-8 のインターフェースを提供します。
ただし text が UTF-8 との相互変換を提供しているのであまり使う機会はない気もします
(自分は使っていません)。
shakespeare はテンプレートエンジンです。 自分は Haskell でヒアドキュメントもどきをやりたいためだけに使っていました。
ロガー
monad-logger
fast-logger
monad-log
katip
今 Haskell のロガーとして最も広く使われているのは monad-logger だと思います。 独立して動作させる以外にもお手元のモナドにログ機能を導入することもできます。
fast-logger は monad-logger や他のいくつかのロガーのバックエンドとして利用されているロガーです。 これを単体で使うことはほぼないと思われますが、 monad-logger で少し凝ったことをやろうとすると fast-logger の API も使うことになったりします。
monad-log は monad-logger に不足している機能 (JSON とかタイムスタンプとか) を追加し、さらに他のモナドとより組み合わせやすくしたもののようです。 これもバックエンドに fast-logger を使っています。 今年登場した新しいロガーですが数ヶ月前から更新が止まったままです……。
katip は珍しく fast-logger を使っていないロガーです。 速度というよりはリッチなログ出力を重視しているようで、 自分が知る限り今最も多機能なロガーです。 また他の言語のロガーとも使用感が近いです。 もちろん JSON とかタイムスタンプに対応しています。
このあたりの話は 別の記事 にも少し書きました。
例外
safe-exceptions
exceptions,
enclosed-exceptions
結論から言うと safe-exceptions を使っておけばいいです。
Haskell の標準の例外機構は非同期例外まわりが大変なのと
IO
に制限されるのが面倒というのがあって、
それをなんとかするために exceptions とか enclosed-exceptions
とかが開発されました。
現在は safe-exceptions が決定版と言えるでしょう (この話題については別の記事を書きました)。
なお safe-exceptions のチームは並行・並列処理に async (後述) パッケージを使うことを推奨しています。 そうすることで safe-exceptions のメリットを最大限享受できます。
ちなみに Haskell の例外は非検査例外ですが、
mtl (後述) の Control.Monad.Except
を使えば検査例外の模倣ができます。
ただし Haskell の例外機構とは独立したものなので注意が必要です。
データ
aeson,
yaml
ini
lens
xml-conduit,
xml-lens
time,
thyme
containers,
unordered-containers
aeson は Haskell のデファクトスタンダードな JSON ライブラリです。 JSON を扱う他のライブラリは大体これに依存しています。 データ型を書くと自動的に JSON シリアライザ・デシリアライザを用意してくれたりします。 すごい。
yaml は YAML ライブラリですが、独立したものというより aeson の YAML 向けインターフェースといった感じです。 aeson と同じ API で操作できます。
ini は INI ファイルの読み書きをサポートするライブラリです。
lens は Haskell の「入り組んだデータ構造の奥底の値を読んだり書いたりするのが大変」 という問題に対する救世主です。 しかし代償として多くの演算子を導入します。 まあ基本的な一部の演算子をインポートするだけでも十分メリットがあるというのが私の実感です。
xml-conduit は XML の読み書きをサポートするライブラリです。 名前に conduit とありますが conduit ライブラリ(後述)のことは特に意識しなくても使えます。 xml-lens は xml-conduit に lens のインターフェースを導入します。 XML はまさに lens が得意とする複雑なデータ構造(であることが多い)ので適任です。
time は日付時刻ライブラリのデファクトスタンダードです。
thyme は time をより高速で扱いやすくし、さらに lens インターフェースを追加したものらしいですがまだ触っていません。
なんで日付時刻に lens? と思うかもしれませんが、
time の ZonedDate
型とかは込み入ったデータ構造になっていて時刻を0秒にするとかが結構大変なんですよね。
そういう意味では lens インターフェースをつけたいという動機は確かにあります。
containers と unordered-containers は Map とか Set とかそういうデータ構造を提供するライブラリです。
データベース
relational-record
HDBC,
HDBC-mysql
persistent
persistent-relational-record
relational-record を使うと Haskell の言語内 DSL で SQL クエリを組み立てることができます。
SQL クエリに型をつけられるというのがまずすごいし、
do 構文内の DSL なので let
とか <-
でクエリ構築中に変数を作れるのが結構便利です。
さらにコンパイル時に DB にアクセスしてテーブル定義から Haskell のデータ型を作ってくれる機能もあります。
ただちょっと自分の場合は DSL に慣れるまで時間がかかりました
国内の某 ISP の一部サービスはこれで動いているらしい。 実装はファントムタイプのお化けみたいな感じなのでつまりお化けのお化け。
relational-record は実際に DB と通信する部分では HDBC を使うことが想定されています。 HDBC-mysql は長らく開発が止まっている状態だったのですが最近新しいメンテナが入ったみたいなのでよかった。
公平のために言っておくと、 RDB 関係では persistent が今一番有名なライブラリだと思います。 これは Yesod の陣営が作っているので一定の信頼が置けます。 自分はこちらは使ったことがないです。
あと最近 persistent-relational-record という persistent と relational-record を連携させるライブラリがリリースされました。
並行・並列
async,
lifted-async
monad-par
stm
async は Haskell の軽量スレッドを安全で扱いやすくしてくれます。
スレッドをタイムアウトさせるとか複数スレッドで一番先に結果を返したやつを採用して他を kill するとかも簡単にできる。
lifted-async は async が IO
ベースだというのが使いづらい場合に便利。
monad-par は並列計算文脈 Par モナドを導入します(こう書くと必殺技っぽい)。 たぶん async より決定的な計算をするのに向いています。
stm はソフトウェアトランザクショナルメモリのライブラリです。 軽量スレッド間での通信を安全にしたいときに便利。
なおこのあたりの話は Haskellによる並列・並行プログラミング というすばらしい本(日本語訳)にまとまっています。
通信
高レベルの HTTP クライアントとしては wreq が有名です(たぶん wget のオマージュでしょう)。 lens ベースの API を備えていて GET とか POST とかを簡単に投げることができます。 しかし残念ながら wreq は現在開発が止まっているようです。
そこで出てきたのが req です。 現在は HTTP クライアントとしてはこれを採用するのが良さそうです。
req が wreq と比べて足りないところは、 wreq が備えていた AWS の HTTP API リクエスト署名の機能がないことくらいです。 これについては req の作者も考えているようですが今のところありません。
まあ AWS については amazonka シリーズや aws などの専用のライブラリがあるのでそれほど困らないかもしれません。
ストリーム・リソース
Haskell のリストは最初から遅延ストリーム的にも使えるので、 なんでストリームライブラリが必要なんだと思われるかもしれません。 しかしながら、ストリームデータの生成元リソースを解放する必要があるとか、 ストリーム処理中に例外を吐いて失敗する可能性がある場合などは、 リストでは制御が難しくなってきます。
そのようなときには conduit のようなストリーム処理ライブラリが有用です。
resourcet はリソースを解放することを保証するモナドを提供し、 conduit とも連携できます。
テスト
hspec
HUnit
QuickCheck
smallcheck
tasty
HTF
doctest
silently
Haskell とユニットテストの関係について何か誤解している向きもいらっしゃるようですが実際のところ Haskell のユニットテスト環境は充実しています。
いわゆる普通のユニットテストを提供するのは hspec と HUnit です。 hspec は Ruby の rspec インスパイアドなテストライブラリです。 同様に HUnit は Java の JUnit インスパイアドなライブラリです。 (advanced features として JUnit 風の関数ではなく演算子でテストを書くこともできますがかえって分かりにくくなる気がします)。
Haskell で特に発展しているのが性質テスト (property-based testing) というテスト手法です。 入力値を規則に従って自動生成する手法で、 恣意的な入力値を想定してのテストが書けないため関数の性質に着目したテストを書くことになります。 代表的な QuickCheck は入力値をランダムに生成します。 smallcheck は小さいデータ構造については全数テストを生成できるという特徴があります。
上記のようないろいろな種類のテストをひとまとめにして実行できるインターフェースを提供するのが tasty や HTF です。 実は hspec もこの仲間で、 rspec 風のテスト API を提供するだけでなく HUnit や QuickCheck へのインターフェースを持っていたりします。
また Haskell にはユニットテストだけでなく Python 風の doctest もあります。 ドキュメントコメントにあるテストを実行して API ドキュメントが嘘を言っていないか確かめることができます。
テストの種類に依らず使えるユーティリティとして silently があります。 silently は標準出力への出力などをキャプチャすることを可能にします。 標準出力のテストを行いたいときなどに便利です。
モナド
mtl,
transformers
monad-skeleton
mtl と transformers はいくつかの汎用的なモナド変換子を提供するライブラリです。
mtl の方がより新しいです。
(
@masahiro_sakai さんから「mtlの方がより古い」とのご指摘を頂きました。
たしかに Haskell Wiki
にそのように書いてありました。
まず初期にmtl(mtl V1)があり、その後に新機能のtype familiesを用いたmtl-tfが開発されたものの
mtlと重複した部分が多く互換性もなかった。
そこで拡張なしの素のHaskell98で動くtransformersが開発され、
その上にmonads-fdとmonads-tfを別々に構築することで互換性の問題は解決された。
しかしその頃にはすでにmtlが広く使われていたため、transformersとmonads-fdからmtlを再構築(mtl V2)することになった、
という経緯のようです。ご指摘ありがとうござました。
)
よほど古い Haskell 資産があるのでなければ mtl を採用してしまってよいと思います。
monad-skeleton はモナドの自作をサポートするライブラリです。 自分でモナドを作るときというのはたいてい DSL が欲しいときなのですが、 これを使うといとも簡単に DSL が作れてしまいます。 自分はこれでログファイル解析クエリ用の DSL とかを作ってました。 作者による使い方の説明(日本語) もあります。
Webフレームワーク
yesod はフルスタックの Web フレームワークです。 かなり巨大で取っ付きづらかったのですが stack で雛形アプリケーションを生成できたり、 stackage によって依存問題が起きなくなったので始めやすくなりました。 (stack も stackage も yesod の人たちが作っているわけで、すごいことです)。
あ、年末のコミックマーケットというイベントで Yesod の本に寄稿することになりました。 うまくいけば本が出ますのでよろしくお願いします。
servant は比較的新しい Web フレームワークです。 型レベルでルーティングを記述できるというのが大きな特徴です。 自分が触っていたときは認証とか DB とかのサポートがあまりなかったのですが、 最近になってそのあたりも充実してきたみたいです。
パーサ
パーサコンビネータライブラリで有名なのは parsec とattoparsec で、 基本的な選択基準はエラーメッセージの分かりやすい方をとるか速度重視の方をとるかです。 parsec の方が比較的エラーメッセージが分かりやすいと言われています。 一方の attoparsec は速度重視です。
parsec はちょっと作りが古いしそれほど長いものをパースしないので私は attoparsec を使っているのですが、 parsec の後継的な megaparsec というライブラリが最近出てきました。
ユーティリティ
file-embed
filepath,
path
haddock
resource-pool
auto-update
zlib
file-embed はコンパイル時にファイルを読み込んで bytestring として展開します。 Haskell バイナリに小さい画像などを埋め込みたい場合などに使えるでしょう。
filepath と path は PC 上のパスを扱うためのライブラリです。 path の方がより高レイヤーです。
haddock は Haskell コードのドキュメントコメントから API ドキュメントを生成するツールです。
stack にサポートされていて stack haddock
コマンドでドキュメントを生成できます。
resource-pool は DB へのコネクションプールのようなものを作ることを可能にするライブラリです。
auto-update は現在時刻のような情報を何度も取得する場合などに便利なライブラリです。 たとえば現在時刻を秒間に何度も取得する可能性があるが時刻の精度は1秒単位でいい場合、 時刻の値をキャッシュしておいて実際の取得は1秒に1回で十分です。 auto-update は値を更新する別スレッドを立てて定期的に実行し、 値を要求されたときにはキャッシュを返す、 というような機能を提供します。
zlib は zlib へのバインディングです。 gzip 圧縮などを行いたい場合に使えます。 純粋関数のような API ですが失敗すると普通に例外を返すので注意が必要です。
所感
今年はいろいろなライブラリを触っていたような気もしますね。 やはり stackage によって依存関係に神経を使わずにライブラリを試せるようになったのが大きい。
一方で、見返すとあまり新しいものには触っていないですね (safe-exceptions と servant くらい?)。 これは見方を変えると Haskell の開発環境が安定してきたということなのかもしれません。
それでは 2017 年もよい Haskell ライフを!
HaskellのロガーKatipを試す
モチベ
Haskell の実行時ログ出力を行うライブラリは monad-logger が一番有名っぽい。 これは Yesod 陣営が開発しているから安心感があるし、バックエンドが fast-logger なので速度も信頼できる。 ただ (自分の調べ方が悪いのかもしれないが) ちょっと自分の用途には機能が足りなかった。 具体的には以下の機能:
- ログにタイムスタンプを付記したい。
- ロガーに名前をつけたい。
- ファイルサイズか日付でログローテーションしたい。
Katip という別のロガーライブラリは機能が豊富のようなので今回はそれを試してみる。
(この記事のHaskell環境: lts-6.23)
Katipのおそらく最小の構成
とりあえず動かしてみる。
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TemplateHaskell #-} module Main where import System.IO (stdout) import Control.Monad.Trans (lift) import Katip main :: IO () main = do le <- initLogEnv "MyApp" "prod" -- (1) hs <- mkHandleScribe ColorIfTerminal stdout InfoS V0 -- (2) let le' = registerScribe "stdout scribe" hs le -- (3) runKatipContextT le' () "main" $ do -- (4) $logTM InfoS "ログ出力" -- (5) $logTM DebugS "これは出力されない" -- (6) lift $ putStrLn "ふつうの出力" return () return () -- > [2016-10-30 08:00:31][MyApp.main][Info][kaz-mba-lb8o][23644][ThreadId 7][main:Main app/Main.hs:17:5] ログ出力 -- > ふつうの出力
- 新しい
LogEnv
を作る。ルートロガー名と実行環境名を指定している。 - 新しい
Scribe
を作る。Scribe
というのはログの出力先とログレベルなどの設定を組にしたもの。Katip はログレベルとして severity level (InfoとかErrorとか) だけでなくverbosity level (V0とかV1とか) も指定できる。 LogEnv
にScribe
を登録する。- 作成した
LogEnv
でロガーのモナドを走らせる。 - Info レベルでログを出力する。
$
がついているのはTemplateHaskell
でソースファイルの行番号などをログに出力するため。 - Debug レベルでログを出力する。しかし
LogEnv
が Info レベルのScribe
しか持っていないので出力されない。
ロガーのモナドの下でログ出力を行うという点以外はかなり普通のロガーという感じだ。
ファイルに出力したいときは stdout
の代わりにファイルハンドルを渡せばいいし、
出力先を増やしたいときは Scribe
を追加すればいい。
ログローテーション
Katip は組込みのログローテーションの仕組みは持っていないようだが、 Katip の開発元の Soostone 社は Katip と別に rotating-log というライブラリを公開している。 rotating-log の説明文によると、どうやらファイルサイズベースのローテーションを提供しているらしい。
それならログローテーションはこれでやればいいのかな……と思いきや、
This is so that it can be used to log non-textual streams such as binary serialized or compressed content.
とのことなのでテキストログのローテーションで使うものではないらしい。
一応 rotating-log を Scribe
にしてテキストログのローテーションを試してみたが (以下のコード)
参考にしない方がいいかもしれない。
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TemplateHaskell #-} module Main where import System.IO ( hSetBuffering, hIsTerminalDevice, stdout , BufferMode(..) , Handle ) import Control.Monad (when) import Control.Monad.Trans (lift) import Data.Monoid ((<>)) import qualified Data.Text.Encoding as T import qualified Data.Text.Lazy as LT import qualified Data.Text.Lazy.IO as LT import qualified Data.Text.Lazy.Builder as LT import qualified Data.Text.Lazy.Encoding as LT import Katip import Katip.Scribes.Handle (formatItem) import System.RotatingLog ( RotatingLog , mkRotatingLog, archiveFile , rotatedWrite, rotatedClose ) mkRotatingLogScribe :: RotatingLog -> Severity -> Verbosity -> IO Scribe mkRotatingLogScribe rl sev verb = return $ Scribe $ \i -> do when (permitItem sev i) $ rotatedWrite rl $ T.encodeUtf8 $ LT.toStrict $ LT.toLazyText $ (formatItem False verb i <> "\n") main :: IO () main = do le <- initLogEnv "MyApp" "prod" rl <- mkRotatingLog "log/myapp" 256 LineBuffering (archiveFile "log/archive/") s <- mkRotatingLogScribe rl InfoS V0 let le' = registerScribe "rotating log scribe" s le runKatipContextT le' () "main" $ do sequence_ $ replicate 10 $ $logTM InfoS "ログ出力" -- > $ tree log -- > log -- > ├── archive -- > │ ├── myapp_2016_10_30_17_07_51.852205.log -- > │ ├── myapp_2016_10_30_17_07_51.852508.log -- > │ ├── myapp_2016_10_30_17_07_51.85274.log -- > │ └── myapp_2016_10_30_17_07_51.852956.log -- > └── myapp.log
ちなみに fast-logger はサイズベースのローテーションの仕組みを持っている。 その一方で monad-logger は fast-logger を使って実装されているにも関わらずローテーションのインターフェースを持っていない。 推測だが、monad-logger を開発している Yesod 陣営が Keter を使っているためではないかと思う。 Keter についてはよく分かっていないのだが、Web アプリケーションのデプロイをサポートするものとのことで、 Yesod アプリのデプロイなどに使われているらしい。 ドキュメントによると Keter はログローテーションのAPIを持っているので、 Yesod 陣営にとっては monad-logger にローテーションを実装する必要性がないのではないだろうか。
まとめ
Katip はタイムスタンプや名前空間など、一般的なロガーライブラリが持っているような機能は十分備えている。
ログローテーションの仕組みはないが、これは logrotate
コマンドなどを使えばなんとかなるので大きい問題ではないと思う。
なお Katip はこの記事で紹介した以外にも次のような機能がある。
- JSON形式データの出力
- 他のモナドへの組込み
- Elasticsearch へのログ出力 (katip-elasticsearch)