Scala
cats

Cats 入門 その1 (型クラス、Cats概要、基本型クラス)

More than 1 year has passed since last update.


cats 入門


概要

ulgeek アドベントカレンダーの 5日目です。

Advanced Scala with Cats というPDF があり、

Cats を通して型クラスの便利さを体感するのにとっても良い資料だと思いましたので、その内容をベースに型クラスによる FP(Functional Programming) を紹介していきます。

私は FP(Functional Programming) は副作用や状態変更を可能な限り局所化するプログラム方法だと思っています。

端的に言えば IO 処理や変数の再代入といったコードはできないか制約があります。副作用や状態変更を排するとプログラムが他のプログラムの動作に影響を与える可能性が、関数の引数や戻り値に限定されるので再利用性や結合性が高まり、バグやミスが減らせるといわれています。

また、 FP に型をつけることでコンパイル時に多くのエラーを見つけることができます。

Scala の FP ライブラリと言えば scalaz や Cats があります。この記事を通して FP を難しいと思っている人に少しでもFPの便利さを知ってもらえればと思います。

上記で紹介した PDF は Scala コンサルや研修をやっている underscore 社のテキストで、テキストのみ無料で入手できます。

この記事は Scala をある程度わかっている方を対象にしています。

Catsは、バージョン 1.0.0-RC1 で動作確認しています。もうすぐ1.0が出ますね。


型クラス(Type Class) とは

型クラスは、とある型に対して実装しなければいけないメソッドを定義したものでJavaのインターフェースに近いものです。

ただし、従来のインターフェースと異なり定義済みの型に対して後から実装を追加できます。

型クラスのインスタンスとは、型クラスを特定の型に対して実装したものです。

型クラスを第一級で使うプログラムは Haskell, Rust などがあります。

Scala は trait やクラスの継承のほかに implicit を使って型クラスによるプログラムも行えます。

型クラス(Type Class) やインスタンスという単語が何度か登場しました。名前が似ていますが Java などの OOP のクラス・インスタンスとは全く異なるものだと認識しておきましょう。


型クラスと型クラスのインスタンスを作る

次の自作の case class を定義しておきます。

case class Person(name:String, age:Int)

オブジェクトの文字列表現を取得する Show という型クラスを例にとります。

型クラスは、型パラメータを引数に取る trait として宣言します。

trait Show[A] {

def show(a:A):String
}

Show trait を String と、上記の Person それぞれに実装します。

object ShowInstances {

implicit val stringShowInstance: Show[String] = new Show[String] {
override def show(a: String): String = a
}
implicit val personShowInstance: Show[Person] = new Show[Person] {
override def show(a: Person): String = s"${a.name}(${a.age})"
}
}

これが、型クラスについて特定の型に対して実装を定義したものなので、型クラスのインスタンスと呼びます。 implicit 宣言するのがポイントです。


型クラスによるプログラム

型クラスを使用する API は次のように implicit パラメータで型クラスのインスタンスを要求します。

def show[A](a:A)(implicit evidence:Show[A]):String = evidence.show(a)

implicit パラメータは、スコープ内に型が一致する implicit で宣言されたメンバがあればそれを暗黙的にパラメータに設定する仕組みです。

そのため次のようなプログラムができます。

import ShowInstances._ // ShowInstances 内の implicit メンバをインポート


println(show("aaa")) // "aaa" , stringShowInstanceが選択
println(show(Person("name", 18))) // name(18) , personShowInstanceが選択

インスタンスの無い型に対する呼び出しはコンパイルエラーになります。

そのため次のように Int に対する呼び出しはできません。

// Int の Show インスタンスはないので、コンパイルエラー

println(show(1))

型クラスを使うと次のような利点があります。


  • 定義済みの型に機能を追加できる


    • Show だけでなく、様々な型クラスのインスタンスを追加できる



  • 型クラスのインスタンスが存在する場合だけコンパイルが成功する


    • アドホック多相



Javaの場合このような機能を実現するにはアノテーションやリフレクション、あるいはDIコンテナを使うと思います。これらの技術は設定の不備がプログラムの実行時にしかわからないという欠点があり、コンパイル時に不備がわかるというも型クラスの利点の一つです。


型クラスの API を便利に

型クラスを使うAPIをもう少し便利にする方法が2つあります。


インスタンス形式

次のようなインスタンスを取得するためのオブジェクトを用意しておきます。

object Show {

import ShowInstances._
// インスタンスを提供する方法
def apply[A](implicit a: Show[A]) = a
}

すると次のように型パラメータの指定でインスタンスが取得できます。

