17

最近、Scala ブロガーの間で型クラスパターンに熱狂しているようです。このパターンでは、単純なクラスに、何らかの特性またはパターンに準拠する追加のクラスによって機能が追加されます。非常に単純化された例として、単純なクラス:

case class Wotsit (value: Int)

Foo トレイトに適合させることができます:

trait Foo[T] {
  def write (t: T): Unit
}

この型クラスの助けを借りて:

implicit object WotsitIsFoo extends Foo[Wotsit] {
  def write (wotsit: Wotsit) = println(wotsit.value)
}

型クラスは通常、コンパイル時に暗黙的にキャプチャされ、Wotsit とその型クラスの両方を一緒に高階関数に渡すことができます。

def writeAll[T] (items: List[T])(implicit tc: Foo[T]) =
  items.foreach(w => tc.write(w))

writeAll(wotsits)

(あなたが私を訂正する前に、私はそれが過度に単純化された例だと言いました)

ただし、暗黙の使用は、アイテムの正確なタイプがコンパイル時にわかっていることを前提としています。私のコードでは、多くの場合そうではないことがわかります。あるタイプのアイテム List[T] のリストがあり、それらを操作するための正しいタイプ クラスを検出する必要があります。

Scala で提案されているアプローチは、呼び出し階層のすべてのポイントに typeclass 引数を追加することです。これは、コードがスケーリングされ、これらの依存関係がますます無関係になるメソッドを介して、ますます長いチェーンに渡される必要があるため、煩わしいものになる可能性があります。これにより、Scala の目的とは逆に、コードが雑然として保守が難しくなります。

通常、これは依存性注入が介入する場所であり、ライブラリを使用して、必要な時点で目的のオブジェクトを提供します。詳細は、DI 用に選択されたライブラリによって異なります。私は過去に Java で独自のライブラリを作成しました。

問題は、型クラスの場合、コンパイル時に正確な値がわからないことです。ポリモーフィック記述に基づいて選択する必要があります。そして重大なことに、型情報はコンパイラによって消去されています。マニフェストは型消去に対する Scala のソリューションですが、マニフェストを使用してこの問題に対処する方法は、私にはまったくわかりません。

これに取り組む方法として、人々はどのような手法と Scala の依存性注入ライブラリを提案しますか? 私はトリックを逃していますか?完璧な DI ライブラリ? それとも、これが本当に問題になっているように見えるのでしょうか?


明確化

これには本当に2つの側面があると思います。最初のケースでは、型クラスが必要なポイントは、そのオペランドの正確な型がわかっているポイントからの直接の関数呼び出しによって到達されるため、十分なタイプ ラングリングとシンタックス シュガーにより、型クラスを必要な点です。

2 番目のケースでは、2 つの点がバリアによって分離されています。たとえば、変更できない API、データベースやオブジェクト ストアに保存されている API、シリアル化されて別のコンピューターに送信されている API などです。つまり、型クラスは変更できないことを意味します。オペランドとともに渡されません。この場合、型と値が実行時にしか分からないオブジェクトがある場合、何らかの方法で型クラスを検出する必要があります。

関数型プログラマーは最初のケースを想定する習慣があると思います-十分に高度な言語では、オペランドの型は常に認識可能です。David と mkniessl はこれについて良い答えを提供してくれました。私はそれらを批判したくありません。しかし、2 番目のケースは確かに存在するため、依存性注入を問題に持ち込んだのはそのためです。

4

3 に答える 3

13

新しいコンテキスト バインド構文を使用すると、これらの暗黙的な依存関係を渡すことのかなりの面倒さを軽減できます。あなたの例は

def writeAll[T:Foo] (items: List[T]) =
  items.foreach(w => implicitly[Foo[T]].write(w))

これは同じようにコンパイルされますが、きれいで明確な署名が作成され、「ノイズ」変数が少なくなります。

素晴らしい答えではありませんが、代替案にはおそらくリフレクションが含まれており、これを自動的に機能させるライブラリは知りません。

于 2010-07-15T14:18:37.547 に答える
11

(質問の名前を置き換えましたが、問題について考えるのに役立ちませんでした)

