Haskellを導入した話とHRRの紹介

khibino氏(以下、khibino):「Haskellを導入した話とHRRの紹介」ということで発表させていただきます。

自己紹介スライドですけど、Twitterアカウントは「@khibino」です。

僕は8年ぐらい職業Haskellプログラマとして働いています。最近転職したので、前職の話なんですけど、とあるISPで契約管理とか認証バックエンドのシステムの開発の仕事をしていました。

この時にHaskellを導入した時の話をしようかなと思ったのと、あとそれだけだとネタが不足しそうだったので、私が開発している「Haskell Relational Record」というライブラリの紹介を後半に入れています。

まずHaskellを導入した話なんですけど、きっかけは、Perlに代わるGlue言語を検討したくなったことです。

これはもうすでに10年ぐらい前に、これらを置き換えたくなったみたいなところから始まっている話なんですけど(笑)。

Perlでいろいろ書いてありました。Javaのバッチプログラムを呼び出す大量のPerl Scriptが書いてあって。気がついたら私しかメンテナンスしてないんですよね。どうしようかなと思って。

あと、複雑な処理が書いてある部分があるので、実行前に検査したいんですけど、基本的にあんまり検査できない。一応Perlにも、本当に実行が始まる前に走らせる仕組みとか一応あるんですけど、やっぱりそれだけで全部やるのはつらいので。

この頃だと、JavaがUNIXとの親和性あまり高くないんですね。いろいろと。だから「Javaはプラットフォームを選ばない」とか言ってるけど、「むしろ『どのプラットフォームでもうまく動かない』みたいになってたんじゃないかな?」とちょっと思ってたんですけど。

(会場笑)

「困ったな」と思って。

欲しかったのはけっこう単純で、実行前になにかいろいろと、型検査とかでいいんですけど、検査をできること。あとUNIXとの親和性がちゃんと欲しい。あとPerlがスクリプト言語だったので、「スクリプト実行もできるといいんじゃないの?」みたいなのもちょろっと思ったんですけど、ここはどっちがいいのかよくわからなくて。

これは私1人でいろいろ選んで検討してた時期もあったので、けっこう適当なんですけれど。

一時期Common Lispとか検討したこともあったんですけど、これもちょっと(笑)。いくらコンパイル時にマクロでいろいろできるといっても、これだけ全部やるのはつらいんじゃないですか。やっぱり型欲しいですよね、と思ったり。

あと、OCamlとかも考えたんですけど、OCamlを試しているときにHaskellに興味出てきちゃって(笑)。

それでHaskellは、unix packageをその時読んでみたら、けっこうよくできてそうだなと思って。あと「Template Haskellにマクロもあるぜ!」みたいな感じで、これやってみようかなと思いました。

Haskell導入前の検討

あとこのへんのunixとかfilepathとかdirectoryとかprocessとかって、もうコンパイラにライブラリが入っているんですよね。だから、GHCさえ入っていれば絶対使えるんですよ。

だからそういう面倒くささがないなと思って。いろんなライブラリを入れなきゃいけないということも、このぐらいだったらあんまりないだろうと思って。いよいよ2010年ぐらいから導入しだしました。

すごいですよこれ! むちゃくちゃ古いですよ! みなさんもうわからないんじゃないですかね。Debian lennyとかもう知らないでしょ? Debian 5.0です。5.0! あと、GHC6.8ですから! 8.6じゃないですからね? 6.8ですから!

(会場笑)

6.8です。ヤバイ! cabal installがこの頃なかったんですよ。すごくないですか!?

(会場笑)

まあそれはそれとして。でも、別に小さいプログラムを書くだけだったら大したことないので。

新しく書く部分はもうどんどんPerlで書くのをやめてHaskellにしていって。まぁ私1人でなんですけど。でも、ファイル操作とかJavaをひたすら呼び出すとかそんなのばっかりなので、けっこう簡単なプログラムですね。

1人なんですけど、Haskellのプログラムはあんまり大きくないので、アプリの本数に比べて行数がぜんぜん少ないんですね。アプリ16個で2,700行とか、アプリ22本で5,800行とか、そんな感じなんですけど。あんまり大きくないです。

あとは、Perlで書いてる頃よりだいぶ精神的負荷が下がりましたよね。

Haskellを導入して書きやすくなったもの

それはそうと、最初はバッチプログラムを呼び出すプログラムとか書いてたんですけど、ちゃんと使えるのでだんだんいろんなことに使い始めて。それもほとんど全部私がやってたんですけど。

だから、ふだんから仕事で使っててやっぱり感動したのは、Parser Combinatorが使いやすさですね。

ISPとかだと変なテキスト処理がいっぱいあるんです。とにかくよくわからない整理されていないログがいっぱいあるので、そこから無理やり必要な情報を取り出すはめになるみたいのがけっこう多くて。

Parser Combinatorはここに書いてあるような……山ほどくるトラフィックデータを全部加工してDBに突っ込んだり、社内のIRCがやたらめったらチャンネル立っているのをログを検索したいとかって言われて、一生懸命それ用に加工したり。

