2017年12月27日水曜日

servant + persistent + puxを使ったWebアプリの作成

servant + persistent + puxを使ったWebアプリのサンプルを作成したので紹介します。

WebAPIの定義にservant 、データ永続化ライブラリに persistent 、フロントエンドフレームワークにPureScript製のpuxを使っています。

全てのソースは https://github.com/orehathiya/example-servant-persistent にあります。

servant + persistent を使ったWebAPIの紹介は以下のように既に記事がいくつか存在するのでpux との連携部分について主に紹介します。


servantの型定義からPureScriptの型とAPIクエリ関数を生成する


servantは実装するAPIの仕様を型によって定義することができ、自動でHTTPリクエストパラメータやレスポンスのシリアライズ/デシリアライズを行ったり、自動でドキュメントを生成したり、APIにアクセスするクライアント関数を生成したりすることができます。

これによってAPIの実際の実装とドキュメントやクライアント関数との間に乖離が発生することを防ぐことができます。

サンプルでは servant-purescript を使ってservantで定義したHaskellのAPIの型とデータ型からPureScriptのAPIクエリ関数とデータ型を生成しています。

自分で定義したUserとReport型は何も問題なく生成できますが、PersistentのEntity aとKey a型はそのままGenericのインスタンスにできない(GADTs等はできないらしい)ので構造が似たようなMyEntityとMyKeyを定義してそのRepで上書きしています(このあたりもっと綺麗な書き方があれば教えて欲しいです)。

data MyEntity record = Entity
  { key :: Key record
  , value :: record
  } deriving (Generic)

newtype MyKey a =
  Key Int
  deriving (Generic)

instance Generic (Entity a) where
  type Rep (Entity a) = Rep (MyEntity a)
  from = from
  to = to

instance Generic (Key a) where
  type Rep (Key a) = Rep (MyKey a)
  from = from
  to = to

myTypes :: [SumType 'Haskell]
myTypes =
  [ mkSumType (Proxy :: Proxy (Entity A))
  , mkSumType (Proxy :: Proxy (Key A))
  , mkSumType (Proxy :: Proxy User)
  , mkSumType (Proxy :: Proxy Report)
  ] 

生成される型とAPIクエリ関数


生成される型は以下のようになります。
newtype User =
    User {
      name :: String
    , age :: Int
    }

derive instance genericUser :: Generic User

derive instance newtypeUser :: Newtype User _


newtype Report =
    Report {
      imp :: Int
    , click :: Int
    , ctr :: Int
    }

derive instance genericReport :: Generic Report

derive instance newtypeReport :: Newtype Report _

Haskell側の型とほぼおなじですね。Generic型クラスのインスタンスにもなっているのでpurescript-argonaut-generic を使ってJSONのシリアライズ/デシリアライズができます。

APIクエリ関数は以下のようになります(多いのでgetUserGetByName関数だけ抜粋)。

getUserGetByName :: forall eff m.
                    MonadAsk (SPSettings_ SPParams_) m => MonadError AjaxError m => MonadAff ( ajax :: AJAX | eff) m
                    => String -> m (Entity User)
getUserGetByName name = do
  spOpts_' <- -="" ask="" let="" o="" of="" spopts_="" spsettings_=""> o
  let spParams_ = case spOpts_.params of SPParams_ ps_ -> ps_
  let baseURL = spParams_.baseURL
  let httpMethod = "GET"
  let queryString = ""
  let reqUrl = baseURL <> "user" <> "/" <> "get"
        <> "/" <> encodeURLPiece spOpts_' name <> queryString
  let reqHeaders =
        []
  let affReq = defaultRequest
                 { method = httpMethod
                 , url = reqUrl
                 , headers = defaultRequest.headers <> reqHeaders
                 }
  affResp <- -="" affjax="" affreq="" d="" decodejson="case" let="" of="" spopts_.decodejson="" spsettingsdecodejson_=""> d
  getResult affReq decodeJson affResp

APIの定義通りnameパラメータを受け取ってEntity User型を返す関数になっています。

PUXから使う


生成されたAPIクエリ関数はimportして以下のように使えます。

foldp (ReceiveUser (Entity user)) (State st) =
  noEffects $ State st {
    user = Just (Entity user)
  , status = "User"
  }
foldp (RequestUser) state = runEffectActions state [ReceiveUser <$> getUserGetByName "Alice"]

puxはいわゆるElm ArchitectureのPureScript製のフロントエンドフレームワーク です。foldpはElmのupdate、Reduxのreducerに相当する状態更新のための関数です。このfoldp関数では状態(state)の更新と副作用のあるeffectfulな処理の両方を行うことができます(詳しくは公式ドキュメント参照)。
effect内で自動生成された先ほどのgetUserGetByName関数を実行してReceiveUserイベントを発火し、state内のuserに取得したユーザーデータをセットしています。

まとめ


WebAPIを作成する時のよくある悩みとしてドキュメントやクライアントと実装の乖離が起こる事や、サーバーサイドとクライアントサイドで同じようなモデルを2回定義する必要がある事などがありますが、servantを使うとこれらの悩みを解消することができます。

皆様もぜひservantを使って楽々WebAPI開発を楽しみましょう!

参考サイト

  • https://github.com/eskimor/purescript-bridge
  • https://github.com/haskell-servant/example-servant-persistent
  • https://github.com/alexmingoia/pux-starter-app

0 件のコメント:

コメントを投稿

HaskellとScalaの型クラスの違い

 HaskellとScalaの型クラスの違いについて社内勉強会で発表しました 型クラス from hakukotsu