2 つのステップで問題を解決します。最初に、ネストされたスコープにより、型クラス パラメーターをその使用法全体にわたって宣言する必要がなくなることを示します。次に、型クラスのインスタンスが「依存性が注入された」バリアントを示します。

クラスインスタンスをクラスパラメータとして入力

すべての中間呼び出しで型クラス インスタンスを暗黙的なパラメーターとして宣言する必要をなくすために、特定の型クラス インスタンスを使用できるスコープを定義するクラスで型クラス インスタンスを宣言できます。クラス パラメーターの定義には、ショートカット構文 (「コンテキスト バインド」) を使用しています。

object TypeClassDI1 {

  // The type class
  trait ATypeClass[T] {
    def typeClassMethod(t: T): Unit
  }

  // Some data type
  case class Something (value: Int)

  // The type class instance as implicit
  implicit object SomethingInstance extends ATypeClass[Something] {
    def typeClassMethod(s: Something): Unit =
      println("SomthingInstance " + s.value)
  }

  // A method directly using the type class
  def writeAll[T:ATypeClass](items: List[T]) =
    items.foreach(w => implicitly[ATypeClass[T]].typeClassMethod(w))

  // A class defining a scope with a type class instance known to be available    
  class ATypeClassUser[T:ATypeClass] {

    // bar only indirectly uses the type class via writeAll
    // and does not declare an implicit parameter for it.
    def bar(items: List[T]) {
      // (here the evidence class parameter defined 
      // with the context bound is used for writeAll)
      writeAll(items)
    }
  }

  def main(args: Array[String]) {
    val aTypeClassUser = new ATypeClassUser[Something]
    aTypeClassUser.bar(List(Something(42), Something(4711)))
  }
}

書き込み可能なフィールドとしてクラス インスタンスを入力します (setter インジェクション)

セッター注入を使用して使用できる上記の変形。今回は、型クラスを使用して、setter 呼び出しを介して型クラス インスタンスが Bean に渡されます。

object TypeClassDI2 {

  // The type class
  trait ATypeClass[T] {
    def typeClassMethod(t: T): Unit
  }

  // Some data type
  case class Something (value: Int)

  // The type class instance (not implicit here)
  object SomethingInstance extends ATypeClass[Something] {
    def typeClassMethod(s: Something): Unit =
      println("SomthingInstance " + s.value)
  }

  // A method directly using the type class
  def writeAll[T:ATypeClass](items: List[T]) =
    items.foreach(w => implicitly[ATypeClass[T]].typeClassMethod(w))

  // A "service bean" class defining a scope with a type class instance.
  // Setter based injection style for simplicity.
  class ATypeClassBean[T] {
    implicit var aTypeClassInstance: ATypeClass[T] = _

    // bar only indirectly uses the type class via writeAll
    // and does not declare an implicit parameter for it.
    def bar(items: List[T]) {
      // (here the implicit var is used for writeAll)
      writeAll(items)
    }
  }

  def main(args: Array[String]) {
    val aTypeClassBean = new ATypeClassBean[Something]()

    // "inject" the type class instance
    aTypeClassBean.aTypeClassInstance = SomethingInstance

    aTypeClassBean.bar(List(Something(42), Something(4711)))
  }
}

2 番目のソリューションには、セッター ベースのインジェクションの一般的な欠陥があり、依存関係を設定するのを忘れて、使用時に素敵な NullPointerException を取得する可能性があることに注意してください...

于 2010-07-15T19:59:40.710 に答える
0

ここでの依存性注入としての型クラスに対する議論は、型クラスでは「アイテムの正確な型はコンパイル時にわかっている」のに対し、依存性注入ではそうではないということです。この Scala プロジェクトの書き直し作業に興味があるかもしれません。ここでは、依存性注入のためにケーキ パターンから型クラスに移行しました。暗黙の宣言が行われるこのファイルを見てください。環境変数の使用によって正確な型がどのように決定されるかに注目してください。これにより、型クラスのコンパイル時の要件と、依存性注入の実行時の必要性を調和させることができます。

于 2018-03-29T18:10:52.597 に答える