軽い気持ちでScalazを使う
Aizu Advent Calendar 4日目です!
前はhimaaaattiさんのGoogle Summer of Code 応募について
はじめに
こわいひとはみないで
Scalaz
ScalazはScalaで関数型プログラミングをするときの補助となるようなライブラリです。
Scalaz is a Scala library for functional programming. It provides purely functional data structures to complement those from the Scala standard library. It defines a set of foundational type classes (e.g. Functor, Monad) and corresponding instances for a large number of data structures.
FunctorとかMonadとか出てきて若干怖めですが、ガリガリに使わなければ難しい概念を覚えなくても便利に使うことができます。
型合わない問題
Scalaのfor式は特定の条件を満たす型に対して使える便利構文です。
// Future[A]は成功したらAを保持するような非同期処理を扱う型 def findTodos(): Future[Seq[Todo]] = ??? // 終了したTodoを取得する val finishedTodosF: Future[Seq[Todo]] = for ( todos <- findTodos(); finishedTodos <- todos.filter(_.isFinished) ) yield finishedTodos // これと同じ findTodos().map(_.filter(_.isFinished))
単純な処理だと問題ありませんが、ちょっと複雑になると困ることがあります。
case class Todo(id: TodoId, body: TodoBody, assigneeId: MemberId) case class Member(id: MemberId, name: MemberName) def findTodoById(id: TodoId): Future[Option[Todo]] = ??? def findMemberById(id: MemberId): Future[Option[Member]] = ??? // TodoIdから、そのTodoにアサインされたMemberを取得する。 def findAssigneeMember(id: TodoId) = for ( todo <- findTodoById(id); assigneeMember <- todo.map(t => findMemberById(t.assigneeId)) ) yield assigneeMember
このfor式はコンパイルが通りません。この場合、for式内の右辺値の型はFuture[A]
でないといけませんが、assigneeMember <- todo.map(t => findMemberById(t.assigneeId))
の型はOption[Future[Option[Member]]]
になっています。
todoがNoneだったらFuture.failureを返すようにすると型は合うようになります。
def findAssigneeMember_(todoId: TodoId): Future[Option[Member]] = for ( todo <- findTodoById(todoId); assignedMember <- todo.map(t => findMemberById(t.assigneeId)).getOrElse(Future.failed(new RuntimeException(s"Todo ${todoId.value} not found!"))) ) yield assignedMember
しかしよく見ると、本来findTodoById()
がNone
を返した時はFuture
は成功してその上でNone
を返して欲しいのに、これだとFuture
自体が失敗してしまうので意味が変わってしまいます。
意味は変えずに型を合わせたい...
Option[Future[Option[Member]]]
をFuture[Option[Member]]
にしたい...
これを解決するsequence
という関数がScalazには定義されています。
sequenceってなに
sequence
は特定の条件を満たすA[_]
とB[_]
について、A[B[C]]
をB[A[C]]
に変換します。
どんな仕組みやねん。
このsequence
はtraverse
という関数の用途を限定したバージョンなので、traverse
の定義を先に見てみます。
trait Traverse[F[_]] extends Functor[F] with Foldable[F] { self => // ... def traverse[G[_]:Applicative,A,B](fa: F[A])(f: A => G[B]): G[F[B]] = ... // ... }
このFunctor[F]
、Foldable[F]
、G[_]: Applicative
が特定の条件です。雑に説明すると、
Functor[F]
: Fはmapができる。Foldable[F]
: Fはfold(reduce)ができる。G[_]: Applicative
: GはGに包まれた関数にGに包まれた値を適用できる。
Applicative
だけ補足します。さっきの例でも使ったOption
は実はApplicative
な型でもあります。なぜかと言うと、
// (1: Int).someはSome(1): Option[Int]と同じ 5.some <*> (4.some <*> {(a: Int, b: Int) => a + b}.curried.some) // => Some(9)
こんな感じで、Option
な値をOption
な関数に適用できるからです。こういう性質の実装を持つ(持てる)型をApplicative
1型と言います。
なのでtraverse
は、A
がFunctor
かつFoldable
でB
がApplicative
の時、A[X]
とX => B[Y]
な関数を受け取って、B[A[X]]
を返す関数でした。
例としてList
のtreverse
の実装を見てみます。
def traverseImpl[F[_], A, B](l: List[A])(f: A => F[B])(implicit F: Applicative[F]) = {
l.reverse.foldLeft(F.point(Nil: List[B])) { (flb: F[List[B]], a: A) =>
F.apply2(f(a), flb)(_ :: _)
}
}
また雑に説明すると、
- リスト
l
と、リストの要素をApplicative
な値に変換する関数f
を引数にとる l
を反転する。l
の要素を先頭から(本来の末尾から)f
でApplicative
な値に変換し、Applicative
な関数を使ってApplicative
なリストの先頭に繋げる。
...シミュレーションしてみます。
// Some(List(6, 7, 8))を期待 List(1, 2, 3).traverse(i => Some(i + 5): Option[Int]) // 1. リストを反転 List(3, 2, 1) // 2. foldの初期値はSome(List()) Some(List()) // 3. 先頭の要素 3 にfを適用してApplicativeな値に変換 f(3) == Some(8) // 4. Applicativeな関数を使ってApplicativeなリストの先頭に繋げる {_ :: _} ---Applicativeに---> Some({_ :: _}) Some({_ :: _}) ---適用---> (Some(8), Some(List())) => Some(List(8)) // 5. 3と4を全ての要素分回す Some(List(6, 7, 8))
こうして見ると、traverse
はmap
に似てる気もします。
さて、traverse
がなんとなく分かったところでsequence
の定義を見てみます。
/** Traverse with the identity function. */ def sequence[G[_]:Applicative,A](fga: F[G[A]]): G[F[A]] = traverse(fga)(ga => ga)
コメントにあるように、sequence
はtraverse
の引数f
をidentity function、つまり何もせずにそのまま返す関数にしたものです。
sequence
の呼び出しもシミュレーションしてみます。
// Some(List(1, 2, 3))を期待 List(Some(1), Some(2), Some(3)).sequence // 1. リストを反転 List(Some(3), Some(2), Some(1)) // 2. foldの初期値はSome(List()) Some(List()) // 3. 先頭の要素 Some(3) にfを適用してApplicativeな値に変換 f(Some(3)) == Some(3) // 4. Applicativeな関数を使ってApplicativeなリストの先頭に繋げる {_ :: _} ---Applicativeに---> Some({_ :: _}) Some({_ :: _}) ---適用---> (Some(3), Some(List())) => Some(List(3)) // 5. 3と4を全ての要素分回す Some(List(1, 2, 3))
List[Option[Int]]
をOption[List[Int]]
に変換できました!
簡単か?
冒頭で、「ガリガリに使わなければ難しい概念を覚えなくても便利に使うことができます。」と言いました。
正直、sequence
の実装を理解した上で使うのは簡単ではないと思います。
そこでsequence
の振る舞いだけ見てみましょう。sequence
はA[B[C]]
をB[A[C]]
に変換できる不思議な関数です。
これを使ってさっきの問題を解決してみます。
def findAssigneeMember(id: TodoId) = for ( todo <- findTodoById(id); assigneeMember <- todo.map(t => findMemberById(t.assigneeId)).sequence.map(_.flatten) ) yield assigneeMember
ただ使うだけならFunctor
とかApplicative
とかどうでもいいですね!
しかし、注意しないといけないのはA
はFunctor
かつFoldable
、B
はApplicative
でないといけないということです。適当に作った型ではsequence
は使えません。
なので、使うだけなら難しい概念は考えなくても良いものの、何に使えて何に使えないかを知るには頑張りが必要です。
おわりに
Scalazには関数型プログラミングの特性を使った便利な関数がたくさん入っています。ゴリゴリに使いこなすまでいかなくても今回のsequence
のように問題の解決策として使うと幸せになれるのではないでしょうか。
-
本当はこれはApplyの説明です。Applicativeの場合はもう一つ性質が必要です。↩