extensible の拡張可能レコードを使って REST API Haskell パッケージを作る
fumieval さんが作ってる extensible
パッケージの拡張可能レコードを使って,楽天のウェブサービスのAPIのための Haskell パッケージ rakuten
というのを作ってるので,その簡単な紹介です.
まだ途中だけど,extensible
の拡張可能レコードを使うためにの良い例になるかなと思ったので書いておく.
いきさつ
前では req
パッケージを使って chatwork という REST API の Haskell パッケージを作った話を書いた. その時の最後にも,レコードのフィールド名が被ったりしてつらいので extensible
を使いたい,という話を書いた.
で,さらにバイト先から「楽天の API のも作って」と言われたので,こっちは最初っから extensible
使っちゃおうと思ったわけです(ちゃんと許可は取ったよ).
まだ,楽天商品検索 API しか作ってないけどね.
拡張可能レコードを使う
今のところ次の2ヶ所で使ってる.
API のレスポンス JSON
ドキュメントに従って,こんな風に書ける.
type IchibaItems =
Record '[
"count" ':> Int,
"page" ':> Int,
"first" ':> Int,
"last" ':> Int,
"hits" ':> Int,
"carrier" ':> Int,
"pageCount" ':> Int,
"Items" ':> [ItemWrap],
"GenreInformation" ':> [GenreInformation],
"TagInformation" ':> [TagGroupWrap]
]
ItemWrap
型とか,GenreInformation
型とかも同じように定義してある. で,これらを aeson
パッケージの FromJSON
型クラスのインスタンスにする必要があるが,extensible
の拡張可能レコードなら一括にできる!
instance Forall (KeyValue KnownSymbol FromJSON) xs => FromJSON (Record xs) where
= withObject "Object" $
parseJSON -> hgenerateFor (Proxy :: Proxy (KeyValue KnownSymbol FromJSON)) $
\v -> let k = symbolVal (proxyAssocKey m) in
\m case HM.lookup (fromString k) v of
Just a -> Field . return <$> parseJSON a
Nothing -> fail $ "Missing key: " `mappend` k
まぁこれは extensible
のリポジトリにサンプルとして置いてあるんだけどね.
リクエストパラメータ
検索をしたいので,いろんなパラメータを渡せる仕様になってる...(つらい). フィールドの名前も被りそうだし,これも拡張可能レコードにしてしまおう.
type IchibaItemSearchParam =
Record '[
"keyword" ':> Text,
"shopCode" ':> Maybe Text,
"itemCode" ':> Maybe Text,
"genreId" ':> Maybe Int,
"tagId" ':> Maybe Int,
"hits" ':> Maybe Int,
"page" ':> Maybe Int,
"sort" ':> Maybe Text,
"minPrice" ':> Maybe Int,
"maxPrice" ':> Maybe Int,
"availability" ':> Maybe Bool,
"field" ':> Maybe Bool,
"carrier" ':> Maybe Bool,
"imageFlag" ':> Maybe Bool,
"orFlag" ':> Maybe Bool,
"NGKeyword" ':> Maybe Text,
"purchaseType" ':> Maybe Int,
"shipOverseasFlag" ':> Maybe Bool,
"shipOverseasArea" ':> Maybe Text,
"asurakuFlag" ':> Maybe Bool,
"asurakuArea" ':> Maybe Int,
"pointRateFlag" ':> Maybe Bool,
"pointRate" ':> Maybe Int,
"postageFlag" ':> Maybe Bool,
"creditCardFlag" ':> Maybe Bool,
"giftFlag" ':> Maybe Bool,
"hasReviewFlag" ':> Maybe Bool,
"maxAffiliateRate" ':> Maybe Double,
"minAffiliateRate" ':> Maybe Double,
"hasMovieFlag" ':> Maybe Bool,
"pamphletFlag" ':> Maybe Bool,
"appointDeliveryDateFlag" ':> Maybe Bool,
"genreInformationFlag" ':> Maybe Bool,
"tagInformationFlag" ':> Maybe Bool
]
(ほぼなくても良いので Maybe
型になってる)
req
パッケージでリクエストパラメータにするには次のように Monoid
型クラスで合成していく.
= "price" =: (24 :: Int)
param <> "mmember" =: ("hoge" :: Text)
この数を書くのはだるいよね... そこで extensible
ですよ!
class ToParam a where
toParam :: (QueryParam param, Monoid param) => Text -> a -> param
instance ToParam Int where
= (=:)
toParam
instance ToParam a => ToParam (Maybe a) where
= maybe mempty . toParam
toParam
instance ToParam a => ToParam (Identity a) where
= toParam name . runIdentity
toParam name
class ToParams a where
toParams :: (QueryParam param, Monoid param) => a -> param
instance Forall (KeyValue KnownSymbol ToParam) xs => ToParams (Record xs) where
= flip appEndo mempty . hfoldMap getConst' . hzipWith
toParams Comp Dict) -> Const' . Endo . (<>) .
(\(. symbolVal . proxyAssocKey) getField)
liftA2 toParam (fromString library :: Comp Dict (KeyValue KnownSymbol ToParam) :* xs) (
ToParam
型クラスは chatwork
パッケージのときにも定義した. もちろん,Text
型や Bool
型のインスタンスも定義してある. 前と違うのは Identity
型のインスタンス. Forall (KeyValue KnownSymbol ToParam) xs
型クラスは,xs
が普通の拡張可能レコードの場合は Member xs x => ToParam (Identity (AssocValue x))
であることを保証するため定義した(Identity
以外のモナドにしようと思えばできるけど).
これで toParams
を呼ぶだけで,req
でのリクエストパラメータとして使える(やったぁ).
Default
型クラス
リクエストパラメータのフィールドはこんなにあるけど,基本使わない... なので,デフォルトな値があって,レコード構文みたいに書き換えるのが良くある手法な気がする.
= defaultParam { keyword = "Rakuten" } param
しかし,このデフォルトの値を定義するのもだるい! なので,拡張可能レコードを data-default-class
パッケージの Default
型クラスのインスタンスにしてしまおう!
instance Default a => Default (Identity a) where
= Identity def
def
instance Default Text where
= mempty
def
instance Forall (KeyValue KnownSymbol Default) xs => Default (Record xs) where
= runIdentity $ hgenerateFor
def Proxy :: Proxy (KeyValue KnownSymbol Default)) (const $ pure (Field def)) (
簡単ですね. これで,lens
のセッターを使って簡単に書き換えれるようになった.
= def & #keyword .~ "Rakuten" param
気になるところ
Haskell の型クラスのインスタンスのスコープって制限するのは難しい. なので,拡張可能レコード全般という,こうも広範囲に影響しそうなインスタンスをバシバシ定義してもいいのかなぁという気持ちはある.
(だれもこのパッケージを使わないだろうけど)
おしまい
それでも全てのエンドポイント分を作るのはだるいけどね...