LoginSignup
5

More than 3 years have passed since last update.

【Scala】超入門・型クラス

Last updated at Posted at 2019-10-12

最初はとっつきにくい、型クラスについての入門記事を書いてみました。

間違いや、表現を変えた方ががより分かりやすい等あれば、ご指摘お願いします:bow:

Goal

細かい理論はひとまず置いといて、とりあえず型クラスを使えるようになる

前提知識

implicitの使い方がわかる。
implicitについてはドワンゴさんのテキストが最高に分かりやすいです。
https://scalajp.github.io/scala_text/implicit.html

型クラスとは何か?

とりあえずサラッと見てみます。
最初は、まあ、そういうものなのかな?くらいの理解で構わないと思います。

「すごいH本(Learn You a Haskell for Great Good!)」より

いきなりHaskellの本から参照ですが・・・個人的にはこれが一番分かりやすい表現で書いているかなと思いました。

A typeclass is a sort of interface that defines some behavior. If a type is a part of a typeclass, that means that it supports and implements the behavior the typeclass describes.

型クラスは、何らかの振る舞いを定義するインターフェイスです。ある型クラスのインスタンスである型は、その型クラスが記述する振る舞いを実装します。

さらに日本語書籍版では

もっと具体的に言うと、型クラスというのは関数の集まりを定めます。
ある型を型クラスのインスタンスにしようと考え たときには、それらの関数がその型ではどういう意味を成すのかを定義します。

と続いています。

「Poor Man's Type Classes(Martin Odersky)」より

scalaで型クラスを使う時の理解として 、分かりやすい表現だと思います。

Type classes are essentially implicitly passed dictionaries, and
dictionaries are essentially objects.

型クラスは本質的に暗黙的に渡される辞書であり、辞書は本質的にオブジェクトである

「Cats Type classes」より

Catsの説明はポリモーフィズムを理解している人には分かりやすい説明かもしれませんが、少し難解です。

Type classes are a powerful tool used in functional programming to enable ad-hoc polymorphism, more commonly known as overloading. Where many object-oriented languages leverage subtyping for polymorphic code, functional programming tends towards a combination of parametric polymorphism (think type parameters, like Java generics) and ad-hoc polymorphism.

型クラスは、一般的にオーバーロードとして知られているアドホック多相有効にするために、関数型プログラミングで使用される強力なツールです。
多くのオブジェクト指向言語がポリモーフィックコードのサブタイピングを活用する場合、関数型プログラミングはパラメトリック多相(Javaジェネリックなどの型パラメーターを考える)とアドホック多相の組み合わせに向かう傾向があります。

参考: 多相(Polymorphism)

  • アドホック多相: 同じ名前の関数で、異なる引数に異なる実装を持つ。Scalaだとオーバーロード
  • パラメトリック多相: 同じ名前の関数で、特定の型を指定せずに、複数の型で使う事ができる。Scalaだと型パラーメタを用いた実装
  • サブタイピング多相: 上位型を持つ複数の方を一つの型(=上位型)として扱う

型クラスを実際に使ってみる

型クラスとは結局何なのか?

こんな感じで書いてあったら、

trait G[A] {
  def g(a: A): A
}

def f[A](a: A)(implicit ev: G[A]): A = ev.g(a)

Gが型クラスです。

そしてf[A](a: A)implicit ev: G[A]G[A]という型がどう動作するか?
という辞書(=使い方)evを渡されます。
ちなみにevはevidence parameterの略らしいです。もちろん別の変数名でも構わないですが、evが使われる事が多いと思います。

実際に使ってみます。
まず辞書であるG[A]を用意(=実装)しておく必要があります。

G型クラスはメソッドg(a: A): Aを持っている型クラスと定義されます。
つまりG型クラスのインスタンスとなるには、g(a: A): Aの実装を定義してやる必要があります。


implicit object GInt extends G[Int] {
  def g(a: Int): Int = a + 1
}
// 古いライブラリや昔からあるものは、`implicit val`で書かれている事もありますが、意味は同じです
implicit val GDouble: G[Double] = new G[Double] {
  def g(a: Double): Double = a + -1d
}

つまりこれが、Odersky先生が書いている、暗黙的に渡される辞書というわけです。
G[Int],G[Double]の使い方を書いた辞書をというわけです。

また、このG[Int],G[Double]を実装する事を、Int,DoubleG型クラスのインスタンスにすると言う事もあります。

ちなみに、なぜGInta + 1で、GDoublea - 1dなのか?
と思うかもしれませんが、意味はありません。

本来はGがもっと意味のある名前になり、g(a)もなんらかの意味があるはずなので、その時はきちんとした意味あるメソッドを定義します。

この状態で、f(a)を使ってみましょう。

scala> f(1)
res0: Int = 2

scala> f(1d)
res1: Double = 0.0

きちんと使えていますね

