LoginSignup
3

More than 3 years have passed since last update.

ページネーションを利用したAPIと再帰関数の相性が抜群だった

Last updated at Posted at 2019-05-16

はじめに

概要

しばらく前に再帰関数の実装方法について解説しました.
1. Scalaハンズオンで学んだ関数型プログラミング
2. Scalaによる末尾再帰を使った 関数型プログラミング

フィボナッチ数列を例に上げていたのですが, 実際そんなん書かないでしょというイマイチな例で解説をしていました. しかし, 最近ページネーションを利用したAPIを使う際に, 再帰関数を使うとスッキリ書けることがわかったので, また再帰関数を推しておこうかと思います.

この記事では, Googleカレンダーから予定を取得するプログラムを例に説明します.

補足. そもそもページネーション利用したAPI

一覧取得するためのエンドポイントにアクセスしたときに, 一度にすべてのリソースを返却するのが見慣れたAPIの仕様. 少なくとも私は見慣れている.

ところがページネーションを利用したAPIは, 予め決められた一定件数のリソースだけを返却する.
そのかわり, 次のリソースにアクセスするためのトークンを一緒に返却してくれる.
このトークンを使って再びリクエストを送ると, また一定件数の1回目のリクエストとは異なるリソースが返却される.
このようなAPIをここでは取り扱います.

Googleカレンダーの予定を取得するプログラム

GoogleCalendarAPIがまさにページネーションを使用したAPIだったので, これを使って説明してみる.

実現したいこと

特定のカレンダーに登録されている指定期間内の予定を取得する.

GoogleCalendarAPIの仕様

Google Calendar API Events: list

  • 1回のリクエストで取得できる予定の数は決まっている。
  • レスポンスから取得できるnextPageTokenを次のリクエスト時に使うと、重複なく予定を取得できる。
  • レスポンスから取得できるnextPageTokennullであることは、検索結果をすべて取得できたことを表す。

考え方

  1. GoogleCalendarAPIを叩く
  2. nextPageTokenがnullだったらこれまで取得したイベントのリストと, 1.で取得したイベントを結合して返却.
  3. そうでなければ, 再帰関数を再度呼び出す.

コード

private val jsonFactory = JacksonFactory.getDefaultInstance
private val httpTransport = GoogleNetHttpTransport.newTrustedTransport()
private val scopes = Collections.singletonList(CalendarScopes.CALENDAR_EVENTS)
private val applicationName = ""

def getEventsByCalendarIdAndDuration(calendarId: String, duration: Duration): Try[Seq[Event]] = {
  val credential = GoogleCredential
    .fromStream(this.getClass.getResourceAsStream("/token/source-owner.json"))
    .createScoped(scopes)

  val service = new Calendar.Builder(httpTransport, jsonFactory, credential)
    .setApplicationName(applicationName)
    .build()

  @scala.annotation.tailrec
  def extractEventsRecursive(accum: Seq[Event] = Nil, calendarId: String, service: Calendar, timeMin: DateTime, timeMax: DateTime, maybePageToken: Option[String] = None): Try[Seq[Event]] = {
    val maybeResponse = Try(service.events()
      .list(calendarId)
      .setTimeMin(timeMin)
      .setTimeMax(timeMax)
      .setPageToken(maybePageToken.orNull)
      .execute())

    maybeResponse match {
      case Success(response) if response.getNextPageToken == null => Success(accum ++ response.getItems.asScala.map(Event))
      case Success(response) if response.getNextPageToken != null =>
        extractEventsRecursive(accum ++ response.getItems.asScala.map(Event), calendarId, service, timeMin, timeMax, Option(response.getNextPageToken))
      case Failure(exception) => Failure(exception)
    }
  }
  extractEventsRecursive(calendarId = calendarId, service = service, timeMin = duration.start.toGoogleDateTime, timeMax = duration.end.toGoogleDateTime)
}

implicit class GoogleDateTime(date: Date) {
  def toGoogleDateTime = new DateTime(date.value.atStartOfDay(ZoneId.systemDefault()).toInstant.toEpochMilli)
}

全然関係ないところだけど迷った点

1. 再帰関数内で使う変数って, 引数だけにしたほうがいいの?

上のコード例だと再帰関数内で使用する変数はすべて関数の仮引数として宣言したものです.
でも関数実行毎に変わらない変数(timeMinとかtimeMax)は, 引数として指定する必要ないんじゃないの?と思ったりもした.
こういうコードでもコンパイルは通る. でも同じ引数だったら同じ結果が返るという関数の性質からは離れたように思える.

def getEventsByCalendarIdAndDuration(calendarId: String, duration: Duration): Try[Seq[Event]] = {
  val credential = GoogleCredential
    .fromStream(this.getClass.getResourceAsStream("/token/source-owner.json"))
    .createScoped(scopes)

  val service = new Calendar.Builder(httpTransport, jsonFactory, credential)
    .setApplicationName(applicationName)
    .build()

  @scala.annotation.tailrec
  def extractEventsRecursive(accum: Seq[Event] = Nil, maybePageToken: Option[String] = None): Try[Seq[Event]] = {
    val maybeResponse = Try(service.events()
      .list(calendarId)
      .setTimeMin(duration.start.toGoogleDateTime)
      .setTimeMax(duration.end.toGoogleDateTime)
      .setPageToken(maybePageToken.orNull)
      .execute())

    maybeResponse match {
      case Success(response) if response.getNextPageToken == null => Success(accum ++ response.getItems.asScala.map(Event))
      case Success(response) if response.getNextPageToken != null =>
        extractEventsRecursive(accum ++ response.getItems.asScala.map(Event), calendarId, service, timeMin, timeMax, Option(response.getNextPageToken))
      case Failure(exception) => Failure(exception)
    }
  }
  extractEventsRecursive()
}

2. どうせ引数だけで表現しないなら, 無限リスト使っていい感じにできないかしら

全然わからなくて挫折しました…ごめんなさい

と思ったら!
とっても優しい @yasuabe2613 さんが記事にしてくださいました.
興味のある方はぜひ見てみてください!
Web API のページネーションへの FS2 Stream の応用

さいごに

ページネーションを利用したAPIは, GoogleCalendarAPIが初めてで, なんだこれwと思いました. 公式のサンプルコードがJavaのdo~while文で書いてあり, Scalaでどう表現すればいいかなと考えたところ再帰関数に落ち着きました. 最近使っているGitHubのAPIもページネーションを利用しているものなので, 実装の選択肢が増えてよかったと個人的に思っています.

怖がらずに再帰関数を使ってみてください.

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3