読者です 読者をやめる 読者になる 読者になる

セカイノカタチ

世界のカタチを探求するブログ。関数型言語に興味があり、HaskellやScalaを勉強中。最近はカメラの話題も多め

マーブルワーズ

http-client-wrapperをStateモナドに魔改造した

/* 注意!! この記事は、Scalaのプログラミングについて書かれています */

麗しき、@ponkotuy氏の作ったhttp-client-wrapperを改造して、Stateモナドを利用してみました。

使い勝手は、むしろ悪化した模様。(^^;

元プログラムの解説記事はこちらです。

qiita.com

モチベーション

元記事にもありますが、グローバルな変数をimplicitで引き渡すような設計となっていたので、これをなんとかしたいと思いました。

あと、request -> (response, session); request -> (response, session)という構造が見えたので、Stateパターンだなという直感が働き、実装してみたかったというのが一番の動機です。

実装してみる

Stateモナドとは、a -> (s, b)というパターンをモナドを使って数珠つなぎにしていく、設計パターンです。

Scalaの標準ライブラリには装備されていないので、scalazを利用しました。

scalazを使うのは、実は今回初めてで、ついにscalazデビューかと感慨深いです。(^^;;

実装に際しては、横田さんのこちらのページを参考にしました(厳密にはscalazでは無いですけど)。

猫番 — State データ型

前回のリクエストのセッションを受け取り、次のリクエストを発行して、更新されたセッションを返すという処理をGET, POST, PUTなどのメソッド分用意する感じになりました。

核となる部分は、下記の部分で、Stateクラスのインスタンスを生成しています。

  private def buildRequest(method: Method, url: String)(f: Request => Request): State[Session, Response] = State[Session, Response] {
    case SomeSession(cookie, lastRes, lastReq, lastUrl) =>
      val next = nextUrl(url, lastUrl)
      val req = f(Request(next.toString)).header("Cookie", cookie.toString)
      val newSession = invokeRequest(method, req, next)
      (newSession, newSession.lastRes)
    case EmptySession =>
      val init = initUrl(url)
      val req = f(Request(init))
      val newSession = invokeRequest(method, req, new URL(init))
      (newSession, newSession.lastRes)
  } 

Stateクラスは、コンストラクタに関数を受け取り、これが、Session => (Session, Response)という型を持つことになります。

地味に、初回はセッションがないので、case式にて場合分けして、2パターン書いています。

ちょっとゴミゴミしているところが残念なポイントです。

ソースコードは、下記にあります。

GitHub - qtamaki/http-client-state: The Skinny's HTTP Client Wrapper on CLI.

元のコードを9割型書き換えてしまった上に、使い勝手の向上もなく、まったく別物になってしまったため、プロジェクト名を変更しました。(^^;

結果

一応、当初の目的であった、グローバルなスコープを持つ変数や引き回しのためのimplicitを排除することはできました。

処理側は、インスタンス変数を持たず(というかobject)、データ型としてのcaseクラスが値を持つだけになっています。

モナドなので、forを使って処理を合成することもできます(そうしないとsessionを引き回せない)。

「ちょっとした検証にREPLでHttpClientを使う」という当初の用途からは後退した気がしますが、取り急ぎこんな感じでやってみました。(^^;