13
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Scala with Catsを日本語で要約してみた ~第1章 イントロダクション~

Last updated at Posted at 2019-09-19

概要

  • Scala with Cats という英語の本を読んでCatsについて学習しており、内容を忘れないように日本語でまとめました。

Type Class(型クラス)

Type Classとは?

  • 型クラスは実装したい機能を表すインターフェースまたはAPIのこと
  • Catsでは型クラスは最低1つの型パラメータをもつtraitとして表現される
sealed trait Json
final case class JsObject(get: Map[String, Json]) extends Json
final case class JsString(get: String) extends Json
final case class JsNumber(get: Double) extends Json
case object JsNull extends Json

trait JsonWriter[A] {
  def write(value: A): Json
}

Type Class Instances(型クラスインスタンス)

  • 型クラスインスタンスは型の実装を提供する
  • Scalaでは型クラスの具体的な実装を作成し、implicitでタグ付けすることでインスタンスを定義する

final case class Person(name: String, email: String)

object JsonWriterInstances {
  implicit val stringWriter: JsonWriter[String] =
    new JsonWriter[String] {
      def write(value: String): Json = 
        JsString(value)
    }
    
  implicit val personWriter: JsonWriter[Person] =
    new JsonWriter[Person] {
      def write(value: Person): Json =
        JsObject(Map(
          "name" -> JsString(value.name),
          "email" -> JsString(value.email)
        ))
    }
}

Type Class Interfaces(型クラスインターフェース)

  • 型クラスインターフェースはユーザーに公開するあらゆる機能のこと
  • インターフェースはimplicit parameterとして型クラスインスタンスを受け取るメソッド
  • インターフェースを指定する方法には2種類ある
    • Interface Objects
    • Interface Syntax

Interface Objects

object Json {
  def toJson[A](value: A)(implicit w: JsonWriter[A]): Json = w.write(value)
}

// 呼び出し側
import JsonWriterInstances._

Json.toJson(Person("Dave", "dave@example.com"))

Interface Syntax

  • extension methods(拡張メソッド)を使うことで既存の型をインターフェースメソッドで拡張することができる
  • Catsではこれを型クラスの構文(Syntax)と呼んでいる
object JsonSyntax {
  implicit class JsonWriterOps[A](value: A) {
    def toJson(implicit w: JsonWriter[A]): Json = w.write(value)
  }
}

// 呼び出し側
import JsonWriterInstances._
import JsonSyntax._

Person("dave", "dave@example.com").toJson

implicitly Method

  • Scalaでは汎用型クラスインターフェースとしてimplicitlyというものを標準ライブラリで提供している
def implicitly[A](implicit value: A): A = value
  • implicitのスコープから任意の値を呼び出すために暗黙的に使用できる
import JsonWriterInstances._

implicitly[JsonWriter[String]] // JsonWriter[String]型のJsonWriterInsntacesが返る

Implicits

  • 型クラスを扱うにはimplicit valueとimplicit parametersを扱う必要がある

Implicit Scope

  • コンパイラは以下の範囲を暗黙的に探す
    • ローカルもしくは継承された定義
    • importされた定義
    • 型クラスまたはパラメータ型のコンパニオンオブジェクト
  • コンパイラはimplicitが複数あるとコンパイルエラーを吐く
implicit val writer1: JsonWriter[String] = JsonWriterInstances.stringWriter
implicit val writer2: JsonWriter[String] = JsonWriterInstances.stringWriter

Json.toJson("A string")
// <console>:23: error: ambiguous implicit values:
// both value stringWriter in object JsonWriterInstances of type => JsonWriter[String]
// and value writer1 of type => JsonWriter[String]
// match expected type JsonWriter[String]
// Json.toJson("A string")
//            ^
  • implicitなものをどこに定義するかはおおよそ4つの方法に分けられる
    1. JsonWrietrInstancesなどのオブジェクトに配置する => importすることでスコープに入る
    2. traitに入れる => 継承するとスコープに入る
    3. 型クラスのコンパニオンオブジェクトに入れる => 常にスコープに入る
    4. パラメータ型のコンパニオンオブジェクトに入れる => 常にスコープに入る

Recursive Implicit Resolution

  • 型クラスのインスタンスを定義する方法は2つ
    • implicit valを使う
    • implicit defを使う
  • ただimplicit valではスケールしない時がある
  • 例としてJsonWriter[Option[A]]を定義するとすると、全ての型に対してimplicit valを作らないといけなくなる
implicit val optionIntWriter: JsonWriter[Option[Int]] = ???

implicit val optionPersonWriter: JsonWriter[Option[Person]] = ???

// 他にもたくさん必要。。
  • implicit defを使うことでOption[A]を抽象的に定義することができる
