Scala with Cats学習メモ:Introduction

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._

で標準的なインスタンスシンタックスを一気にインポートできる

変位

変位指定は複合型の間の継承関係とそれらの型パラメータ間の継承関係の相関です。

変位指定 | Scala Documentation

trait Foo[A]  // Invariance(非変)
trait Bar[+A] // Covariance(共変)
trait Baz[-A] // Contravariance(反変)

Covariance(共変)

BがAのサブタイプであるとき、型F[B]がF[A]のサブタイプである

covariance

+記号を使って共変を表す

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]のサブタイプである

contravariance

-記号を使って反変を表す

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などが説明されています。そのあたりも学習した内容をまとめていきたいです(余裕があれば)。