Haskell/Servantで行う安全かつ高速なAPI開発

nakaji-dayo氏:株式会社LEMOから来ました。中嶋と申します。本日はHaskellとServantを使い、安全かつ高速なAPI開発を実現する、といった話をしたいと思っています。

160人ってすごい数ですね。こんな人数の前で話すの初めてかもしれないです。ハッカソンとかで発表したことはあるんですが、ハッカソンって最後はみんな死んだ状態なので。

(会場笑)

生きてる160人の前でしゃべるのは初めてです。ちょっと緊張していますがよろしくお願いします。

本日のアジェンダです。

趣旨としましては、弊社はHaskellを業務に導入してけっこうがっつりと使っていますので、技術スタックや開発のスタックを共有しつつ、Haskellにはどんなメリットがあるのか、実際どのようなことができるのかということを説明したいと思います。

もしHaskellを実際に導入していただくとしたら、周りのライブラリの状況って気になるところだと思いますので、そこらへんについてもお話しします。

改めまして本日の目標ですが、Haskellのメリットを共有し、「業務で使えるな」ということを説明します。ですのでみなさんに「Haskellを使ってみよう」とか「使用を検討してみよう」と思っていただけたらと思います。よろしくお願いします。

Haskellをどう使っているか

まず弊社でのHaskellの用途ですね。弊社ではWebAPIのBatchサーバーでバックエンドの開発にHaskellを利用しています。作っているものは一般的なWebアプリやモバイルアプリです。

直近ですと、課金のあるSNSを作っています。これに関してはバックエンドでインフラも含めて10人/月程度の規模のプロジェクトで使っています。

ちなみに社内システムも作っていまして、顧客管理とか生産管理というのが内部のシステムのバックエンドとして使っています。

まず技術スタックを共有いたします。

言語としてはHaskell。WebアプリケーションフレームワークとしてServantでできたものを使っています。今日はこのServantについて主に話すことになります。

クエリビルダとかRDBを抽象化するレイヤーとして、先ほどあったHaskell Relational Recordというものを使っています。これらについてたっぷりと説明します。

HaskellとServantの特徴

まずHaskellについて、午前中のセッションがあったのでかなりざっくりと説明します。

純粋関数型のプログラミング言語です。文字通り、関数型プログラミングがやりやすいといった特性や、副作用を分離しているといった特性があります。また静的型付けの言語で、かつ型推論が強いといった特徴があります。

Servantとは何かという話なんですけど、薄いWebアプリケーションフレームワークです。

Webアプリケーションフレームワークというのはあくまで一般的なもので、Haskellだからと特に身構える必要はなく、HTTPのレイヤーを抽象化していて、どんなリクエストがきたときにどんなレスポンスを返せばいいのかを記述するだけでWebアプリが作れる仕組みになっています。

「薄い」と言っているのは機能がシンプルということです。フレームワークの中には、認証とか認可とか盛り盛りの機能のものもありますが、そうではないもののことを言っています。

node.jsのexpressやRubyのsinatraといったものを想像していただければイメージが湧きやすいかなと思います。

Servantの特徴として、機能自体はシンプルなんですが、型レベルのプログラミングをゴリゴリ使っています。

初見では「難しいな」と感じるかもしれないですが、それによってHaskellの型でAPIを定義することができます。

その結果、型安全なWebアプリケーションの開発や、型からいろんな副産物を作ることができます。この点についてはのちほど詳しく説明していきます。

最後にHaskell Relational Recordについて、先ほどhibinoさんのご説明があったので余計なことを言うだけなんですが。

何をやってるかと言うと、データベースのスキーマからコンパイル時に型を生成して、この型を使ってHaskellでクエリを記述することができます。つまりコンパイルが通れば正しいSQLが書けていたということが保証されます。

DBに問い合わせた結果というのも型にマッピングされますので、取得した結果を使っている処理も、型のある世界で書くことができます。

実際の使い方と開発スタイル

