PureScriptでAPI型定義から導出される副産物
はじめに
サーバーサイドとクライアントサイドのコードを同じ言語で書けると便利ですよね。
PureScriptはJavaScriptにコンパイルされる純粋関数型言語で、それができる言語の一つです。
サーバーとクライアントで共通部分の多いAPIまわりについて、PureScriptではどのように書けるのか、Troutというライブラリを使った一例を紹介します。
API型定義
purescript-troutを使うと、API定義はたとえば次のように書けます。
type Api
= "api"
:/ ( "person" := "people"
:/ ( "list" := Resource (Get (Array String) JSON)
:<|> ("find" := Capture "name" String :> Resource (Get (Maybe String) JSON))
)
)
Troutでは型演算子(:/
、:<|>
、:=
、:>
など)を使ってAPIを組み立てます。このAPIの意味するところは、表にすると次のようになります。
エンドポイント | リクエスト型 | リクエストメソッド | レスポンス型 | レスポンス形式 | 識別子(階層構造) |
---|---|---|---|---|---|
/api/people | なし | GET | Array String | JSON | person: { list } |
/api/people/{name} | String | GET | Maybe String | JSON | person: { find } |
{name}にはString
型の変数が入ります。識別子は副産物に使われるもので、エンドポイントの文字列とは別に存在します。
副産物
副産物の導出のために、先ほど定義したAPIの型情報だけを持つ値を作ります。
api :: Proxy Api
api = Proxy
クライアントサイドの副産物
purescript-trout-clientを使うと、サーバーへのリクエスト関数が導出されます。asClients
にapi
を与えると、次のような関数が得られます。
requests ::
{ parson ::
{ list :: { "GET" :: Aff (Array String) }
, find :: String -> { "GET" :: Aff (Maybe String ) }
}
}
requests = asClients api
requests
はAff
型(モナド)の中(do構文)で、たとえば次のように使えます。
maybeName <- (requests.people.find "Ichiro")."GET"
このコードが実行されると、/api/people/Ichiro
にGETリクエストが投げられ、そのレスポンスがmaybeName
に返ります。引数をなくしたり、"GET"
でないメソッドにするとコンパイルは通りません。
サーバーサイドの副産物
purescript-nodetroutを使うと、API型定義に沿わないリクエストは弾き、沿うリクエストは引数を受け取りレスポンスを返す型が導出されます。serve'
にapi
とresources
を与えると、Api
型に基づいたサーバーが作られます。
server = serve' api resources logShow
logShow
はここではエラーログを出力をする関数です。resources
は自分で実装します。このときresources
の型は導出された次の型と一致することが要求されます。
resources ::
{ parson ::
{ list :: { "GET" :: App (Array String) }
, find :: String -> { "GET" :: App (Maybe String) }
}
}
App
型(モナド)は、中(do構文)でたとえばDBアクセスやバリデーションなどが行われることを想定した型です。resources
の実装はたとえば次のようになります。
resources =
{ parson:
{ list: { "GET": pure names }
, find: \name -> { "GET": pure $ find (\n -> n == name) names }
}
}
where
names =
[ "Ichiro"
, "Jiro"
, "Saburo"
]
find
は候補の中から条件を満たす最初の候補を返す関数です。引数をなくしたり、返す値の型が異なるとコンパイルが通りません。このコードがサーバーで実行されると、/api/people/Ichiro
へのGETリクエストに対してJust Ichiro
を返します。一方/api/people/Shiro
にはNothing
を返します。
まとめ
Troutを使うことで、API型定義からエンドポイントの漏れなく型安全にクライアントとサーバーの関数を導出することができます。リクエスト型をレコード型にしたり、リクエストメソッドをPOSTにしたり、レスポンス形式をHTMLにしたり、ヘッダーを要求したりと、実用的なAPIももちろん作れます。お試しあれ。
一例と内容は少し違いますが、簡単なyarnプロジェクトをGitHubレポジトリに上げました。
補足
他言語ライブラリとの比較
HaskellのServant
API型定義から副産物を導出するアイデアはもともとHaskellのServantからきています。しかしHaskellでクライアントサイドのコードを書くのは困難が多いです。一方ServantはSwagger出力もできるので、そこから言語を問わずクライアントサイドのコード生成もできます。ただ、生成されるコードは言語のバージョンに依存しますし、クライアントコードのコンパイルが通るにためはコード生成コマンドとコンパイルと合わせた何かしらのスクリプトが必要、といった保守面のデメリットがあると思います。
TypeScriptのFrourio
機能的には上で紹介したようなこととほぼ同じことができると思います。ただ、Frourioのコード生成はファイル作成する形で行われるため、ファイルの出力先を把握しないといけないですし、これもまた何かしらスクリプトが必要、といった手間があると思います。
これ(PureScriptのTrout)
コード生成(関数の導出)はPureScriptの型クラス(言語機能)だけで行われるため、ファイル出力やスクリプトは必要ありません。技術的なデメリットは特に思いつきませんが、PureScriptユーザーが少ないため人的な懸念はあると思います。でもPureScriptはいいぞ
Discussion