Linuxを日常的に使う実験ブログ

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関数にdefaultHttpConfigreq関数で作ったリクエストを適用すると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リクエストボディが不要な場合に使用
ReqBodyJsonJSONデータを送信する時に使用
ReqBodyUrlEnc主にフォームデータを送信する際に使用
ReqBodyMultipart主にファイルアップロードする際に使用

ReqBodyMultipart以外は同名の型コンストラクタを用いて型を作成します。ReqBodyMultipartはreqBodyMultipart関数を使ってボディを作成します。reqBodyMultipartを使うと以下のようにMonadIOモナドに包まれた形で生成されますのでご注意ください。

reqBodyMultipart :: MonadIO m => [Part] -> m ReqBodyMultipart

JSONデータの送信とフォームデータの送信の例は後ほど紹介しますので、そこで使い方を見てください。

レスポンスについて

req関数の第4引数に適用するレスポンスですが、これはレスポンスボディをどのような形式で受けたいかによって入れるものが変わってきます。レスポンスボディが不要の場合にはignoreResponseを入れておけばOKです。以下表にレスポンスを生成する関数をまとめておきました。

レスポンス生成関数解説
ignoreResponseレスポンスボディを無視する
jsonResponseJSON形式で受信したいときに使用
bsResponseByteString形式で受信したいときに使用
lbsResponseByteString(遅延評価)で受信したいとき

オプションについて

クエリパラメータを追加したいとか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のみ)
basicAuthUnsafeHTTPでも使用可能だが使用には注意が必要
basicProxAuthプロキシのベーシック認証用
oAuth1OAuth1.0用のオプション
oAuth2Bearerベアラー認証用のオプション
oAuth2TokenOAuth2.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]