implicit def optionWriter[A](implicit writer: JsonWriter[A]): JsonWriter[Option[A]] = 
  new JsonWriter[Option[A]] {
    def write(option: Option[A]): Json = option match {
      case Some(aValue) => writer.write(aValue)
      case None => JsNull
    }
  }

Meets Cats

  • Catsにおける型クラス
  • ShowはCatsにおけるPrintableと同等の型クラス
    • (注) PrintableとはScala with Catsに出てくる練習問題用のクラス
package cats

trait Show[A] {
  def show(value: A): String
}

Importing Type Class

  • 型クラスそのもの(ここでいうShow)と実装をもつ型クラスインスタンスをインポートする必要がある

Importing Default Instances

  • cats.instancesパッケージにデフォルト(組み込み型)のインスタンスがあるので、これらをインポートする
    • ex.
    • cats.instances.int
    • cats.instances.list
    • cats.instances.option
import cats.instances.int._    
import cats.instances.string._ 

val showInt: Show[Int] = Show.apply[Int] 
val showString: Show[String] = Show.apply[String]

val intAsString: String = showInt.show(123) // "123"
val stringAsString: String = showString.show("abc") // "abc"

Importing Interface Syntax

  • 型クラスの構文(Syntax)はcats.syntax パッケージにある
import cats.syntax.show._ 

val shownInt = 123.show // "123"
val shownString = "abc".show // "abc"

Importing All The Things!

  • 全てインポートするには以下のようにする
import cats._
import cats.implicits._

Defining Custom Instances

  • デフォルトインスタンスに無いクラスはカスタムのインスタンスを作ることでShowを使うことができる
import java.util.Date

implicit val dateShow: Show[Date] =
  new Show[Date] {
    def show(date: Date): String =
      s"${date.getTime}ms since the epoch."
}
  • 上記のようにもできるがCatsでは簡単にカスタムインスタンスを作るためのメソッドがすでに準備されている
  • Showのコンパニオンオブジェクトには2つのコンストラクションメソッドがあるので、それを使うことでカスタムインスタンスを作ることができる
object Show {
  // Convert a function to a `Show` instance:
  def show[A](f: A => String): Show[A] = ???
  
  // Create a `Show` instance from a `toString` method:
  def fromToString[A]: Show[A] = ???
}
  • 上記のコンストラクションメソッドを使うと以下のようになる
implicit val dateShow: Show[Date] = Show.show(date => s"${date.getTime}ms since the epoch.")

Controlling Instance Selection

  • 型クラスを使う上で考えなければならないインスタンス選定(Instance Selection)の2つの課題がある
    • インスタンスとして定義した型とその派生型(subtype)の関係性
      • ex. JsonWriter[Option[Int]] を定義した場合、Json.toJson(Some(1)) が選択される(SomeはOptionの派生型(subtype))
    • たくさんの選択肢がある中でどのように型クラスインスタンスを選択するか
      • ex. JsonWriterに対して2つのPerson型を定義した時、どちらが選択されるか

Variance(変位)

  • 型クラスを定義する時、変位注釈(変位アノテーション)をつけることで、コンパイラが暗黙的にインスタンス選定(instance selection)できるようになる

Covariance(共変)

  • 型パラメータに+をつけることで共変になる
  • 共変とは、BAの派生型(subtype)の場合、F[B]F[A]も派生型(subtype)であるという性質
  • 共変の場合、F[A] = F[B]という代入ができるようになる
  • ListやOptionは共変の1つの例
trait List[+A]
trait Option[+A]
  • Listは共変なので、Aの派生型(subtype)であれば、代入することができる
sealed trait Shape
case class Circle(redius: Double) extends Shape

val circles: List[Circle] = ???
val shapes: List[Shape] = circles

Contracariance(反変)

  • 型パラメータに-をつけることで反変になる
  • 反変とは、ABの派生型の場合、F[B]F[A]の派生型であるという性質
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とShape, Circleクラスがあった場合、反変の性質により、JsonWriter[Circle]の場所でshapeWriterが使えるようになる

Invariance(非変)

  • +-もつかない
  • 非変の場合F[A]F[B]はどちらもどちらの派生型ではないことを表す

どのインスタンスが使われるか

sealed trait A
final case object B extends A
final case object C extends A
  • 上記のような代数的データ型があった場合、インスタンスとしてsuper typeが使われるか、sub typeが使われるかは変位指定によって異なる
- 非変 共変 反変
super typeのインスタンスが使用される No No Yes
より詳細な型が好まれる No Yes No

参考資料

続き

Scala with Catsを日本語で要約してみた ~第2章 Monoids and Semigroups~

13
10
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?