技術スタックは以上で、ここからは具体的にどういうふうに開発していくかを説明したいと思います。

これから実例をもって説明しますが、その中で実現したいことの説明を先にしてしまいます。

APIですので、HTTPでリクエストが来て、そのあといろんな処理をしていくんですが、このいろんな処理の一連のものが全部型で守られている状態、型のある世界で行われているといったことを実現したいと思っています。

実際に業務でありそうなフローに則って説明していきます。

最初のフェーズは、APIの設計ですね。設計フェーズ。APIを設計していくんですが、これはエクセルとかで書いてもいいんですけど、せっかくなのでServantの機能を使って設計を書いていきます。

ここでServantの説明が入るんですが、まず「APIを型で定義するってどういうことなの?」って話です。これは例で説明しています。

まずユーザー一覧ですね。GET /usersでページを指定できるエンドポイントと、PATCH /users/:idでユーザーの更新をPOST出来るエンドポイント。2個のエンドポイントを例にしています。

1個目のコードがServantのHaskellのほうでして、雰囲気はなんとなくパッと見でわかっていただけると思うんですが。この右矢印みたいなやつが、エンドポイント内のPath SegmentやParameterなど、エンドポイントの構成をつなぐものです。次のダイヤっぽいものがエンドポイント同士をつなぐものとなっています。

ですので1個目のユーザー数に対してクエリパラメータとしてpageという名前で数字を渡すことができるエンドポイント。レスポンスがJSONとして、ここでは定義されていませんがGetUsersResponseという型を書いてエンドポイントを表しています。

PATCHのほうも似たような感じですね。PATCHなのでリクエストボディがあって、それも指定しているという感じですね。

これを最初に見ると面食らうかもしれないですが、単なるHaskellのtype aliasですね。わかりやすいところだとMapみたいなものだと思うんですが。単に型に新しい名前を付けています。これはAPIという名前を付けています。

APIの定義に対応する実装とHandlerについて

APIの型の定義が終わると、それに対応する実装を書いていくことになるんですが、この実装はすごく単純な関数です。

例えば先ほどのgetUsersに関してはクエリパラメータとしてpageを受け取るので、Maybe Intを受け取って、レスポンスであるGetUsersResponseを返す関数を書くだけという仕組みになっています。

これ、Handlerってなってますが、Handlerについては一旦ここではIOみたいなものだと思ってください。あとで説明します。

クエリパラメータが指定されていない可能性があるため、Maybe Intとなっています。patchUserのほうもリクエストボディを受け取っていて似たようなものです。

一番下のコードなんですが、この2個のエンドポイントを付けるとServerとAPIという型になっています。このServerというのがServantを受け取ることができる型で、後ろのAPIというのが先ほど自分で定義したAPIの型のことを表しています。

ここまで説明しましたが、なにがうれしいかと言いますと、定義したAPIの型、定義できなかった定義と実装がずれない。ずれている場合はコンパイルが通らないといったことが保証されています。

また実際に実行したときに送られたリクエストがその定義と一致しているかというのはServantによってバリデーションされ、例えばpageにStringとかを渡すと、その時点で400を返してくれるので、定義した型にあってる以降の実装を意識していれば実装できてるところがうれしいです。

Handlerについて

Handlerについて補足します。Handlerの型が何だったかについて、ざっくり言ってしまうとIOモナドとExceptモナドを合成したものになります。モナド構成については省略しますが、何ができるかと言うと、まずIOモナドなので副作用のある計算をする能力を持っています。

ExceptT ServantErr。これは何かと言うと、400 Bad Requestみたいな、HTTPに関する例外を投げることができるといったものになっています。

下の例で言いますと、IOモナドを使ってhttpRequestを行って、さらにthowErrorとしてHTTPの418をエラーとして投げているといったコードを書いています。なのでAPIを実装するのに、必要そうな機能があるのがHandlerといった感じですね。

Servantの説明はさっき言ったので以上でして。実際に機能を使ってAPIを定義するといったことをやっていきます。

