軽い気持ちでScalazを使う

Aizu Advent Calendar 4日目です!

adventar.org

前は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]]に変換します。

どんな仕組みやねん。

このsequencetraverseという関数の用途を限定したバージョンなので、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な関数に適用できるからです。こういう性質の実装を持つ(持てる)型をApplicative1型と言います。

なのでtraverseは、AFunctorかつFoldableBApplicativeの時、A[X]X => B[Y]な関数を受け取って、B[A[X]]を返す関数でした。

例としてListtreverseの実装を見てみます。

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)(_ :: _)
  }
}

また雑に説明すると、

  1. リストlと、リストの要素をApplicativeな値に変換する関数fを引数にとる
  2. lを反転する。
  3. lの要素を先頭から(本来の末尾から)fApplicativeな値に変換し、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))

こうして見ると、traversemapに似てる気もします。

さて、traverseがなんとなく分かったところでsequenceの定義を見てみます。

/** Traverse with the identity function. */
  def sequence[G[_]:Applicative,A](fga: F[G[A]]): G[F[A]] =
    traverse(fga)(ga => ga)

コメントにあるように、sequencetraverseの引数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の振る舞いだけ見てみましょう。sequenceA[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とかどうでもいいですね!

しかし、注意しないといけないのはAFunctorかつFoldableBApplicativeでないといけないということです。適当に作った型ではsequenceは使えません。

なので、使うだけなら難しい概念は考えなくても良いものの、何に使えて何に使えないかを知るには頑張りが必要です。

おわりに

Scalazには関数型プログラミングの特性を使った便利な関数がたくさん入っています。ゴリゴリに使いこなすまでいかなくても今回のsequenceのように問題の解決策として使うと幸せになれるのではないでしょうか。


  1. 本当はこれはApplyの説明です。Applicativeの場合はもう一つ性質が必要です。