val personInstance = Show[Person]

personInstance.show(Person("name", 18))

Show[String].show("aaa")

型を指定したインスタンスのメソッドは、指定した型と同じ型のオブジェクトを引数に渡さないとコンパイルエラーになります。


シンタックス形式

シンタックス形式を使うと、型にメソッドがあるかのように見えてコードがすっきり見えます。

次のような implict class を定義します。

object ShowSyntax {

implicit class ShowOps[A](value:A) {
def show(implicit a: Show[A]): String = {
a.show(value)
}
}
}

implicit class はコンストラクタで受け取った元のクラスをラップして、メソッドを追加するような働きをします。

つまり上記の場合 A の型に、 show メソッドを呼び出せるようになります。

しかもそこに型クラスの制約が加わり型クラスのインスタンスが存在する型のみに適用が限定されます。


import ShowSyntax._
import ShowInstances._

"aaa".show
Person("a", 18).show
// 1.show コンパイルエラー!


Cats とは

Catsが提供する主な機能は次のものです


  • Monoidなどの代表的な型クラス



  • 上記型クラスに対する Scala 標準の List や Option のインスタンス

  • 上記インスタンスのインスタンス形式とシンタックス形式のAPIの提供

  • モナドトランスフォーマー、 Stateなど型クラスを使ったライブラリの提供



他にも公式やサードパーティの拡張として Freeモナド、 Lens モナド、 JSON 変換などの機能があります。

公式サイト はドキュメントが非常に充実しているので一度目を通すと良いでしょう。

上記で扱った Show も Cats には含まれていて次のように使えます。

// 型クラスのインポート

import cats.Show
// インスタンスのインポート
import cats.instances.all._ // cats.instances.int._ のように個別の指定も可能。
// syntax 形式APIのインポート
import cats.syntax.show._ // cats.syntax.all._ で全 syntaxのインポートも可能。
print(List(1,2,3).show)

基本的に cats 配下に型クラス、 cats.instances 配下にインスタンス、

cats.syntax 配下に シンタックスが入っていてそれぞれをインポートして使います。

インスタンスとシンタックスには個別指定のほかに all で一括指定も可能です。

次のように、すべてをまとめてインポートする次のような方法もあります。

import cats._ // 型クラスをインポート

import cats.implicits._ // instances, syntaxをすべてインポート

また、自作の型に対するインスタンス定義ももちろんできますし

次のようにショートカットでインスタンス定義ができるAPIも用意されています。

implicit  val personShowInstance:Show[Person] 

= Show.show(p => s"${p.name}(${p.age})")

以上が型クラスと Catsの概略です。


Cats の型クラス(抜粋)

Cats で定義されている型クラスで、有用なものを紹介していきます。

今回は入門向けに、型引数が1次元のものだけを紹介します。

私見ですが、型クラスはプログラムでよく使う特定のパターンを型レベルで抽象化して汎用的に使えるようにしたものだと思っています。その点も併せて説明してます。

基本的には上記の Syntax 形式で紹介し cats._ , cats.implicits._ をインポートしてある前提で進めていきます。


Show


  • 役割: オブジェクトの文字列表現を得る。toStringの代わり

  • 主なメソッド: show

  • 主な Syntax: show

toString メソッドのような文字列表現を得ることができますが toString と違って後付けで定義できます。

任意のクラスの Show インスタンスを作るには次のようにします。

implicit  val personShowInstance:Show[Person] 

= Show.show(p => s"${p.name}(${p.age})")

次のように show メソッドを呼び出します。

  println(235.show)

println("Hello".show)
println(Person("name", 18).show)

もちろん Show インスタンスが解決できない型に対する show メソッドの呼び出しはコンパイルエラーです。


Eq


  • 役割: 同値性の定義。 ==, equalsの代わり

  • 主なメソッド: eqv

  • 主な Syntax: ===, =!=

型に対しどういった内容なら同じであるのかを定義します。

// equels のエイリアス。case class だとそのまま使える。

implicit val eqPerson = Eq.fromUniversalEquals[Person]
// 任意の内容を設定したい場合.
implicit val dateTimeEq = Eq.by((d:Date) => "%tD%n".format(d)) //年月日のみで比較

=== は同じなら true, =!= は同じなら false を返します。

"Hello" === "Hello" // true

Person("a", 18) === Person("b", 29) // false

Eqが非常に優れているのはEqインスタンスの型であっても、

比較する型が両辺で異なる場合にコンパイルエラーになることです。

(Javaの equals ではこれはできません)

// どちらもコンパイルエラー

"Hello" === 3
Person("a", 21) =!= new Date()


