MicroAd Advent Calendar 2023 の21日目の記事です。
Scala with Cats を読んだので学習した内容のメモを残します。
1章のIntroductionです。
はじめに
Catsには様々な関数型プログラミングのツールが含まれているが、それらの大半は既存のScalaの型に適用できる型クラスで提供される。
型クラスはHaskellで使われ始めたプログラミングパターンで、これによって継承を用いずに既存のコードの機能拡張が行える。
用語
※ クラスやインスタンスはオブジェクト指向で聞き慣れた言葉だが、意味が異なるので注意
型クラス
実装したい機能を表現するインタフェースやAPIのこと
Scalaでは1つ以上の型パラメータを持つtraitで表される
trait Show[A] { def show(value: A): Unit }
例えば上記で定義したShowはコンソール出力を生成するメカニズムを提供する型クラスで、型パラメータAを持つ
型クラスのインスタンス
型クラスの実装を提供する
Scalaではimplicit valueでインスタンスを定義する
implicit val showIntInstance: Show[Int] = new Show[Int] { def show(value: Int): Unit = println(value.toString) }
既存のクラスにShowの機能がほしい場合も継承ではなく、型クラスのインスタンスを作成し拡張できる
case class Person(name: String, age: Int) implicit val showPersonInstance: Show[Person] = new Show[Person] { def show(value: Person): Unit = println(s"name=${value.name}, age=${value.age}") }
型クラスの使用
型クラスの使用は動作するために型クラスのインスタンスを必要とする機能のこと
Scalaでは型クラスのインスタンスをimplicitパラメータとして受け取るメソッドで定義
2つの方法がある
方法1: Interface Objects
シングルトンオブジェクトにメソッドを置く方法
object ShowObject { def showValue[A](value: A)(implicit show: Show[A]): Unit = show.show(value) }
実行する場合には型クラスのインスタンスが必要になる
import ShowObject._ showValue(1) // showIntInstance showValue(Person("a", 1)) // showPersonInstance // showValue(100L) // 対応するインスタンスがないためコンパイルエラー
方法2: Interface Syntax
implicit classで実装する方法
Catsではシンタックスと呼ばれる
object ShowSyntax { implicit class ShowOps[A](value: A) { def showValue(implicit show: Show[A]): Unit = show.show(value) } }
必要なインスタンスをインポートして実行する
import ShowSyntax._ 1.showValue // showIntInstance Person("Bob", 30).showValue // showPersonInstance // 100L.showValue // 対応するインスタンスがないためコンパイルエラー
Catsのパッケージ構成
型クラス
Catsの型クラスはcats
パッケージで定義されている
import cats.Show
インスタンス
cats.instances
パッケージにインスタンスが定義されている
import cats.instances.int import cats.instances.string import cats.instances.list ...
シンタックス
cats.syntax
パッケージからシンタックスをインポートできる
import cats.syntax.show._
全部
実は個別にインポートしなくても
import cats._ import cats.implicits._
で標準的なインスタンスとシンタックスを一気にインポートできる
変位
変位指定は複合型の間の継承関係とそれらの型パラメータ間の継承関係の相関です。
trait Foo[A] // Invariance(非変) trait Bar[+A] // Covariance(共変) trait Baz[-A] // Contravariance(反変)
Covariance(共変)
BがAのサブタイプであるとき、型F[B]がF[A]のサブタイプである
+
記号を使って共変を表す
trait F[+A]
ScalaではListやOptionが共変で定義されている
trait List[+A] trait Option[+A]
共変を使えば、ある型のコレクションをそのサブタイプのコレクションで代用することができる
seald trait Animal case class Dog(name: String) extends Animal val dogs: List[Dog] = List(Dog("A"), Dog("B")) val animals: List[Animal] = dogs
一般的には共変は出力、つまりメソッドの戻り値などに使われる。
Contravariance(反変)
AがBのサブタイプであるとき、型F[B]がF[A]のサブタイプである
-
記号を使って反変を表す
trait F[-A]
反変は型クラスJsonWriterのように、入力を表す型をモデリングする
trait JsonWriter[-A] { def write(value: A): Json } val shape: Shape = ??? val circle: Circle = ??? val shapeWriter: JsonWriter[Shape] = ??? val circleWriter: JsonWriter[Circle] = ??? def format[A](value: A, writer: JsonWriter[A]): Json = writer.write(value)
どの値とどのJsonWriterの組み合わせをformat関数に渡すことができるか?
全てのCircleはShapeなので、CircleはJsonWriter[Shape]とJsonWriter[Circle]のどちらを使ってもwriteが可能
一方、全てのShapeがCircleではないので、JsonWriter[Circle]でShapeをwriteできない
format(shape, shapeWriter)
// format(shape, circleWriter) // compile error
format(circle, shapeWriter)
format(circle, circleWriter)
Invariance(非変)
共変でも反変でもない。Scalaでは標準で非変になる。
trait F[A]
インスタンスの選択
型クラスを扱う際に、インスタンスの選択を行ううえで2つの問題がある
これらの質問に対する回答は変位ごとに異なる
型クラスの変位 | 質問A | 質問B |
---|---|---|
非変 | No | No |
共変 | No | Yes |
反変 | Yes | No |
さいごに
2章以降では1章で紹介されたShowやEqなどよりもさらに抽象度が高い型クラスである Semigroup、Monoid、Functor、Monad、Semigroupal、Applicative、Traverseなどが説明されています。そのあたりも学習した内容をまとめていきたいです(余裕があれば)。