エンドポイントの定義自体は先ほど説明したものとまったく同じです。このフェーズでは実装部分をundefinedで全部済ますといったことをやっています。型だけで実行できない状態にします。

実際に使うリクエストの型とかをここで定義していきます。下の例ですね。リクエストボディやレスポンスの型を定義していきます。これはidとnameというフィールドをもってる型ですね。

この型はすべていくつかの型クラスのインスタンスにしています。のちのち便利になるので、こうしておきます。

具体的には、ToJSON/FromJSONといったJSONに変換可能なクラスや、Default値から値を生成可能なクラスや、Swaggerドキュメントに変換可能といったいくつかのクラスを載せています。このうちほとんどはGenericsを使って自動的に生成することができるので、手間はありません。

swagger, Mock,Server等の生成

ここまででAPIの設計フェーズが終わっていまして、次に先ほど定義したAPIを使っていくつかの副産物を作っていきます。

まずSwaggerですね。API定義、いわゆるAPI定義書です。servant-swaggerっていうライブラリを使うことで、先ほど定義したAPIから自動的に生成することができます。

なにがうれしいかと言いますと、実装とドキュメントがずれることが絶対にありません。またSwaggerからコードを生成したりとか 、独自DSLを使うのとは違い、Haskellの言語自体の力をフルで使うことができます。

同じくモックサーバーも生成することができるのでしています。servant-mockというライブラリを使い、API定義だけあれば自動的にモックサーバーが立ち上がります。ここではテストライブラリのQuickCheckの機能を使ってランダムな値を返すモックサーバーができあがりました。

フレームワークの拡張

次にフレームワークの拡張です。先ほど説明したとおり、Servant自体非常に薄いフレームワークですので、実際に使うにはそのプロジェクトに合わせてある程度カスタマイズする必要がある場合が多いです。そこでフレームワークの拡張を行なっていきます。

私たちがやっている例をもとに説明します。まず先ほどでてきたHandlerを拡張する必要があります。

Handlerは非常に単純な機能ですので自分たちが使いたいものをくっ付けて拡張する必要があります。ここではReaderモナドといったものを合成して新しくAppMといった型を作っています。

Readerというのはグローバルに参照する値を扱うことができるもので、ここではAPI起動時に読み込むような設定値の引き回しをし、あとDBのコネクションプールやトランザクションの管理といったものに使っています。

実際にはほかにもロガーのライブラリがMonadicなものでしたらそれを合成したりとか、いくつかのモナドを付けていくことになるかと思います。

次に、型を使ってさまざまな制限をかけることがあります。

どういうモチベーションかと言いますと、先ほどのHandlerやAppMといったものはなんでもできすぎてしまうと。

例えばビジネスロジックですね。サービスと呼んでいますが、ビジネスロジック内でHTTP的な例外を投げられてしまうのはおかしいと考えたときに、それを型で制約したいといったものがモチベーションです。

ここでは2個の型を作っていまして、まずMonadService、ロジックを書く関数に使う型ですね。これは副作用やDBへのアクセスは実行可能なんですが、HTTP的な例外は投げられない。

もう1つはViewで使う型、MonadViewを作っています。これはも副作用、DBへのアクセスとかIOとかではできなくて、ただし設定値の参照は可能です。例えばS3のBucketNameとかは取得できるようになっています。

仕組みはともかく、このようにできることに制約をかけられるといった言語機能があるということを覚えてもらえればと思います。

例えばMonadServiceで実行されることが前提のクエリを投げる関数みたいなのを定義すると、それを間違ってViewの中で使ってしまったりすると、ちゃんとコンパイルエラーになるよみたいなことが実現できます。

実装とテスト

ここまででフレームワークや設計やドキュメントの生成、フレームワークの拡張が終わりましたので、実装やテストを実際に行っていきます。

ざっくりしてますね。実装に関してはごく普通にHaskellのコードを書いていきましょうということなので、私からはあまり説明しません。

