ガチャピンの引き出し

学んだことをなるべく体系的に纏めて記事にします。

ポリモーフィズム再入門

実は、ポリモーフィズムが複数存在することをご存知でしょうか。

Javaの入門書であったり、オブジェクト指向について学んでいると必ず遭遇しますが、とっつきづらく、あまり理解していない(する必要もないかも)という場面に良く出会います。

この記事では、言語機能と照らし合わせながら、文字だけでない実践に近い形で、複数のポリモーフィズムを紹介します。

ポリモーフィズムとは

ポリモーフィズム(polymorphism / 多相性)とは、複数の異なる型に対して、共通のインタフェースを提供すること です。つまり、抽象化の話になります。実装を定義する具象型(実装)とは別に、抽象的なインターフェースを用意することで、具象型(実装)を持ちつつも、共通のインターフェースを通じて処理を実行できます。

※ 余談ですが、オブジェクト指向言語の持つ性質/概念の一つという説明も見ますが、オブジェクト指向言語に限った話ではなく、関数型言語にも多相性の概念は存在します。

いろんなポリモーフィズム

ポリモーフィズム(polymorphism / 多相性)にも種類があります。
言語機能としては利用しているが、ポリモーフィズムだと意識していない物も(もしかしたら)あるかもしれません。以下に代表的なものを列挙しましたので、是非ご一読下さい。

部分型(サブタイプ)多相 subtype polymorphism

共通の上位型をもつ複数の型を、1つの名前で扱うような多相を部分型(サブタイプ)多相と言います。オブジェクト指向言語の文脈で、単にポリモーフィズム と言うと部分型(サブタイプ)多相のことです。入門書などで取り上げられ、継承やインターフェースを用いる最も見慣れたものです。

abstract class Animal { def cry() }

class Dog extends Animal { def cry() = println("ワン") }
class Cat extends Animal { def cry() = println("ニャー" )}

val animals:List[Animal] = List(new Dog(), new Cat())

scala> animals.foreach(_.cry)
ワン
ニャー

Dog型やCat型などの具象型ではなく、Animal型という抽象型を通して関数を呼び出しています。Animal型は広義でのインターフェースなので、具象型が何かを気にすることがなく関数を呼び出すことができます。

構造的部分型のようなダック・タイピングで多相を実現するケースもあるので、必ずしも継承が必要な訳ではありません。

パラメータ多相 Parametric polymorphism

型に依存しない共通の振る舞い・性質を定義し、型の情報は引数(型パラメータ)で受け取ります。このような多相をパラメータ多相と言います。いわゆるGenericsジェネリクス/総称型)のことで、型パラメータAと何らかの振る舞い・性質をセットにした型を生成します。代表例はリストです。

val animals:List[Animal] = List(new Dog(), new Cat())

良く見る例ですが、List[A] に対し、 Animalを型パラメータとして渡しています。
他にもFuture[A] は将来A型の値を保持する可能性のある型で、Option[A] はA型の値を保持するか何も保持していない型のことです。

List[A] 、Future[A] 、Option[A] のように型パラメータを受け取り、具体的な型を返す型のことを 型コンストラクタと言います。

以下にミニマムな実装例を載せます。

def f[A](x: A) = x

scala> f[Int](1)
res6: Int = 1

scala> f[String](1)
<console>:13: error: type mismatch;
 found   : Int(1)
 required: String
       f[String](1)
class MyCollection[A](var v: A) {
  def set(a: A) = { v = a }
  def get(): A = v
}

scala> val c = new MyCollection[Int](1)
c: MyCollection[Int] = MyCollection@5c4cc644

scala> c.get()
res0: Int = 1

scala> c.set(10)

scala> c.get()
res2: Int = 10

scala> c.set("hello")
<console>:13: error: type mismatch;
 found   : String("hello")
 required: Int
       c.set("hello")

アドホック多相 Ad hoc polymorphism

ある関数が、引数の型によって異なる振る舞い(実装)を持つような多相のことをアドホック多相と言います。オブジェクト指向言語オーバーロードや、関数型言語の型クラスなどがそれに該当します。

オーバーロード

同じ名前の関数を引数だけ変えて複数定義します。対応している言語であれば、これだけで実現できます。

class Dog { 
  def cry() = println("ワン") 
  def cry(s: String) = println(s) 
}

scala> val dog = new Dog()
dog: Dog = Dog@55c581e4

scala> dog.cry()
ワン

scala> dog.cry("ニャー")
ニャー
型クラス

型クラス自体の説明は、逸脱した内容なので割愛します。
実行結果を見ると、同じ関数で引数が違う場合に、振る舞いが変わっていることが確認できます。
狭義でのインターフェースとよく比較されますが、インターフェースが部分型(サブタイプ)多相を実現するものに対し、型クラスはアドホック多相を実現するものです。

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

// 関数として実行用
def show[A](a: A)(implicit x: Show[A]): String = x.show(a)

// 型クラスのインスタンス
implicit val ShowString: Show[String] = new Show[String] {
  def show(a: String): String = a
}
implicit val ShowInt: Show[Int] = new Show[Int] {
  def show(a: Int): String = a.toString + "!"
}

scala> show("aaa")
res0: String = aaa

scala> show(1)
res1: String = 1!
// メソッドとして生やす
implicit class ShowOps[A](a: A) {
  def show(implicit x: Show[A]): String = { x.show(a) }
}

scala> "aaa".show
res2: String = aaa

scala> 1.show
res3: String = 1!

高階多相 higher-order polymorphism

パラメータ多相の拡張版で型コンストラクタを型引数に取るような多相のことを高階多相と言います。 型コンストラクタを引数にとる型コンストラクタのことで、代表例はファンクターです。

trait Functor[F[_]] {
 def fmap[A, B](f: F[A])(g: A => B): F[B]
}

型コンストラクタは、型パラメータを引数に取って型を返す型のことで、代表例はリストです。List[String], List[Int] と利用しますが、ファンクターはFunctor[List], Functor[Option] のように型コンストラクタ(F[_])を与えることができます。
単純なパラメータ多相では、型コンストラクタを引数に取ることはできません。このようなパラメータ多相の拡張版が高階多相です。

高カインド多相(higher-kinded polymorphism)などとも呼ばれ、現状では決まった呼ばれ方は存在しないようです。私は高階多相 or 高カインド多相と呼ぶ事にしています。

まとめ

今回はポリモーフィズム(polymorphism / 多相性)について纏めました。
ポリモーフィズムにも種類があり、どの多相性を実現するための言語機能かを意識する事で、捉え方や視点が変わります。
例えば、型クラスとインターフェースの違いなどが話題にあがりますが、多相性という概念レベルで違うものを実現しようとしている事を知れば、違いをよりイメージしやすくなるでしょう。

型クラスやファンクターなど、この記事では書ききれないような内容については別記事を書きます。それでは!

参考

参考にさせて頂いた先輩方ありがとうございます。