scalaの多相いろいろ

scalaでのいろいろな多相の実装方法とそのpimp my library pattern版をいろいろ書いて比較してみたいと思います。

オーバーロード

これも一応多相と言うらしい。

// オーバーロードによる多相
class John
class Taro

def greet(john: John) {
  println("My name is John!")
}

def greet(taro: Taro) {
  println("太郎です")
}

val john = new John()
greet(john)

val taro = new Taro()
greet(taro)

継承

一般的にはこれをポリモーフィズムと呼ぶことが多いと思います。

// 継承による多相
trait Greeter {
  def greet()
}

class John extends Greeter {
  def greet() {
    println("My name is John!")
  }
}

class Taro extends Greeter {
  def greet() {
    println("太郎です")
  }
}

val john = new John()
john.greet()

val taro = new Taro()
taro.greet()

pimp my library pattern

継承関係になくてもメソッドのように扱うことができる。

// pimp my library
class John
class Taro

trait Greeter {
  def greet()
}

implicit class JohnGreeter(john: John) extends Greeter {
  def greet() {
    println("My name is John!")
  }
}

implicit class TaroGreeter(taro: Taro) extends Greeter {
  def greet() {
    println("太郎です")
  }
}

val john = new John()
john.greet()

val taro = new Taro()
taro.greet()

型クラス

オーバーロードに近いが、関数自体の定義は1カ所のみ。

他のサンプルと挙動を合わせるためにdef greet[A: Greeter](a: A)aは利用していないが、say(a: A)として定義すればaによって挙動を変えることもできる。

// 型クラスによる多相
class John
class Taro

trait Greeter[A] {
  def say()
}

implicit val johnGreeter = new Greeter[John] {
  def say() {
    println("My name is John!")
  }
}

implicit val taroGreeter = new Greeter[Taro] {
  def say() {
    println("太郎です")
  }
}

def greet[A: Greeter](a: A) {
  implicitly[Greeter[A]].say()
}

val john = new John()
greet(john)

val taro = new Taro()
greet(taro)

型クラスのpimp my library patternバージョン。implicit classとcontext boundを使うと結構きれいに書ける。

// 型クラスによる多相(pimp my library pattern)
class John
class Taro

trait Greeter[A] {
  def say()
}

implicit class GreeterOps[A: Greeter](a: A) {
  def greet() {
    implicitly[Greeter[A]].say()
  }
}

implicit val johnGreeter = new Greeter[John] {
  def say() {
    println("My name is John")
  }
}

implicit val taroGreeter = new Greeter[Taro] {
  def say() {
    println("太郎です")
  }
}

val john = new John()
john.greet()

val taro = new Taro()
taro.greet()

型クラス(dependent method types)

型クラスをそのまま書くと戻り値の型は任意の型にできないが(間違ってたらすいません)、dependent method typesを利用することで戻り値の型を任意にすることができる。ただし、context boundで書くことができなくなる。

// 型クラスによる多相(dependent method types)
class John
class Taro

trait Greeter[A] {
  type Result
  def say(): Result
}

implicit val johnGreeter = new Greeter[John] {
  type Result = Unit
  def say(): Result = println("My name is John!")
}

implicit val taroGreeter = new Greeter[Taro] {
  type Result = Unit
  def say(): Result = println("太郎です")
}

def greet[A](a: A)(implicit ev: Greeter[A]): ev.Result = ev.say()

val john = new John()
greet(john)

val taro = new Taro()
greet(taro)

magnet pattern

以下の例ではメリットを感じにくいが、これもdependent method typesによって引数の型によって処理と戻り値の型をばらばらにできる。

また、type erasureによる衝突を回避できる。元記事はこちら。

ほぼほぼ型クラスのdependent method types版と同じで、implicit conversionを使うかimplicit parameterを使うか、という感じ。

// magnet patternによる多相
class John
class Taro

trait GreetingMagnet {
  type Result
  def apply(): Result
}

object GreetingMagnet {
  implicit class JohnGreetingMagnet(john: John) extends GreetingMagnet {
    type Result = Unit
    def apply(): Result = println("My name is John!")
  }

  implicit class TaroGreetingMagnet(taro: Taro) extends GreetingMagnet {
    type Result = Unit
    def apply(): Result = println("私は太郎です")
  }
}

def greet(magnet: GreetingMagnet): magnet.Result = magnet()

val john = new John()
greet(john)

val taro = new Taro()
greet(taro)

magnet patternとpimp my library patternの組み合わせ。だいぶごつごつしてしまった。

// magnet patternによる多相
class John
class Taro

trait GreetingMagnet {
  type Result
  def apply(): Result
}

object GreetingMagnet {
  def apply(john: John): GreetingMagnet = new GreetingMagnet {
    type Result = Unit
    def apply(): Result = println("My name is John!")
  }

  def apply(taro: Taro): GreetingMagnet = new GreetingMagnet {
    type Result = Unit
    def apply(): Result = println("私は太郎です")
  }
}

class GreetingMagnetOps(val magnet: GreetingMagnet) {
  def greet(): magnet.Result = magnet()
}

implicit def john2GreetingMagnetOps(john: John): GreetingMagnetOps =
  new GreetingMagnetOps(GreetingMagnet(john))

implicit def taro2GreetingMagnetOps(taro: Taro): GreetingMagnetOps =
  new GreetingMagnetOps(GreetingMagnet(taro))

val john = new John()
john.greet()

val taro = new Taro()
taro.greet()

比較

パターン 多相 継承関係 戻り値の型変更 定義箇所
オーバーロード アドホック 不要 N
継承 部分型 必要 不可 1
pimp my library pattern アドホック 不要 一部可 N
型クラス アドホック 不要 一部可 1
型クラス(dependent method types) アドホック 不要 1
magnet pattern アドホック 不要 1

まとめ

従来のオブジェクト指向デザインパターンとは違ったことができるので、何でもできる一方で設計力が試されますね。

ざっくりとした知識で書いているので、間違っていたらご指摘いただければ!