普通にHaskellでコードを書いたり、そのコードのクエリを実行したりしてロジックを書いていきます。ここでみなさんは「最高のコードを書きたい」と思うでしょうが、Haskellは非常に簡潔なコードを書くことができます。

次にViewです。

ここもたいしたことはないですが……。うちらはAPIを作っているので、単にJSONにできるデータ型に値を詰めていく作業をViewと呼んでいます。ちなみにHTMLなどにしたい場合は、テンプレートエンジン的なライブラリがいくつかあります。

その中ではheterocephalusやshakespeareといった型安全なテンプレート、コンパイル時にHaskellのコードになるので、変数の使い方が合っているかなどがチェックされるような型安全なライブラリがおすすめです。これは私たちもAPIですがEメール送信とか文章を扱うところで使っています。

開発スタイル

最後は開発のスタイルです。もちろんTDD、テストも書いているんですが。ときにはAPIを実際に実行して叩きながら実装したりとかってこともよくあるかなと思っています。

これを非常に簡単にやるためにREPLを使っています。

ghcにはghciというREPLが用意されていまして、これを使っています。先ほどから見てきたとおり、エンドポイントですね、Handlerの関数って非常に単純な関数です。これをREPLで実行することができます。

この例だとpostMessagesというエンドポイントがあって、リクエストのボディを受け取ってMessageという値が返ってくるだけの関数です。これはghciで実行できます。

ここで工夫していまして、例えばPostMessageBodyというのがすごくでかい型だったときに、いちいち構築するのはけっこう面倒です。

この例では先ほど説明していたデフォルトという型クラスを使いまして、defという関数でデフォルト値を生成して、今興味があるtextというフィールドだけを書き換えてこのエンドポイントを実行しているといった例です。このように雑にREPLでバンバン叩きながら開発するといったこともできます。

全体が型でつながった状態になる利点

ここまでの説明で全体の実装は終わっているんですが、何ができたかと言いますと、外の世界、HTTPとか型がない世界からやってきたものが、Servantによって自分の定義したAPIの型になります。

それで自分が定義したリクエストの型としてプログラムの中に入ってきて、そこでいろいろ処理していくんですが、そのまま外の世界とかDBとか、いろんなものと通信します。

その部分もHaskell Relational Recordとかで型に守られるとか、型を使った処理でひと通りかけるようになっています。そのあとレスポンスを返すところまでひと通り型でつながっている状態になっています。

また設計上の制約をかけたり、ドキュメントの生成などいくつか型を使ってちょっとしたうれしいこともできました。

改めて何がうれしいかですが、人間のミスによる単純なバグを防ぐことができます。また、自明なテストを書く必要がなくなります。例えばレスポンスにいらないものを返していないかとか。

あとよくあるのがすごく単純なモックのテストですね。ある関数のテストをするのに、この中に呼び出しているものをモックにして、それを呼び出しているかだけ、みたいなテストを書くことが概ね必要なくなります。

今回の例ではRDBのみになっていて、Haskell Relational Recordを使った例だけとなっていますが、Haskellでは多くのライブラリが型安全である傾向があるので、このように全体が型で安全な状態を作ることができる可能性が高いです。

利用したライブラリについて

最後に、利用しているライブラリの概要と、ライブラリがどのくらい揃っているのかというのを共有したいと思います。こちらは使っているライブラリを適当にピックアップして分類しました。

何がしたいかと言いますと、ライブラリの揃い具合を一旦共有したくて。JSONを扱うライブラリやYAML、CSV、PDF、いろんなデータを扱うライブラリやテンプレートエンジン、ロガー、暗号化に関するライブラリとか。いろんなものが十分に揃っているというのがあります。

ただ実際に作るものによって人のライブラリってまったく違うと思っていまして。「こんなのもあるよ」と言ってもなかなか自分たちのとかを共有するのは難しいかなと思っています。

そんな中で、普通の一般的なサービスを使うときにみんなが必要になるだろうなというところで、クライアントライブラリですね。ミドルウェアとか外部サービスと通信するためのクライアントライブラリの揃い具合といったところが共有しやすいかなと思ったので、そこについて包括して説明します。真ん中の赤い部分ですね。

