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


93

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

型クラス

別の方法はQuantified[A]、あるタイプ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つの概念の本質とそれらの相互関係を説明することは、例がなくてもよいでしょう。


タイプクラスはコンテキストバウンドを使用しますが、「ビューバウンド」について言及するタイプクラスポイントにはいくつかの混乱があります。
ダニエルC.ソブラル2011

1
+1のすばらしい質問。私はこれに対する完全な答えに非常に興味があります。
Dan Burton

@ダニエルありがとうございます。私はいつもそれらを間違えます。
ziggystar 2011

2
1つの場所で間違っています:2番目の暗黙的な変換の例sizeでは、リストのを値に格納し、それが数量化のための後続の呼び出しでのリストの高額な走査を回避しますがquantifylist2quantifiablegets へのすべての呼び出しでトリガーされますつまり、Quantifiableを再インスタンス化し、quantifyプロパティを再計算します。私が言っているのは、暗黙の変換で結果をキャッシュする方法は実際にはないということです。
Nikita Volkov

@NikitaVolkovあなたの観察は正しいです。そして、私はこれを私の質問の最後から2番目の段落で扱います。キャッシングは、変換されたオブジェクトが1回の変換メソッド呼び出しの後に長時間使用されると(そしておそらく変換された形式で渡されると)機能します。一方、型クラスは、より深く行くと、変換されていないオブジェクトに沿ってチェーンされる可能性があります。
ziggystar

回答:


42

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つは、型クラス/特性が暗黙的な検索をより適切にサポートすることです。

最初の例では、暗黙的なビューを使用する場合、コンパイラーは暗黙的なルックアップを実行します。

Function1[Int, ?]

これは、Function1のコンパニオンオブジェクトとコンパニオンオブジェクトを調べIntます。

お知らせQuantifiableはありませんどこにも暗黙のルックアップで。つまり、暗黙的なビューをパッケージオブジェクトに配置する、スコープにインポートする必要があります。何が起こっているのかを覚えるのはもっと大変です。

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

Quantifiable[Int]

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

一般に、私は型クラスを使用します。最初の例では、それらは無限に柔軟です。暗黙的な変換を使用する唯一の場所は、ScalaラッパーとJavaライブラリの間でAPIレイヤーを使用する場合です。注意しないと、これも「危険」になる可能性があります。


20

関係する可能性のある基準の1つは、新機能をどのように「感じさせる」かです。暗黙の変換を使用して、それを別の方法のように見せることができます。

"my string".newFeature

...型クラスを使用している間は、常に外部関数を呼び出しているように見えます。

newFeature("my string")

暗黙的な変換ではなく型クラスで実現できることの1つは、型のインスタンスではなく、にプロパティを追加することです。利用可能なタイプのインスタンスがない場合でも、これらのプロパティにアクセスできます。正規の例は次のとおりです。

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プロパティを持つことができるのは有限のタイプに限られます。


@Phillippe-私が書いたテクニックに非常に興味があります...しかし、Scala 2.11.6では動作しないようです。回答の更新を求める質問を投稿しました。あなたは、事前に感謝を助けることができる場合:ご参照ください: stackoverflow.com/questions/31910923/...
クリス・フォード

@ChrisBedford default将来の読者のために定義を追加しました。
フィリップ

13

名前付きラッパーを使用するだけで、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]の部分的な適用のようなものと考えることができます。この良い例は、Miles Sabinによって「Scalaの機能的依存関係」として作成されました。Foo2[A, B]A

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

  • (暗黙の変換による)クラスの「ポンピング」は、「ゼロ次」の場合です...
  • 型クラスの宣言は「最初の順序」の場合です...
  • Fundeps(またはFundepsのようなもの)を持つマルチパラメータタイプクラスが一般的なケースです。
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.