そうですね、接続ログ。みなさんがプロバイダとかでこのフレッツとかプロバイダを使うときに、「PPPoEで認証して」みたいな認証ログがものすごい数あるんですけど、そいつを集計するプログラムをHaskellで作りました。

このAttoparsecは、当時Parsecよりだいぶ高速に動作するParser Combinatorとして有名で、この速度に助けられたこともあります。

あと、いろいろトラブルが起きるんですけど、そうすると、実際なにが起きたのかをログから解析しなきゃいけなくて、そういったことを抜き出す仕事もいっぱいありましたが、これもすごい助かりました。

あと、認証バックエンドを作った時に、古い認証サービスの認証データを加工して新しい認証サービスに入れたりする時とかに使いました。

ビルドシステムもHaskellで書きました。

実は前職のISPはバックエンドのリレーショナルデータベースでDB2を使ってたんですけど、そのDB2のパッケージをIBMから配られているDebian用に変換するプログラムを書いて、それ全部ビルドするみたいな。

あと、内製のHaskellライブラリもいっぱい作ったので、それを全部Debianパッケージに変換するプログラムを書いたりしました。全部ビルドすると、気がついたら40分ぐらいかかるようになりました。びっくりしましたけど。

認証バックエンドの認証サーバを作ったんです。Haskellって Software Transactional Memory( STM)が簡単に使えるので、状態のあるマルチスレッドプログラムがすごい書きやすくて助かりました。

マルチスレッドとDSLについて

オープンリゾルバ検出システム、これけっこうおもしろかったんですけど。

実はISPのお客様にオープンリゾルバの方がいっぱいいらっしゃって、「どのぐらいいるんだろう?」と思って探しにいくというプログラムを書いていた。これもDNSクエリーするコマンドを呼び出すだけのすごい並列度の高いプログラムで、たぶん今でも毎日900並列ぐらいで動いているんじゃないかと思います。

あとは、バックエンドの接続認証サーバはやっぱりマルチスレッドのものを書きました。こういうものが書きやすかったんですね。

あと、やっぱりDomain Specific Language(DSL)ですね。

解こうとした問題に合ったDSLを定義するみたいのは、やっぱりHaskellはけっこうやりやすいんじゃないかと私は思っていて。なんといっても、まずMonadが入ってたりするんですけど。実際どういうふうに使うかみたいのは、今日はあまり詳しく説明できないですけど。

あと、私が作ってるHaskell Relational Recordってライブラリがあって。これはSQLを組み立てるDSLなので、これを使ってデータベースのトランザクション処理みたいなのを書いて、実際に債権データの集計処理とか、さっき言った認証サービスのバックエンドのデータ管理のシステムとかを作りました。

では、ここらへんで一応導入した話のまとめです。

まず、Glue言語の置き換えとしてHaskellを導入しました。書きやすかったのは、テキスト処理とかプロセス制御とかマルチスレッドとか、あとDSLだったという話でした。

Haskell Relational Recordの紹介

ここから後半で、Haskell Relational Recordの紹介なんですけど。これは入門者向けの内容じゃなくなってるかもしれないので、注意事項という感じなんですけど。あんまり厳密な「どういうふうに動いているんだ?」みたいのはとりあえず気にしなくても大丈夫です。ただ、SQLがわからないと厳しいかもしれません。

要するにHaskellのコードとSQLが対応づいている、みたいなのが今日出てくる予定です。なのでそれがなんとなくわかれば、細かいところは気にしなくていいと思います。実際DSLって、そういうふうに適当に書くと、うまく展開されて、なんかいい感じになる、というためのものなので。

一応このURLがプロジェクトページになりまして、「SQLを組み立てるHaskell内DSL」、Haskell Relational Recordライブラリです。

売りは部品化と型安全です。だから、なにか小さいクエリを組み合わせて、より複雑な大きいクエリを作ることができて、部品化がちゃんとできていて、あと部品を組み合わせるときに型をチェックするので、変なものが変なところに組み合わさっても、誤りがあればコンパイル時に発見できるでしょうという感じです。

あと、実はコンパイル時にTemplate Haskellのマクロの機能を呼んで、Database Schemaをデータベースから読んできて、それに合った型定義を生成するので、データベース側ともだいたい型が合っていることがわかります。

このライブラリは2013年ぐらいから開発してました。

SQLの結合と集合演算

いきなりですが、SQLのJOIN(結合)ですね。

これがどういうものかという話なんですけど、これほとんど説明がなにも書いてないですけど。今「person」というテーブルと「birthday」というテーブルがあって、「person」にはnameとageとfamilyが入ってて、「birthday」にはnameとdayいうふうに入っているんですね。こいつをJOINしました。

これはこういう集合演算です。数式アレルギーの人はうんざりする話なんですけど、数式だとこういうものだと思ってください。だからpersonの集合から1個取って、birthdayの集合から1個取って、その1個取ったやつのnameを取り出してるのが等しいようなやつだけ、組を全部集めてくださいって言ってるわけですね。

