昨日書いたこれ
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をフルに使って書いてみたりすると面白いかもしれません。読者の宿題とします(気が向かない限り、べつにあとで答え書くつもりもない)
以前書いたこれ
と同じテクニックか、似たテクニックか、はたまた全く別のテクニックになるのかわかりませんが、抽象化のやりがいがありそう(たぶん)
*1:もっと細かくというか正確にいうと、さらにそのbを下で使ってるのがポイントですが