今日も今日とてScala with Catsを読んでいく。
Semigroupal and Applicative
- Semigroupal
- コンテキストのペアを構成する概念を内包する
- Catsは
Semigroupal
とFunctor
を利用して、複数の引数を持つ関数のシーケンスを可能にするcats.syntax.apply
モジュールを提供する
- Parallel
Parallel
はモナドインスタンスを持つ型をSemigroupal
インスタンスと関連する型に変換する
- Applicative
Applicative
はSemigroupal
とFunctor
を継承する
- コンテキスト内のパラメータに関数を適用する方法を提供する
Semigroupal
cats.Semigroupal
はコンテキストを組み合わせることができる型クラスである。
もし、F[A]
とF[B]
という2つの型のオブジェクトがあったら、Semigroupa[F]
はF[(A, B)]
のような形に組み合わせることできる。
trait Semigroupal[F[_]] {
def product[A, B](fa: F[A], fb: F[B]): F[(A, B)]
}
Optionを例として以下の実行結果を見てみると分かるように、両方の値がSomeの場合はタプルで値を返すが、一方がNoneの場合は結果はNoneとなる。
println(Semigroupal[Option].product(Some("abc"), Some(123)))
println(Semigroupal[Option].product(Some("abc"), None))
Semigroupal Laws
Semigroupalの法則は一つで、product
メソッドが結合法則を満たしていることである。
product(a, product(b, c) == product(product(a, b), c)
Apply Syntax
CatsはSemigroupalのメソッドショートハンドであるtupled
やmapN
などの便利なapply syntax
を提供している。
Semigroupal Applied to Different Types
Semigroupal
はいつも期待するような振る舞いはしない。特にMonad
インスタンスも持つ型だ。
Future
import cats.syntax.apply._
import scala.concurrent.{Await, Future}
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration.DurationInt
case class Cat(name: String, yearOfBirth: Int, favoriteFoods: List[String])
val futureCat = (Future("Garfield"), Future(1978), Future(List("Lasagne"))).mapN(Cat)
println(Await.result(futureCat, 1.second))
List
SemigroupalのListを組み合わせた場合、いくつか期待しない結果になる。
println(Semigroupal[List].product(List(1, 2), List(3, 4)))
Either
Eitherにproduct
メソッドを適用するとエラーが累積されることを期待するが、結果を見てみると分かるように最初のエラーのみが結果として返されている。
type ErrorOr[A] = Either[Vector[String], A]
println(
Semigroupal[ErrorOr]
.product(Left(Vector("Error 1")), Left(Vector("Error 2")))
)
Semigroupal Applied to Monads
ListやEitherに対するproductメソッドの結果が期待したものと違っている理由は、それらがモナドであるから。
モナドの場合は、product
メソッドの実装は次のようになる。
import cats.Monad
import cats.syntax.functor._
import cats.syntax.flatMap._
def product[F[_]: MOnad, A, B](fa: F[A], fb: F[B]): F[(A, B)] =
fa.flatMap(a => fb.map(b => (a, b)))
product
メソッドの実装によって異なるセマンティクスをもつのはとても奇妙なことだ。
そこで、Catsのモナド(Semigroupalを拡張したもの)は上記のような標準実装を提供している。
Parallel
Paralell型とそれらに関連する構文によって、ある種のモナドの代替的なセマンティクスにアクセスすることができる。
import cats.Semigroupal
import cats.syntax.parallel._
import cats.syntax.apply._
type ErrorOr[A] = Either[Vector[String], A]
val error1: ErrorOr[Int] = Left(Vector("Error 1"))
val error2: ErrorOr[Int] = Left(Vector("Error 2"))
println(Semigroupal[ErrorOr].product(error1, error2))
println((error1, error2).tupled)
println((error1, error2).parTupled)
parTupled
を使うと両方のエラーが返却されるのが分かる。
なぜこのような振る舞いになるのかParallel
の定義を見てみる。
trait Parallel[M[_]] {
type F[_]
def applicative: Applicative[F]
def monad: Monad[M]
def parallel: ~>[M, F]
- M型のモナドインスタンスがある
- 関連するFの型コンストラクタがあり、それはApplicativeインスタンスを持っている
- MからFに変換できる
※ 型コンストラクタ(type-constructor)は、型を引数に取って新しい型を作るもの
~>
というのはFunctionK
のエイリアスで、~>[M, F]
はMからFに変換するということを表す。
FuntionK M ~> F
は、M[A] 型の値から F[A] 型の値への関数である。
import cats.arrow.FunctionK
object optionToList extends FunctionK[Option, List] {
def apply[A](fa: Option[A]): List[A] =
fa match {
case None => List.empty[A]
case Some(a) => List(a)
}
}
optionToList(Some(1))
optionToList(None)
Apply and Applicative
Catsは2つの型クラスを用いてApplicative
をモデル化する。
1つ目はcats.Apply
で、これはSemigroupal
とFunctor
を継承しており、コンテキスト内の関数にパラメータを適用するap
メソッドを追加したもの。
2つ目は、cats.Applicative
で、これはApply
を継承し、pure
メソッドを追加したもの。
trait Apply[F[_]] extends Semigroupal[F] with Functor[F] {
def ap[A, B](ff: F[A => B](fa: F[A]): F[B]
def product[A, B](fa: F[F], fb: F[B]): F[(A, B)] =
ap(map(fa)(a => (b: B) => (a, b)))(fb)
}
trait Applicative[F[_]] extends Apply[F] {
def pure[A](a: A): F[A]
}
モナドの型クラスの階層