Reqライブラリを使ってHaskellでお手軽にHTTP通信をする
2022-10-23
2022-10-30
こんにちは。今回のテーマは「Reqライブラリを使ってHaskellでお手軽にHTTP通信をする」です。使う言語に限らずちょっとAPI叩いてデータを送受信したいということは頻繁に起こります。今回はHaskellで直感的にHTTP通信が行えるReqライブラリの使い方を紹介したいと思います。 [adsense02]
Reqとは
Haskellで容易にHTTP通信を行えるようにすることを目的としたHTTPクライアントライブラリです。Python使いの方にはRequestsモジュールのようなものだと言うと分かりやすいでしょうか。直感的にHTTP通信が行える作りになっており、似たようなライブラリにWreqがあります。HTTP通信をしてAPIから値を取得してきたり、APIにJSONで値を送信したりすることはとても頻繁に行うことですので、Haskell初学者の方も使えるようになっておくと便利なライブラリかと思います。
本記事で書きたかったこと
本記事中でReqライブラリのすべての機能を説明することはできません。よってReqライブラリを使うとHaskellでこんな感じでHTTP通信をすることができるよ、ということを感じていただける記事を書きたいなと考えました。Reqライブラリの全機能はHackageを見ていただくとして、本記事では具体例を示しつつ、基本的な使い方を紹介したいと思います。
リクエストの作り方の基本
リクエストの作り方の基本はreq
関数を使うことです。使い方としてはこんな感じです。HTMLで書かれたサイトにURLにGETメソッドでアクセスして取得したHTMLを表示するだけのアクションを書いてみます。 サンプル1:WEBサイトにアクセスしてHTMLを表示する例
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE DataKinds #-}
import Network.HTTP.Req
import Control.Monad.IO.Class (liftIO)
import qualified Data.ByteString.Lazy as DBL (putStr)
-- レスポンスのHTMLを表示するだけ
simpleReq :: IO ()
simpleReq = do
runReq defaultHttpConfig $ do
-- Req aモナド
res <- req GET
(http "note.kurodigi.com")
NoReqBody
lbsResponse
mempty
liftIO $ DBL.putStr $ responseBody res
上の例では遅延ByteStringとしてHTMLを受けて、それを表示しています。Reqライブラリの使い方は非常にシンプルで、使い勝手が良いです。runReq
関数の引数にHttpConfig型とReq a型の関数を適用して(MonadIO m) => m a型を得ます。リクエストはreq
関数で作ることができます。話を敢えて単純化するとrunReq
関数にdefaultHttpConfig
とreq
関数で作ったリクエストを適用するとIO ()を得ると考えれば良いと思います。リクエストの作り方は基本的には以下のように
req HTTPメソッド(GET, POSTなど)
URL
リクエストボディ
レスポンス
オプション
の書き方をすればOKです。req関数の演算結果は(HttpMonad m, HttpResponse response) => m response型なのですが、基本的にはReq a型を得ると覚えておけば良いかなと思います。Req a型はMonadクラスに属するのでdo構文の中で逐次処理などが行えます。Req aモナドの中でIO処理を行いたい場合はliftIO
関数を適用すればOKです。
Urlについて
URLはUrl scheme型を用います。ややこしい説明は省いてここでは実用的な例で説明します。例えばhttp://example.com/hoge/1000というURLにアクセスするときReqライブラリで使用するURLを作成するには
url :: Url 'Http
url = http "example.com" /: "hoge" :~ (1000 :: Int)
のように作成します。またプロトコルがhttpsのhttps://example.com/hoge/1000だった場合は
url :: Url 'Https
url = https "example.com" /: "hoge" /~ (1000 :: Int)
となります。(/:)関数は第2引数がText型、(/~)関数は第2引数がToHttpApiDataクラスに属する型(主にはIntだと思います)の場合です。例えばInt型の引数を取って動的にURLを作成する関数なども容易に作れます。
url' page :: Int -> Url 'Http
url' page = http "example.com" /: "hoge" /~ page
尚、URLにクエリパラメータをもたせるケースもよくありますが、これに関してはオプションの項目で説明したいと思います。
リクエストボディについて
すべてのリクエストボディの詳細はHackageのReqの説明を参考にしていただくのが良いと思いますが、基本的には以下表が用意されていて、使いたいものを選ぶだけです。以下にはよく使いそうなものと、極めて簡単な解説をつけて表にしてみました。
リクエストボディ | 簡単な解説 |
---|---|
NoReqBody | リクエストボディが不要な場合に使用 |
ReqBodyJson | JSONデータを送信する時に使用 |
ReqBodyUrlEnc | 主にフォームデータを送信する際に使用 |
ReqBodyMultipart | 主にファイルアップロードする際に使用 |
ReqBodyMultipart以外は同名の型コンストラクタを用いて型を作成します。ReqBodyMultipartはreqBodyMultipart
関数を使ってボディを作成します。reqBodyMultipart
を使うと以下のようにMonadIOモナドに包まれた形で生成されますのでご注意ください。
reqBodyMultipart :: MonadIO m => [Part] -> m ReqBodyMultipart
JSONデータの送信とフォームデータの送信の例は後ほど紹介しますので、そこで使い方を見てください。
レスポンスについて
req
関数の第4引数に適用するレスポンスですが、これはレスポンスボディをどのような形式で受けたいかによって入れるものが変わってきます。レスポンスボディが不要の場合にはignoreResponse
を入れておけばOKです。以下表にレスポンスを生成する関数をまとめておきました。
レスポンス生成関数 | 解説 |
---|---|
ignoreResponse | レスポンスボディを無視する |
jsonResponse | JSON形式で受信したいときに使用 |
bsResponse | ByteString形式で受信したいときに使用 |
lbsResponse | ByteString(遅延評価)で受信したいとき |
オプションについて
クエリパラメータを追加したいとかOAuth認証をしたい場合など、リクエストに追加で情報を付与したい場合に使います。作成したオプションは前述のreq
関数の第5引数に渡します。ではオプションの作り方を見ていきましょう。 クエリパラメータに関しては(=:)
関数で作成します。
(=:) :: (QueryParam param, ToHttpApiData a) => Text -> a -> param
ですので、キーとなる文字列(Text型)と数値や文字列等の値を結合させることでOption scheme型のオプションを生成します。
queryParams :: Option scheme
queryParams = "key1" =: (200 :: Int) <> "key2" =: ("value2" :: Text)
Option scheme型はSemigroupクラスに属しているので(<>)
関数を使用して結合することができます。複数のオプションを結合して使用することができます。 オプションにはクエリパラメータの他に認証・認可用のオプションも用意されています。主なものを以下表にまとめてみましたので参考にしてください。
認証系オプション作成関数 | 解説 |
---|---|
basicAuth | ベーシック認証用のオプション(HTTPSのみ) |
basicAuthUnsafe | HTTPでも使用可能だが使用には注意が必要 |
basicProxAuth | プロキシのベーシック認証用 |
oAuth1 | OAuth1.0用のオプション |
oAuth2Bearer | ベアラー認証用のオプション |
oAuth2Token | OAuth2.0用のオプション |
尚、オプション不要の場合にはからのオプションであるmempty
を適用しましょう。
フォームの値をPOST送信したいとき
ここからは、Reqライブラリを使って実際にどのようなことができるのか見ていきましょう。POSTでキーと値をセットで送信したいというのはよくある操作だと思います。POSTで値を送信したい場合にはオプションのクエリパラメータではなく、ReqBodyUrlEnc
型のボディを作成します。(ファイルのアップロードの場合はReqBodyMultipart
型を使います。)では例を見てみましょう。 サンプル2:POSTで値を送信する
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE DataKinds #-}
import Network.HTTP.Req
import Control.Monad.IO.Class (liftIO)
import Data.Aeson
import Data.Aeson.Types
import Data.Text (Text)
-- 例2: 値をPOSTして結果をJSONで受ける
postSample :: IO ()
postSample = do
runReq defaultHttpConfig $ do
res <- req
POST
url
(ReqBodyUrlEnc params)
jsonResponse
mempty
liftIO $ print (responseBody res :: Value)
where
url :: Url 'Https
url = https "example.com" /: "hogehoge"
params :: FormUrlEncodedParam
params = "key1" =: ("value1" :: Text)
<> "key2" =: (2 :: Int)
req関数の第3引数にReqBodyUrlEnc
型のボディを作成して入れています。ReqBodyUrlEncの型引数にはFormUrlEncodedParam型を入れます。サンプルコードではparams
という名前をつけています。FormUrlEncodedParamはオプションの項で説明したOption scheme型のクエリパラメータと同じ構文で作ることができます。
JSONデータをPOSTしたいとき
APIへの通信ではJSONデータをPOST送信したいということもとても多いと思います。この場合はReqBodyJson a 型のボディを作成します。ReqBodyJson a 型の型引数にはToJSONクラスに属する型が入ります。 Aesonライブラリのobject
関数を使ってValue型を生成してReqBodyJsonコンストラクタに渡しましょう。これでReqBodyJson型のボディが作成されます。例を見てみましょう。 サンプル3:JSON形式で値を送信する
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE DataKinds #-}
import Network.HTTP.Req
import Control.Monad.IO.Class (liftIO)
import Data.Aeson
import Data.Aeson.Types
import Data.Text (Text)
-- 例3: JSONデータをPOSTして結果をJSONで受ける
postSample2 :: IO ()
postSample2 = do
runReq defaultHttpConfig $ do
res <- req
POST
url
(ReqBodyJson jsonParams)
jsonResponse
mempty
liftIO $ print (responseBody res :: Value)
where
url :: Url 'Https
url = https "example.com" /: "hogehoge"
jsonParams :: Value
jsonParams = object -- 送信するJSONデータ
[ "key1" .= ("value1" :: Text)
, "key2" .= (9 :: Int)
, "key3" .= object [ "key4" .= (2000 :: Int)
, "key5" .= ("value5" :: Text)
]
]
JSON形式のレスポンスを解析する
おそらく多くの方がやりたいことはサーバーから帰ってきたJSON形式のレスポンスを解析して値と取り出して扱うことだと思います。今回は気象庁のウェブページ(気象庁はAPIだとは公言してない)にアクセスしてJSON形式で帰ってくるレスポンスを解析してみましょう。Reqライブラリで得たJSON形式のレスポンスはAesonライブラリで扱うことが可能です。多くの場合、Aesonは必須となるでしょう。 JSON形式でレスポンスを受けるときにはレスポンスの項でも触れましたがjsonResponseを用います。取得したレスポンスボディはFromJSON
クラスのインスタンスとして扱うことができます。予め用意された型としてはValue型などです。今回はValue型で得たボディから値を取り出す例を見てみます。 サンプル4:JSON形式のレスポンスを解析する
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE DataKinds #-}
import Network.HTTP.Req
import Control.Monad.IO.Class (liftIO)
import Data.Aeson
import Data.Aeson.Types
import Data.Text (Text)
import qualified Data.Text.IO as DTI (putStrLn)
-- JSONを受取り値を取り出すサンプル
getJsonSample :: IO ()
getJsonSample = do
runReq defaultHttpConfig request
where
-- 通信処理
request :: Req ()
request = do
res <- req GET
url
NoReqBody
jsonResponse
mempty
let val = (responseBody res :: Value)
case val of
Object obj ->
case parse getWether obj of
Success r -> liftIO $ DTI.putStrLn r
Error e -> liftIO $ Prelude.putStrLn e
_ -> liftIO $ DTI.putStrLn "解析に失敗しました"
-- 気象庁のURL
url :: Url 'Https
url = https "www.jma.go.jp" /: "bosai" /: "forecast" /: "data" /: "overview_forecast" /: "130000.json"
-- JSONオブジェクトから天気情報を取り出す関数
getWether :: Object -> Parser Text
getWether obj = do
area <- obj .: "targetArea"
text' <- obj .: "text"
return ("地域:" <> area <> "\n天気概要:" <> text')
サンプル4のコードではレスポンスボディをValue型に変換したため、すこし処理が煩雑になりました。JSONの中身がObject型に変換可能であることが自明である場合はObject型に直接変換しても良いです。そのほうがコードはシンプルになります。では、次に予め自前でWetherInfoという代数的データ型を用意して、レスポンスボディをこの型に変換するサンプルを見てみましょう。オブジェクト指向プログラミングなどでエンティティオブジェクトを用意してJSONデータからマッピングするようなイメージですね。 サンプル5:JSON形式のレスポンスを解析する(その2)
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveGeneric #-}
import Network.HTTP.Req
import Control.Monad.IO.Class (liftIO)
import Data.Aeson
import Data.Aeson.Types
import Data.Text (Text)
import qualified Data.Text.IO as DTI (putStrLn)
import GHC.Generics
-- レスポンスボディをWetherInfo型に変換するサンプル
getJsonSample2 :: IO ()
getJsonSample2 = runReq defaultHttpConfig $ do
res <- req
GET
url
NoReqBody
jsonResponse
mempty
let obj = (responseBody res :: WetherInfo)
liftIO $ DTI.putStrLn $ getWether obj
where
url = https "www.jma.go.jp" /: "bosai" /: "forecast" /: "data" /: "overview_forecast" /: "130000.json"
getWether :: WetherInfo -> Text
getWether obj = "地域:" <> targetArea obj <> "\n天気概要:" <> text obj
data WetherInfo = WetherInfo
{ publishingOffice :: Text
, reportDatetime :: Text
, targetArea :: Text
, text :: Text
} deriving (Show, Generic)
instance FromJSON WetherInfo
ご覧の通り、受けたレスポンスをWetherInfo
型として受けて、データを取り出すことができています。やっていることはサンプル4のコードと同じですが、複数ヶ所でWetherInfo型データを使う場合などはこちらの方がシンプルに実装できると思います。一方、JSONの解析を一度しか行わない場合など、わざわざ新たなデータ型を定義したくない場合などはサンプル4の方法が手軽で良いかもしれません。
最後に
Reqを使ったお手軽なHTTP通信を体感していただけたでしょうか?筆者の力量が足りず、消化不良だったかもしれません。また、人によってはWreqライブラリの方がお好みかもしれません。この記事がHaskellに興味を持つきっかけになれば幸いです。 [adsense]