Order


  • 役割: 型の比較内容の定義。 compareTo の代わり

  • 主なメソッド: compare

  • 主な Syntax: min, max

Eq を拡張し順序の比較を行えるようにしたものです。

次のように定義できます。

// 年齢で比較

implicit val ordPerson = Order.by((p:Person) => p.age)

次のように min, max が使用可能になるほか、Eq を継承しているので ===, =!= も使用可能になります。


"AAA" min "BBB" // "AAA"

Person("a", 18) === Person("b", 18) // true
Person("c", 18) max Person("d", 90) // Person("d", 90)

他にも Scala 標準の Ordering も一緒に提供されるようになるので、

次のようにソートも同じ順序比較でできるようになります。

// sortedeも implict パラメータの Ordering[A]が必要だが、 Orderインスタンスから導出

List(Person("e",20), Person("f", 90), Person("g", 10)).sorted


Monoid, Semigroup

さてここから、ちょっと FP の要素が出てきて面白くなってきます。


  • 役割 Semigroup: 足し算。結合。

  • 役割 Monoid: 単位元。0。

  • 主なメソッド: combine, empty

  • 主な Syntax: |+|

1 + 12 ですし、 "hello" + "world"helloworld" です。

Semigourp はこのような 2つの型を結合する演算を型で表現したものです。

Monoid は Semigourp に 単位元(初期値) を追加したものです。

単位元は、int なら 0 ですし、 String なら、 "" (空文字) です。

任意の型の Monoid インスタンスを定義してみます。

case class Amount(amount:Int)

implicit val amountInstance:Monoid[Amount] = new Monoid[Amount] {
override def combine(x: Amount, y: Amount): Amount = x.copy(amount = x.amount + y.amount)
override def empty: Amount = Amount(0)
}

インスタンスの定義に結合ルールを記載することでプログラム本体がシンプルになります。

また |+| により 2個以上の値を結合できるようになります。

"Hello" |+| "World"  // HelloWorld

Amount(12) |+| Amount(23) |+| Amount(-1) // Amount(34)

さらに単位元があることで、畳込み演算ができるようになります。

def foldRight[A](values:Seq[A])(implicit m:Monoid[A]):A

= values.foldRight(m.empty)(m.combine)

上記の定義は Monoid 側に combileAll というメソッドで同じような定義があるのですが

理解のために書いてみました。

これであらゆる Monoid インスタンスに対して畳込み演算が同じ方法で書けるようになりました。

foldRight(Seq(1,2,3)) // 6

foldRight(Seq(Amount(12),Amount(12),Amount(12))) // Amount(36)
foldRight(Seq("a", "b", "c")) // "abc"

シンプルですね。

これだけでも Semigroup, Monoid はいい感じなのですが

タプルやOption、Map などに対しても適用可能なのでさらにいい感じになってきます。

まずタプルですが、タプルの要素が Monoid ならそれぞれ連結できます。(どれかが Monoid でなければコンパイルエラーです)

(1, Amount(12)) |+| (2, Amount(13)) // (3, Amount(25))

次に Option です。

None の結合も可能です。 None は Some(単位元) とみなされます。

Amount(12).some |+| Amount(24).some // Some(Ammount(36))

1.some |+| 6.some |+| None // Some(7) Noneは Some(0)

なお、 hoge.some というのは Option 型の Some を返すシンタックス拡張でスマートコンストラクタと呼びます。

というのも |+| のようなメソッドは、 Option[A] に対して対応するためで、 Some[A] や None には対応しないためです。

Some(hoge) でインスタンスを作る、その型は Some[A] 型に推論されてしまいますが hoge.some は、 Option[A] 型を返すようになっています。

続いて Map です。

Map の Monoid は、2つの Map 間でキーが同じ value を combine で結合します。

val map1 = Map("A" -> 1, "B" -> 2)

val map2 = Map("A" -> 2, "C" -> 3)

map1 |+| map2 // Map(A -> 3, C -> 3, B -> 2)

Map のマージができて便利です。

Monoid をいったん定義しておくと色々な場面でプログラムを短くしたり、同じような書き方ができたりします。

畳込みや演算で同じようなラムダ式を書いたりしている場合 Monoid を作ってみてはどうでしょうか。


まとめ

長くなったので、いったんここで締めたいと思います。

型クラスプログラムの基礎、Cats の概要、基本的な型クラスについて紹介しました。

今回紹介した範囲でのお気に入りは Monoid です。とても便利ですし明日以降紹介する内容でも深くかかわっています。

明日はモナドなど高カインド型の型クラスについて紹介していきます。