Semigroupal and Applicative

今日も今日とてScala with Catsを読んでいく。

Semigroupal and Applicative

  • Semigroupal
    • コンテキストのペアを構成する概念を内包する
    • CatsはSemigroupalFunctorを利用して、複数の引数を持つ関数のシーケンスを可能にするcats.syntax.applyモジュールを提供する
  • Parallel
    • Parallelはモナドインスタンスを持つ型をSemigroupalインスタンスと関連する型に変換する
  • Applicative
    • ApplicativeSemigroupalFunctorを継承する
    • コンテキスト内のパラメータに関数を適用する方法を提供する

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)))
// Some((abc,123))
println(Semigroupal[Option].product(Some("abc"), None))
// None

Semigroupal Laws

Semigroupalの法則は一つで、productメソッドが結合法則を満たしていることである。

product(a, product(b, c) == product(product(a, b), c)

Apply Syntax

CatsはSemigroupalのメソッドショートハンドであるtupledmapNなどの便利な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))
// Cat(Garfield,1978,List(Lasagne))

List

SemigroupalのListを組み合わせた場合、いくつか期待しない結果になる。

println(Semigroupal[List].product(List(1, 2), List(3, 4)))
// List((1,3), (1,4), (2,3), (2,4))

Either

Eitherにproductメソッドを適用するとエラーが累積されることを期待するが、結果を見てみると分かるように最初のエラーのみが結果として返されている。

type ErrorOr[A] = Either[Vector[String], A]

println(
  Semigroupal[ErrorOr]
    .product(Left(Vector("Error 1")), Left(Vector("Error 2")))
)
// Left(Vector(Error 1))

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))
// Left(Vector(Error 1))

println((error1, error2).tupled)
// Left(Vector(Error 1))

println((error1, error2).parTupled)
// Left(Vector(Error 1, Error 2))

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))
// List(1)

optionToList(None)
// List()

Apply and Applicative

Catsは2つの型クラスを用いてApplicativeをモデル化する。

1つ目はcats.Applyで、これはSemigroupalFunctorを継承しており、コンテキスト内の関数にパラメータを適用する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]
}

モナドの型クラスの階層