🦥

PureScriptでAPI型定義から導出される副産物

2021/01/24に公開

はじめに

サーバーサイドとクライアントサイドのコードを同じ言語で書けると便利ですよね。
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を使うと、サーバーへのリクエスト関数が導出されます。asClientsapiを与えると、次のような関数が得られます。

requests ::
  { parson ::
    { list :: { "GET" :: Aff (Array String) }
    , find :: String -> { "GET" :: Aff (Maybe String ) }
    }
  }
requests = asClients api

requestsAff型(モナド)の中(do構文)で、たとえば次のように使えます。

maybeName <- (requests.people.find "Ichiro")."GET"

このコードが実行されると、/api/people/IchiroにGETリクエストが投げられ、そのレスポンスがmaybeNameに返ります。引数をなくしたり、"GET"でないメソッドにするとコンパイルは通りません。

サーバーサイドの副産物

purescript-nodetroutを使うと、API型定義に沿わないリクエストは弾き、沿うリクエストは引数を受け取りレスポンスを返す型が導出されます。serve'apiresourcesを与えると、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