SQLはこの数式を書こうとしてるんですよ。たぶん……、というか最初に考えた人はそう考えたと思ってて。

Haskellはこの数式に似た記法があるんですよね。例えば「List内包表記」というのがそれで、集合のこの「∈」のところの記号と、この「<-」のですね。「似てるでしょ?」みたいな。まぁ、気分だと思うんですけど。

このリストから1個取り出して、そいつのnameというフィールドが等しくなっていれば、こんだけ全部リストにして返してくださいというのが「List内包表記」ですね。

それで、見た目はちょっと違うけど、これとまったく意味が同じものをdoで書けるんですよ。まったく同じなので読み替えるだけです。この中がどうなったかは気にしなくてもいいんです。

Haskellを使ったDSL

「これと同じようにSQLを組み立てられたらいいんじゃない?」と思ってやってみました。

ゴテゴテしてますけど、personから1個取り出して、birthdayから1個取り出して、そいつのnameというフィールドを取り出したやつを、その結果の2つをくっつけて返してみたいなものが、このList内包表記と同じような、Listモナドと同じようなことをやるクエリがほしいなと思って作ったら、こんなふうになりました。

だから、こういうふうに書くと実はこういうSQLが生成されると。

こうやって定義したやつを展開すると、こういうSQLが生成されます、みたいな。だから、SQLをあんまり知らなくても、もしかして書けちゃうかもしれないみたいな感じのやつです。

このpersonのnameとかbirthdayのnameとかを選ばせるのが面倒くさいという人が多いので、最近は「もうnameというフィールドがあればいいんだろう」みたいなふうに書ける機能が増えたりしました。最近はこれが短くなった。

こうすると型がわかりにくくなることがあるんです。だからこれは一応トレードオフがあるんですよね。なので、みなさん書いてて、「型が決まらなくてびっくり!」みたいなことがあるかもしれないですけど、そういうときは元の書き方とかで工夫してみてください。

Haskell Relational Recordのさまざまな例

ここからはどんどん例が出てくるんですけど。これはLEFT JOINの例ですね。

LEFT JOINをやると、左側が……そうですね。左側はだからMaybeがつかないんですよね。右側がないかもしれないからMaybeがついてます。ここqueryMaybeってやるとLEFT JOINになります。

でも、型を合わせるための書き方が面倒くさい。Maybeじゃないやつから出てるから、justがついてないからjustつけたりしなきゃいけないとか。Maybeのやつから取り出すからfmapみたいのは面倒くさい。逆にここは面倒くさいみたいな。

あと、これは集約関数を使う……あそこでgroupByしてますね。要はfamilyのフィールドでgroupByしたやつが出てきて。

ここでgroupByしたやつじゃないと結果が作れないように型検査になっているのもおもしろいところです。groupByしたやつとsum、集約したやつだけを組み合わせて結果が返ってくるように書いてある。集約してないやつとかを書くと、型エラーになります。

絞り込み。

これはwhereの例ですね。集約をしてるんだけど、whereで条件を絞り込んで出したりもして。これだいぶ複雑なクエリに見えますけど、平成生まれで同じ誕生日の人を数えますみたいな感じですね。これ長いので、この次のページに生成されるSQLが出ますけど。

これ、実はよく見ると同じ表現が何ヶ所か出てくるんですよ。

「#day b」とか、この「count ($ #name p)」とかを使いまわしたりするので。

だけど、これただのHaskellなので、letでこうやって変数に割り当てて再利用できるんですよ。ここの「count $ #name p」とか再利用できる。

だからこのSQLには、COUNTがこことここに2回出てくるというのを、Haskellの中では同じ変数に割り当てて再利用できたり。

あと、これはorderByの例ですね。orderByもこんなふうに書けます。

これは「誕生日」と「名前」の2つのキーでもって複合キーでソートしてますね。単純ですよね。

これでもうほとんど終わりなんですけど、placeholderの例です。

一応placeholderも使えます。placeholderは型を安全にするのが難しくて、今ここにいらっしゃる山本悠滋さんと一緒に取り組んでいる最中なんですけど。型が不安になるけど、一応安全でないところまで書けるようにしてあって。

あとこれがwindow関数というやつです。

window関数はおもしろいですよね。使ったことある人はけっこう少ないかもしれないですけど。説明に書いてあるように、家族ごとの年齢の順位を出すみたいなSQLですけど、こんなやつですよね。

最後のスライドですね。

まとめということで、簡単でしたが、Haskell Relational Recordの紹介でした。

1つめは合成可能性ですね。最初にも言ったんですけど、結局、組み合わせ可能なDSLになっていてSQLの組み合わせができるというのと、定数の定義が再利用できるのでcomposabilityが高いということ。

あと、組み合わせるときに、小さいクエリを組み合わせてより大きなクエリを構成するときに、静的型検査でちょっと安全になりますよと。そういう話でした。ありがとうございました。

(会場拍手)