開発していく上で実際に使ったミドルウェアと、その対応しているライブラリについて、うちらのケースですが説明します。

RDBに関しては先ほどから説明しているようにHRRとHDBCといったコネクタで利用できます。Elasticsearchを使おうと思ったんですが、こっちはbloodhoundというライブラリがあり、Elasticsearchの5系までサポートしています。

AWSやGoogleのAPIに関してはamazonkaとgogolというライブラリがありまして。これらは網羅的なAPIになっているので明らかに十分な機能を持っています。

Firebaseに関しては、これだけちょっと問題がありまして。バラバラと存在はしていたんですが、ほかの言語のAdmin SDKのような、機能すべて網羅的なものが存在しなかったので、これは自分たちで作って使いました。

作ると言いましても、上のgogolやjose等のライブラリを使ってほぼ簡単に作れる程度のものでした。

ライブラリにまつわる問題

逆にライブラリが足りなかったケースとか、なにか問題があったみたいなケースをまとめてみました。

大きく3つの問題にぶつかることが多いかなと思っていまして。比較的新しいものへの対応ですね。先ほどのfirebaseのような例もそうですし、Elasticsearch 6系はまだサポートされていないとか。あとマルチバイト、日本語に関するバグを踏むことはたまにあります。

最後に、これは当たり前かもしれないですが、異様に具体度が高いものとか、変な用途に関する点では問題がでてきます。例えば和暦を扱うライブラリがなかったとか。あとCSVって規格が決まってるんですが、世の中にはバグったCSVがいっぱいあって。

(会場笑)

それをパースしようと思うと、一番有名なCassavaというものがパースできないとか、場合によってはこういう問題も起きてきます。ただこれは当たり前のことだと思いますし、逆に言うとこんなもんでして。

感想としましては、いろんなライブラリを使っていてもバグを踏むことは非常に少ないですし、内容とか見ても「クソ設計だな」っていうライブラリは非常に少ないです。

HaskellでWeb API開発をするメリット

ワーワーと「Haskellやらない?」と言ってきましたが、ここで今までの話をまとめますと、まず安全性といったところで、Haskell自体の機能になりますが、静的型付けの言語で副作用の分離とあるので、ある程度のバグを防ぐことができます。

見てきました通り、データベースとか外部との結合を行う部分でもかなり守ってくれているライブラリが多々あり、バグを防ぐことができます。これに関しては言語機能だけではなくてライブラリを作る人たちの文化といいますか、そういったものが必要になるので、Haskell特有のところかなと思います。

見てきた通りドキュメントと実装の齟齬を起こさないところも安全性かなと思っています。そのほか安全性を担保しつつも高速に開発しないといけないというところで、それはどうなっているのかと言いますと、Haskellはそもそもの特徴としまして、非常に抽象度の高いデータやロジックを記述できるので、コードの記述量も減りますし再利用性が高く使い回せる、そういった特徴もあります。

先ほど説明したように、自明なテストを書かなくてよい状態にするといったことも開発速度の向上につながるかと思います。また型による制約の設定をしてきましたが、こういった機能によりコードレビューのコストを下げることができ、プロジェクト全体のコストも下げられます。

最後に、REPLで殴るといった開発スタイル。これも実際のところ早い開発には必要かなと思っていまして、LLを使う人たちのモチベーションはけっこう雑に殴れるところかと思いますが、Haskellでもそれができます。

まとめますと、「Haskellすごい!」しか言ってないんですけど。多くのメリットがあることを伝えられたかなと思います。一部とがった用途だとないことがあるかもしれませんが、一般的なライブラリは十分に揃っています。

ですのでHaskellでAPIの開発することは十分にできます。業務レベルで、しかもかなり高レベルの開発体験を得られると思います。というわけでみなさんHaskellを使いましょう。ありがとうございました。

(会場拍手)