web-dev-qa-db-ja.com

暗黙的な変換とタイプクラス

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つの概念の本質とそれらの相互関係を説明することは、例がなくてもニースで​​す。

92
ziggystar

Scala In Depth のマテリアルを複製したくありませんが、型クラス/型の特性が無限に柔軟であることは注目に値します。

def foo[T: TypeClass](t: T) = ...

ローカル環境でデフォルトの型クラスを検索する機能があります。ただし、デフォルトの動作は、次の2つの方法のいずれかでいつでもオーバーライドできます。

  1. 暗黙のルックアップを回避するために、スコープで暗黙の型クラスインスタンスを作成/インポートする
  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コンパニオンオブジェクト。

暗黙のルックアップでは、Quantifiablenowhereであることに注意してください。つまり、暗黙的なビューをパッケージオブジェクトに配置するかまたはスコープにインポートする必要があります。何が起こっているのかを覚えるのはもっと大変です。

一方、型クラスはexplicitです。メソッドシグネチャで探しているものがわかります。また、暗黙のルックアップがあります

Quantifiable[Int]

QuantifiableのコンパニオンオブジェクトおよびIntのコンパニオンオブジェクトを検索します。デフォルトを提供できるおよび新しいタイプ(MyStringクラスなど)は、コンパニオンオブジェクトにデフォルトを提供でき、暗黙的に検索されました。

一般に、私は型クラスを使用します。最初の例では、これらは非常に柔軟です。暗黙的な変換を使用する唯一の場所は、ScalaラッパーとJavaライブラリーの間でAPIレイヤーを使用するときです。これは、注意しないでください。

41
jsuereth

関係する可能性がある基準の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プロパティを持つ型は有限でしかありません。

20
Philippe

名前付きラッパーを使用するだけで、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" として作成されました。

だから本当に私のポイントは、原則として、

  • (暗黙の変換による)クラスの「pimping」は「ゼロ次」の場合です...
  • 型クラスの宣言は「最初の順序」の場合です...
  • fundeps(またはFundepsのようなもの)を持つマルチパラメータタイプクラスが一般的なケースです。
13
mergeconflict