Scalaでは、少なくとも2つの方法を使用して、既存の型または新しい型を改良できます。 Int
を使用して何かを数量化できることを表現したいとします。次の特性を定義できます。
trait Quantifiable{ def quantify: Int }
そして、暗黙的な変換を使用して、たとえば、文字列とリスト。
implicit def string2quant(s: String) = new Quantifiable{
def quantify = s.size
}
implicit def list2quantifiable[A](l: List[A]) = new Quantifiable{
val quantify = l.size
}
これらをインポートした後、文字列とリストに対してquantify
メソッドを呼び出すことができます。定量化可能なリストはその長さを格納するので、その後のquantify
の呼び出しでのリストの高価な走査を回避することに注意してください。
別の方法は、一部のタイプA
を数量化できることを示す「目撃者」Quantified[A]
を定義することです。
trait Quantified[A] { def quantify(a: A): Int }
次に、この型クラスのインスタンスをString
およびList
のどこかに提供します。
implicit val stringQuantifiable = new Quantified[String] {
def quantify(s: String) = s.size
}
次に、引数を定量化する必要があるメソッドを作成する場合は、次のように記述します。
def sumQuantities[A](as: List[A])(implicit ev: Quantified[A]) =
as.map(ev.quantify).sum
または、コンテキストバインド構文を使用します。
def sumQuantities[A: Quantified](as: List[A]) =
as.map(implicitly[Quantified[A]].quantify).sum
今問題が来ます。これらの2つの概念の間でどのように決定できますか?
これまでに気づいたこと。
型クラス
T
の値を使用して数値化したいと想像してください。タイプクラスQuantified[A,T]
を作成します暗黙の変換
両方の概念の違いが重要である1つ(または複数)のユースケースを提示し、なぜ私が一方を他方よりも好むのかを説明します。また、2つの概念の本質とそれらの相互関係を説明することは、例がなくてもニースです。
Scala In Depth のマテリアルを複製したくありませんが、型クラス/型の特性が無限に柔軟であることは注目に値します。
def foo[T: TypeClass](t: T) = ...
ローカル環境でデフォルトの型クラスを検索する機能があります。ただし、デフォルトの動作は、次の2つの方法のいずれかでいつでもオーバーライドできます。
次に例を示します。
def myMethod(): Unit = {
// overrides default implicit for Int
implicit object MyIntFoo extends Foo[Int] { ... }
foo(5)
foo(6) // These all use my overridden type class
foo(7)(new Foo[Int] { ... }) // This one needs a different configuration
}
これにより、型クラスが無限に柔軟になります。もう1つは、型クラス/特性が暗黙的なlookupをより適切にサポートすることです。
最初の例では、暗黙的なビューを使用する場合、コンパイラーは暗黙的なルックアップを行います。
Function1[Int, ?]
Function1
のコンパニオンオブジェクトとInt
コンパニオンオブジェクト。
暗黙のルックアップでは、Quantifiable
がnowhereであることに注意してください。つまり、暗黙的なビューをパッケージオブジェクトに配置するかまたはスコープにインポートする必要があります。何が起こっているのかを覚えるのはもっと大変です。
一方、型クラスはexplicitです。メソッドシグネチャで探しているものがわかります。また、暗黙のルックアップがあります
Quantifiable[Int]
Quantifiable
のコンパニオンオブジェクトおよびInt
のコンパニオンオブジェクトを検索します。デフォルトを提供できるおよび新しいタイプ(MyString
クラスなど)は、コンパニオンオブジェクトにデフォルトを提供でき、暗黙的に検索されました。
一般に、私は型クラスを使用します。最初の例では、これらは非常に柔軟です。暗黙的な変換を使用する唯一の場所は、ScalaラッパーとJavaライブラリーの間でAPIレイヤーを使用するときです。これは、注意しないでください。
関係する可能性がある基準の1つは、新機能をどのように「感じさせる」かです。暗黙の変換を使用して、それを別の方法のように見せることができます。
"my string".newFeature
...型クラスを使用している間は、常に外部関数を呼び出しているように見えます。
newFeature("my string")
暗黙的な変換ではなく型クラスで実現できることの1つは、型のインスタンスではなくtypeにプロパティを追加することです。利用可能なタイプのインスタンスがない場合でも、これらのプロパティにアクセスできます。正規の例は次のとおりです。
trait Default[T] { def value : T }
implicit object DefaultInt extends Default[Int] {
def value = 42
}
implicit def listsHaveDefault[T : Default] = new Default[List[T]] {
def value = implicitly[Default[T]].value :: Nil
}
def default[T : Default] = implicitly[Default[T]].value
scala> default[List[List[Int]]]
resN: List[List[Int]] = List(List(42))
この例は、概念がどのように密接に関連しているかも示しています。型クラスは、インスタンスを無限に生成するメカニズムがなければ、それほど有用ではありません。 implicit
メソッドがないと(変換ではなく、確かに)、Default
プロパティを持つ型は有限でしかありません。
名前付きラッパーを使用するだけで、2つの手法の違いを関数の適用に例えることができます。例えば:
trait Foo1[A] { def foo(a: A): Int } // analogous to A => Int
trait Foo0 { def foo: Int } // analogous to Int
前者のインスタンスは型A => Int
の関数をカプセル化しますが、後者のインスタンスはすでにA
に適用されています。あなたはパターンを続けることができます...
trait Foo2[A, B] { def foo(a: A, b: B): Int } // sort of like A => B => Int
したがって、Foo1[B]
をA
インスタンスにFoo2[A, B]
を部分的に適用したようなものと考えることができます。この素晴らしい例は、Miles Sabinによって "Functional Dependencies in Scala" として作成されました。
だから本当に私のポイントは、原則として、