Haskell使ってみた
こんにちは。インフラストラクチャ本部の池原です。
このエントリはGREE Advent Calendar 2013 13日目の記事です。
グリーではミドルウェアの開発にHaskellを用いています。本日は、C/C++やJavaの経験はあるがHaskellは初めてだった私が、Haskellをミドルウェア開発に導入した際に戸惑った事をいくつかご紹介します。
私がHaskellを使い始めたのは1年半ほど前です。最初はOCamlに興味を持っていたのですが、すでに社内で利用者がいたこともあり、諸般の事情からHaskellを選択することにしました。
Haskellに対する私の第一印象はこのような感じでしょうか。
- 型システムが強力なので、つまらないバグでサービスを止める事態を避けられる。
- 他の関数型言語と比べて読みやすい(カッコをあまりつかわなくてもよい)。
- Posix関連のライブラリが充実しており、システムプログラミングが容易である。
Haskellを使えば信頼性が高く、簡潔なコードが書けることは、Haskellを少しでも触ったことがある方なら概ね同意していただけると思います。しかし、私は関数型言語を使ってコードを書いた経験がなかったこともあり、次のような点で戸惑いました。
サーバのようなソフトウェアを書くのは難しいのではないか (;・∀・)
懸念
Haskellは純粋関数型言語にカテゴライズされるため、言語仕様としては、変数のように特定の名前と紐付けて状態を保持する手段が提供されていません。状態を保持する手段がないということは、サーバのように複数のスレッドが同じ状態を共有するようなプログラムは書きにくいのではないか、と思いました。
実際
IO モナドの中だけで使える状態保持の仕組みをランタイムレベルで備えています。むしろ、楽観的ロック操作に基づくSTM(ソフトウェアトランザクションメモリ)、同期機構を備えるMVar、アトミック更新のできるIORef等の高機能な「変数」が用意されているため、アプリケーションの特性に応じて容易に状態を扱えます。
サーバの場合、クライアントからの要求(コマンド等)が来たら処理を行って、サーバの状態を書き換えるとともに、応答を返す必要があります。これをものすごく単純化してみると、以下のように変数とループとして表現できると思います。C言語とHaskellで書いたコードを比べてみても、構造に特段の違いは見られません。
Haskellの入門書はHaskellの特徴にフォーカスするため、変数に相当する機能の話は後の方に書いてあることが多いです。実用主義者の方は入出力(IO)の章から読んで、普通の手続き型言語のような書き方もできる、という事で安心してから学習するとよいかもしれません。
参考
ライブラリが少ないのではないか (;・∀・)
懸念
プログラミング言語を選定するとき、そのプログラミング言語が理論的にどれだけ優れているかということは、選定のための1つの基準にしか過ぎません。やりたいことは、ビジネス上の問題を解決することであって、生産性が最も高い言語を選ぶべきだからです。
その意味では、自分たちの問題領域に関連するライブラリが豊富である、ということは最も重視されてしかるべきです。HaskellはC++やJavaなどと比べるとまだユーザが少ないこともあり、ライブラリが少ないか、あっても未成熟であるのではないかとの印象をもっていました。
実際
HackageというHaskellのライブラリを集めて配布しているサイトがあります。こちらのサイトにあるパッケージ一覧(http://hackage.haskell.org/packages/ )を見ると、様々なライブラリがパッケージ化され、直ぐに利用できるよう準備されていることがわかります。すべてのライブラリがHackageにアップロードされてるとは限らないため、検索エンジン等も併用しつつになりますが、ここで自分が必要とする機能が揃っているか確かめることができます。
幸い、我々が必要とする機能(パーサやWeb/DB関連)の多くはパッケージとして提供されていたので大きな問題とはなりませんでした。ただ、不足している機能もあったのは事実です。
例えばテストに関しては、QuickCheckを始めとするユニットテストフレームワークの充実度は目を見張るものがありましたが、分散システムを作る上で必要な、システムテストのフレームワークはありませんでした。そこで、我々は test-sandbox というフレームワークを作り、分散データベース等の開発に使用しています。OSSになっていますので、ご興味のある方はhttp://gree.github.io/haskell-test-sandbox/ を参照ください。
プラガブルな設計のシステムを作りにくいのではないか (;・∀・)
懸念
Haskellを使って作った制御システムを改善しようとした時のことです。このシステムは当初は実行中のジョブを保存するためのキューとしてApache Zookeeperを使っていました。しかし、ジョブ実行の部分は、運用管理コマンドの実装でも使えるのではないかと思い、データベースアクセスの実装を切り替え可能にした上で、独立したライブラリとして整備しようと思いました。Zookeeperの代わりにSqliteでローカルにジョブの状態を保存すれば、独立したコマンドラインツールとして使えます。
やりたいことは至極簡単で、プログラム起動時に設定ファイルやコマンドライン引数でどちらのキュー実装にするか選択することです。UMLのクラス図で書けば以下のようなことができればいいわけです。
Haskellっぽいコードで書くと、以下のようなことがしたいわけです。
1 2 3 4 5 6 7 8 |
class BackendQueue q where readQueue :: q -> IO (Maybe (BS.ByteString, String)) writeQueue :: q -> BS.ByteString -> IO (String) closeQueue :: q -> IO () data JobQueue e a = JobQueue { backendQueue :: BackendQueue } |
残念ながらこれはコンパイルが通りません。BackendQueueは型クラスであって、データ型ではないからです。
実際
最近のHaskellはパワーアップしていて、GHCコンパイラには上記のような実装を可能とするための拡張が備わっています。
1 2 3 4 5 6 7 8 9 10 11 |
{-# LANGUAGE GADTs #-} class BackendQueue q where readQueue :: q -> IO (Maybe (BS.ByteString, String)) writeQueue :: q -> BS.ByteString -> IO (String) closeQueue :: q -> IO () data JobQueue where JobQueue :: (BackendQueue q) => { backendQueue :: q } -> JobQueue |
これでBackendQueueのインスタンスならどんな型でも保持できるようになりました。
さすがですね。強力な型システムによる制約と多相による柔軟性が両方そなわり最強に見えます。
ファンクターとかアプリカティブとか意味がわからない ( ^ω^)・・・
懸念
見よう見まねでIO関数を使って、たまに普通の(純粋な)関数を呼び出していれば、プログラムは組めるようになりました。しかし、情報収集のため、Haskell関連の情報をネットで読んでいると、時折、「ファンクター」や「アプリカティブ」などの意味不明な言葉が飛び込んできます。
どうやら重要な概念のようなので、これを知らないとHaskellを使う意味がないような気がしてきます。
実際
型クラスなどはオブジェクト指向言語にも同じような概念があるので馴染みやすいのですが、年をとり過ぎたせいで頭が硬くなってるのか、関数型言語特有の概念を理解するのは簡単ではなかったです。というか、定義が簡潔すぎて何の役に立つのかしっくりこないという状態になりました。
とりあえず、無理に使おうとしなくてもコードは書けます。理解があやふやなままに使ってコンパイルできるコードができなければ、周囲にHaskellがダメだという印象を持たれかねません。それよりも、関数型プログラミングのやり方を勉強して、少しずつ手続き型の書き方から関数型の書き方に直していった方がいいかもしれません。
余裕がでてきたら「すごいHaskell本」をこっそり読みましょう(読みました(;・∀・))。ついでにモナドがなんだったのかも理解できます。アプリカティブのありがたみがわかるParsecのようなライブラリを使ってみるのもいいと思います。
参考
実は遅いのではないか ゜д゜)・・・
懸念
GHCのランタイム(RTS)には、並列オプション(-N)というのがあります。Memcachedプロトコルのプロキシーサーバを試作した時のことですが、これを使えばスレッドを使ったサーバのパフォーマンスが上がるのではと思ったのですが、性能が上がりません。
Haskell(というかGHC)は現代的なマルチプロセッササーバでは使い物にならないのではという疑念が浮かびました。
実際
当時使っていたGHC (バージョン7.4)はIOサブシステム(IO Manager)の問題で、マルチスレッドを使ったプログラムがうまくスケールしないという問題がありました。結局、プロキシーサーバの起動時にワーカプロセスをforkすることでマルチプロセス化して性能を上げることができましたが、セッション間で状態を共有しなければいけないサーバでは、マルチスレッドでスケールしないのは問題です。
幸いなことに、次期バージョン(GHC 7.8)でIO Managerが改善され、この問題は解消しています。Haskell Symposium 2013で発表された論文によると、多数のコアを搭載したサーバでもちゃんとスケールするようです。
今までが一体何だったのか、と思ってしまうぐらい高速化していますね。+(0゚・∀・) +
参考
以上、簡単にではありますが、Haskellを使い始めたころに感じた不安や、実際に困った事をいくつかピックアップしてお話しました。我々のHaskellに対する評価は、評価戦略などが従来の言語/ランタイムと異なるのでいくつか気をつけるべき点はあるが十分実用になる言語である、というものです。
とはいえ、我々はまだミドルウェアの開発にしか利用していませんので、関数型言語の実務ユーザが集まる CUFP(http://cufp.org/)の発表や、各種メディアの事例紹介などを参考にしつつ、どこまで適用範囲を広げられるか検討していきたいと思っています。もしかしたら、HaskellでゲームやSNSを実装する日もくるかもしれませんね。
日本のHaskell界隈は先進的なユーザが積極的に情報発信をしているため、学習のための情報が日本語で提供されており、我々は恵まれていると思います。一方で、初学者がはまる点や疑問に思う点を解消する場はまだ少ない気がします。私が疑問に思ったことは大抵、stackoverflow.comで同じような質問がされていましたので(答えがあるとは限らないですが)、困ったら英語でも検索してみるとよいかもしれません。
我々も(ちょっと恥ずかしい)失敗などを公開していますので、よかったら参考にしてみてください。
CUFP 2013: Yasuaki Takebe: A Mobile Gaming Platform Case Study
晒し合って、共に成長しましょう!(;´Д`)
明日は堀口さんがgitの話をしてくれるそうです。私もgitを使ってるので楽しみにしています。