FutureとEitherの話の続き(ApplicativeとMonadの違い)

昨日書いたこれ

ScalaでFutureとEitherを組み合わせたときに綺麗に書く方法

の中で、

実は今回の例だけだと、Applicativeの範囲でいけるので、必ずしもモナドトランスフォーマーは必須ではありません

と書いたところ、以下のような反応をもらったので

例を思いついたので、それも書いておきましょう。丁寧にScalaz初心者向けに説明するの疲れたので、以前よりは雑に要点だけ書きます、ご了承ください。今回も先にコード例のgist貼っておきます。

https://gist.github.com/xuwei-k/051c3b00129b7a0dfcd6

そもそも「Applicativeで範囲よい」「ApplicativeではダメでMonad必要」は、モナドトランスフォーマーは直接関係ない話ですね。それこそ Functional Programming in Scala や、すごいHaskell 本を読むと良いと思います。


さて、一応前回の例を利用しつつ書くとしましょう。お題は

  • 自分のfollowerのfollowerを全員取得
  • ただし、そこから自分自身とfollowerを除く(followerのfollowerが、自身の直接のfollowerだったらそれを含めない。グラフ的に距離が2のものだけを抽出)

とします。メソッド名は適当にfollowerFollowerとしておきます。(酷い名前だ)

もとのfollowersメソッドはそのまま使い、同じくfollowersメソッドだけを使ってfollowerFollowerメソッドを作ります。

まずFuture使わないversion(この残念仕様 https://issues.scala-lang.org/browse/SI-5793 などの関係上、最初からScalazのEither使ってますがご了承ください)

def followerFollower0(userId: UserId): Error \/ List[User] =
  for{
    a <- followers(userId)
    b = a.map(_.id)
    c <- b.traverseU(followersNoFuture)
  } yield c.flatten.distinct.filterNot(user => b.contains(user.id) || user.id == userId)

ポイントは、for式の最初の変数aを、そのすぐ下でもう一回使ってる点です。これだとApplicativeでは不可能です。*1

さて、今回はもう先にEitherTのversion見せましょう。予想ついてる人いるかもしれません?が、結局EitherT使うと、またもやFuture使わないversionと全く同じように書けます、素晴らしいですね

def followerFollower(userId: UserId): EitherT[Future, Error, List[User]] =
  for{
    a <- followers(userId)
    b = a.map(_.id)
    c <- b.traverseU(followers)
  } yield c.flatten.distinct.filterNot(user => b.contains(user.id) || user.id == userId)

さて、じゃあEitherT使わなかったらどう書くの?となりますが、とりあえずこれも2パターンくらい作りました。わりと投げやりなので、もっと綺麗に書く方法あるかもしれません。まぁとにかく長くなって面倒だな―というのを感じてもらえばいいのではないでしょうか

  // Scalazもできるだけ使わないで書くとすると、foldLeftなどしないといけなくて辛い
  def followerFollower1(userId: UserId): Future[Either[Error, List[User]]] = {
    followers(userId).flatMap{
      case Right(a) =>
        val b = a.map(_.id)
        Future.traverse(b)(followers).map{ c =>
          c.foldLeft(Right(Nil): Either[Error, List[User]]){
            case (Right(d), Right(e)) =>
              Right(d ::: e)
            case (e @ Left(_), _) =>
              e
            case (_, e @ Left(_)) =>
              e
          }.right.map{_.filterNot(user => b.contains(user.id) || userId == user.id)}
        }
      case Left(e) =>
        Future.successful(Left(e))
    }
  }

  // scalaz.Traverseなども使って書いたもの。traverseやsequence関数大量でこれも少し辛い。
  // そもそもテストしてないから合ってるのか自信ない。
  // もうちょっと綺麗に書けるというか、書き方かなり色々ある気がする。
  def followerFollower2(userId: UserId): Future[Either[Error, List[User]]] = {
    followers(userId).flatMap{
      case Right(a) =>
        val b = a.map(_.id)
        Future.traverse(b)(followers).map{ x =>
          x.traverse(_.sequenceU).flatten.sequenceU.map(
            _.filterNot(user => b.contains(user.id) || userId == user.id)
          )
        }
      case Left(e) =>
        Future.successful(Left(e))
    }
  }

(上記で使われてるsequenceってなに?と思った人はこれ Scalaで、FutureやOptionやListがネストしてしまったときに、いい感じに変換する方法 とか読んでください)

ただし、(実は前回の例もそうでしたが) 以下のような細かい点をほとんど考慮してなくて

  • エラーが起きた場合にすぐ止まって余計な呼び出ししないか?
  • 色々な効率
  • 並列に呼び出しできるところは、出来る限り並列で呼び出してるか

「とにかくEitherT使うと、Future版とそうでない版が、全く同じで書けるよー」

しか言ってないので、実際に使うとなると色々注意が必要です。




あと、最後完全に余談ですが、この例を一般化して

「followerのfollowerのfollower」や「followの関係において、距離がnのfollower一覧を返す一般化されたメソッド」

を、Scalazをフルに使って書いてみたりすると面白いかもしれません。読者の宿題とします(気が向かない限り、べつにあとで答え書くつもりもない)
以前書いたこれ

ScalazのKleisliとEndomorphic

と同じテクニックか、似たテクニックか、はたまた全く別のテクニックになるのかわかりませんが、抽象化のやりがいがありそう(たぶん)

*1:もっと細かくというか正確にいうと、さらにそのbを下で使ってるのがポイントですが