4日目に出てきた Semigroup は関数型プログラミングの定番で、色んな所に出てくる。
import cats._, cats.syntax.all._
List(1, 2, 3) |+| List(4, 5, 6)
// res0: List[Int] = List(1, 2, 3, 4, 5, 6)
"one" |+| "two"
// res1: String = "onetwo"
似たもので SemigroupK
という型コンストラクタ F[_]
のための型クラスがある。
@typeclass trait SemigroupK[F[_]] { self =>
/**
* Combine two F[A] values.
*/
@simulacrum.op("<+>", alias = true)
def combineK[A](x: F[A], y: F[A]): F[A]
/**
* Given a type A, create a concrete Semigroup[F[A]].
*/
def algebra[A]: Semigroup[F[A]] =
new Semigroup[F[A]] {
def combine(x: F[A], y: F[A]): F[A] = self.combineK(x, y)
}
}
これは combineK
演算子とシンボルを使ったエイリアスである <+>
をを可能とする。使ってみる。
List(1, 2, 3) <+> List(4, 5, 6)
// res2: List[Int] = List(1, 2, 3, 4, 5, 6)
Semigroup
と違って、SemigroupK
は F[_]
の型パラメータが何であっても動作する。
Option[A]
は型パラメータ A
が Semigroup
である時に限って Option[A]
も Semigroup
を形成する。そこで Semigroup
を形成しないデータ型を定義して邪魔してみよう:
case class Foo(x: String)
これはうまくいかない:
Foo("x").some |+| Foo("y").some
// error: value |+| is not a member of Option[repl.MdocSession.App.Foo]
// Foo("x").some |+| Foo("y").some
// ^^^^^^^^^^^^^^^^^
だけど、これは大丈夫:
Foo("x").some <+> Foo("y").some
// res4: Option[Foo] = Some(value = Foo(x = "x"))
この 2つの型クラスの振る舞いは微妙に異なるので注意が必要だ。
1.some |+| 2.some
// res5: Option[Int] = Some(value = 3)
1.some <+> 2.some
// res6: Option[Int] = Some(value = 1)
Semigroup
は Option
の中身の値もつなげるが、SemigroupK
の方は最初の選択する。
trait SemigroupKLaws[F[_]] {
implicit def F: SemigroupK[F]
def semigroupKAssociative[A](a: F[A], b: F[A], c: F[A]): IsEq[F[A]] =
F.combineK(F.combineK(a, b), c) <-> F.combineK(a, F.combineK(b, c))
}