はじめに
「enrich my library」パターンと「型クラス」パターンは、既存のクラスに一切触れずに、既存のクラスに振る舞いを追加したいという願いを叶えてくれるデザインパターンです。
いずれも「implicit」を利用して、あたかも既存のクラスに振る舞いが追加されたかのように見せるところがポイントとなります。
単純な振る舞いの追加であれば、実装自体はどちらのパターンでも可能ですので、同じ振る舞いをそれぞれのパターンで実装することにより、相互理解を深めてみます。
環境
Java 8
Scala 2.11
実装する振る舞い
今回は既存のクラスに同じクラス同士を合わせる「plus」という振る舞いを追加します。
実際の使用例としては、下記のようになります。
scala> plus(3, 5)
res: Int = 8
scala> plus("ABC", "DEF")
res: String = ABCDEF
scala> plus('a', 'b')
res: String = ab
scala> 14.plus(13)
res: Int = 27
scala> "Hello".plus(" World!")
res: String = Hello World!
scala> 'S'.plus('F')
res: String = SF
「enrich my library」パターンによる実装
まずは追加したい振る舞いをtraitで定義します。
trait Plus[A, B] {
def plus(p: A): B
}
次にそれぞれのクラスに対応する振る舞いを実装します。
implicit class IntPlus(r: Int) extends Plus[Int, Int] {
def plus(p: Int): Int = r + p
}
implicit class StringPlus(r: String) extends Plus[String, String] {
def plus(p: String): String = r + p
}
implicit class CharPlus(r: Char) extends Plus[Char, String] {
def plus(p: Char): String = r.toString + p.toString
}
これでレシーバとしての使用が可能になりました。
scala> 14.plus(13)
res: Int = 27
scala> "Hello".plus(" World!")
res: String = Hello World!
scala> 'S'.plus('F')
res: String = SF
implicitのおかげで上記の記述が可能で、実際には下記の記述が自動的に補われています。
scala> IntPlus(14).plus(13)
res: Int = 27
scala> StringPlus("Hello").plus(" World!")
res: String = Hello World!
scala> CharPlus('S').plus('F')
res: String = SF
また、レシーバとしての使用で演算子を追加したい場合、下記のように追記は一か所のみとなります。
trait Plus[A, B] {
def plus(p: A): B
def ++++(p: A): B = plus(p)
}
続いてメソッドのパラメータとして使用するために以下の実装を行います。
def plus[A, B](r: Plus[A, B], p: A): B = r.plus(p)
これでパラメータとしての使用が可能になりました。
scala> plus(3, 5)
res: Int = 8
scala> plus("ABC", "DEF")
res: String = ABCDEF
scala> plus('a', 'b')
res: String = ab
implicitのおかげで上記の記述が可能で、実際には下記の記述が自動的に補われています。
scala> plus(IntPlus(3), 5)
res: Int = 8
scala> plus(StringPlus("ABC"), "DEF")
res: String = ABCDEF
scala> plus(CharPlus('a'), 'b')
res: String = ab
「型クラス」パターンによる実装
まずは追加したい振る舞いをtraitで定義します。(→型クラスの定義)
trait Plus[A, B] {
def plus(r: A, p: A): B
}
次にそれぞれのクラスに対する振る舞いを実装します。(→インスタンスの宣言)
implicit def intPlus = new Plus[Int, Int] {
def plus(r: Int, p: Int): Int = r + p
}
implicit def stringPlus = new Plus[String, String] {
def plus(r: String, p: String): String = r + p
}
implicit def charPlus = new Plus[Char, String] {
def plus(r: Char, p: Char): String = r.toString + p.toString
}
メソッドのパラメータとして使用するために以下の実装を行います。
def plus[A, B](r: A, p: A)(implicit f: Plus[A, B]) = f.plus(r, p)
これでパラメータとしての使用が可能になりました。
scala> plus(3, 5)
res: Int = 8
scala> plus("ABC", "DEF")
res: String = ABCDEF
scala> plus('a', 'b')
res: String = ab
implicitのおかげで上記の記述が可能で、実際には下記の記述が自動的に補われています。
scala> plus(3, 5)(intPlus)
res: Int = 8
scala> plus("ABC", "DEF")(stringPlus)
res: String = ABCDEF
scala> plus('a', 'b')(charPlus)
res: String = ab
レシーバとして使用するために以下の実装を行います。
trait PlusOps[A, B] {
val F: Plus[A, B]
val reciver: A
def plus(p: A): B = F.plus(reciver, p)
}
implicit def toPlusOps[A, B](r: A)(implicit f: Plus[A, B]) = new PlusOps[A, B] {
val F = f
val reciver = r
}
これでレシーバとしての使用が可能になりました。
scala> 14.plus(13)
res: Int = 27
scala> "Hello".plus(" World!")
res: String = Hello World!
scala> 'S'.plus('F')
res: String = SF
implicitのおかげで上記の記述が可能で、実際には下記の記述が自動的に補われています。
scala> toPlusOps(14)(intPlus).plus(13)
res: Int = 27
scala> toPlusOps("Hello")(stringPlus).plus(" World!")
res: String = Hello World!
scala> toPlusOps('S')(charPlus).plus('F')
res: String = SF
また、レシーバとしての使用で演算子を追加したい場合、下記のように追記は一か所のみとなります。
trait PlusOps[A, B] {
val F: Plus[A, B]
val reciver: A
def plus(p: A): B = F.plus(reciver, p)
def ++++(p: A): B = plus(p)
}
さいごに
Scalazなどはscalaをオブジェクト指向と関数型のハイブリッド言語としてではなく、関数型言語として捉え直した拡張ライブラリであるため、Haskell由来の「型クラス」パターンが必須のようですが、オブジェクト指向に慣れている人にとっては、「enrich my library」パターンの方がシンプルでわかりやすいと思います。
ただ、Scalazの理解、関数型言語の理解ということを考えると、やっぱり「型クラス」パターンから逃げるわけには。。。ということでしょうか。。。