何の意味があるのか?

たとえばList[A]にはsumというメソッドがあります。
これは何故sumList[Int]でもList[Double]でも使えるのでしょうか?

apiドキュメントを確認してみます。
https://www.scala-lang.org/api/current/scala/collection/immutable/List.html#sum[B%3E:A](implicitnum:scala.math.Numeric[B]):B

def sum[B >: A](implicit num: math.Numeric[B]): B

このimplicit numNumeric[B]Numeric[Int]Numeric[Double]はデフォルトでロードされているため、IntDoubleは何も気にせず、List(1,2,3).sumのようにすれば、合計値が計算できわけです。

逆をいえば、Numeric型クラスのインスタンスにさえすれば、独自実装したどんな型であっても、sumで計算可能と言う事です。

普通はやりませんが、試しにStringNumeric型クラスのインスタンスにしてみます。
sumで必要なのはplus,fromIntのみなので、他は省略します。

implicit object NumericString extends Numeric[String] {
 def plus(x: String, y: String): String = x + y

 def minus(x: String, y: String): String = ???

 def times(x: String, y: String): String =  ???

 def negate(x: String): String = ???

 def fromInt(x: Int): String = ""

 def toInt(x: String): Int = ???

 def toLong(x: String): Long = ???

 def toFloat(x: String): Float = ???

 def toDouble(x: String): Double = ??? 

 def compare(x: String, y: String): Int = ???
}
scala> List("a", "b", "c").sum
res7: String = abc

強引ではありますが、List[String]sumが利用できる事を確認できました。

独自の型クラスを実装してみる

例として、消費税を計算するメソッドを作ってみます。
最初にイメージするのは、こういう形です(消費税は10%です:cry:)

def includeTax(i: Int): Int = i * 1.1

しかし考えてみると、税計算する値はIntとは限りません。
Doubleかもしれません。
返り値も誤差を出さないようにBigDecimalが良い時や、独自クラスにしたい時もあります。

ではどんな型でも取れるように書き換えてみます。
ここで、Aは課税前の型、Bを課税後の型としています。

scala> def includeTax[A, B](a: A): B =a * 1.1
<console>:18: error: value * is not a member of type parameter A
       def includeTax[A, B](a: A): B =a * 1.1
                                        ^

これだけだと当然エラーになります。
Adef *[A, B](a: A): Bというメソッドを持っているかは分からないためです。

そこで型クラスを用意します。

trait Taxable[A, B] {
   def product(a: A, tax: Double): B
   def include(a: A): B = product(a, 1.1)
}

def includeTax[A, B](a: A)(implicit ev: Taxable[A, B]): B = ev.include(a)

これでTaxable型クラスという名の、振る舞い(=メソッド)を定義したインターフェースと、その振る舞いを実装した辞書(= implicit ev)を使うメソッドincludeTaxを作成できました。

次に実際に使う型に対する、辞書を実装してみます。

implicit object TInt extends Taxable[Int, BigDecimal] {
  def product(a: Int, tax: Double): BigDecimal = BigDecimal(a) * tax
}

implicit object TDouble extends Taxable[Double, BigDecimal] {
  def product(a: Double, tax: Double): BigDecimal = BigDecimal(a) * tax
}

これでIntDoubleTaxable型クラスの辞書にできました。
実際に使ってみます。

scala> includeTax(100)
res12: BigDecimal = 110.0

scala> includeTax(100d)
res13: BigDecimal = 110.00

バッチリ消費税計算ができました。

最後に独自クラスもTaxableのインスタンスにしてみます。

// JPY = 日本円
final case class Jpy(amount: BigDecimal)
object Jpy {
  implicit object TJpy extends Taxable[Jpy, Jpy] {
    def product(a: Jpy, tax: Double): Jpy = Jpy(a.amount * tax)
  }
}
scala>  includeTax(Jpy(BigDecimal(100))) 
res16: Jpy = Jpy(110.0)

問題なく動作します。

継承(MixIn)では駄目なのか?

さて、これまでいくつか型クラスの実装をしてみましたが、traitをMixInしては駄目なのかと思いませんでしたか?
私も最初は思っていました。

この回答としては、自分で実装したクラスなら、機能的には同じ事ができます。

def includeTax[A <: Taxable](a: A): A

final case class Jpy extends Taxable

みたいな形でも同じ機能は実現できるでしょう。

では何故型クラスを使うのかというと

  1. プリミティブ型や、他人のライブラリの型にも適用できる
    • 例えば、IntDoubleみたいなプリミティブ型にはMixInできません。
  2. 不要な継承関係を作らないので、継承関係が分かりやすい
    • 例えば、Rational(有理数)みたいなクラスを実装した時に、そこにTaxableみたいなトレイトをMixInするのは、汎用性を考えると避けたいと思います。

という理由になると思います。

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
5