web-dev-qa-db-ja.com

依存型メソッドタイプの魅力的なユースケースは何ですか?

以前は実験的な機能であった依存メソッドの種類は、トランクでデフォルトで 有効になっており 、明らかにこれが作成されたようです Scalaコミュニティの興奮

一見すると、これが何に役立つかはすぐにはわかりません。 Heiko Seebergerは、依存メソッドタイプの簡単な例を投稿しました here 。コメントに見られるように、メソッドのタイプパラメータで簡単に再現できます。したがって、これは非常に説得力のある例ではありませんでした。 (明白な何かを見逃している可能性があります。もしそうなら私を修正してください。)

依存型メソッドタイプのユースケースの実用的で有用な例として、それらが代替より明らかに有利なものは何ですか?

これまで不可能だった/簡単ではなかった面白いことは何ですか?

彼らは、既存のタイプシステムの機能に対して何を購入しますか?

また、依存メソッドのタイプは、Haskell、OCamlなどの他の高度な型付き言語のタイプシステムに見られる機能に類似しているか、それからインスピレーションを得ていますか?

123
missingfaktor

多かれ少なかれ、メンバー(つまり、ネストされた)型を使用すると、依存するメソッド型が必要になる可能性があります。特に、私は、依存するメソッドタイプがなければ、古典的なケーキパターンはアンチパターンに近いと主張します。

だから問題は何ですか? Scalaの入れ子になった型は、それを囲むインスタンスに依存します。そのため、依存するメソッド型がない場合、そのインスタンスの外側でそれらを使用しようとすると、イライラするほど困難になります。エレガントで魅力的で、悪夢に硬直し、リファクタリングが難しい怪物になります。

上級Scalaトレーニングコース

trait ResourceManager {
  type Resource <: BasicResource
  trait BasicResource {
    def hash : String
    def duplicates(r : Resource) : Boolean
  }
  def create : Resource

  // Test methods: exercise is to move them outside ResourceManager
  def testHash(r : Resource) = assert(r.hash == "9e47088d")  
  def testDuplicates(r : Resource) = assert(r.duplicates(r))
}

trait FileManager extends ResourceManager {
  type Resource <: File
  trait File extends BasicResource {
    def local : Boolean
  }
  override def create : Resource
}

class NetworkFileManager extends FileManager {
  type Resource = RemoteFile
  class RemoteFile extends File {
    def local = false
    def hash = "9e47088d"
    def duplicates(r : Resource) = (local == r.local) && (hash == r.hash)
  }
  override def create : Resource = new RemoteFile
}

これは古典的なケーキパターンの例です。階層化によって徐々に洗練された抽象化のファミリーがあります(ResourceManager/ResourceFileManager/Fileは、NetworkFileManager/RemoteFile)によってさらに洗練されます。これはおもちゃの例ですが、パターンは本物です:Scalaコンパイラー全体で使用され、Scala Eclipseプラグインで広く使用されました。

使用中の抽象化の例を次に示します。

val nfm = new NetworkFileManager
val rf : nfm.Resource = nfm.create
nfm.testHash(rf)
nfm.testDuplicates(rf)

パスの依存性は、コンパイラがtestHashtestDuplicatesおよびNetworkFileManagerメソッドがそれに対応する引数でのみ呼び出せることを保証することを意味することに注意してください。それは独自のRemoteFilesであり、それ以外は何もありません。

これは間違いなく望ましいプロパティですが、このテストコードを別のソースファイルに移動したいとしますか?依存するメソッドタイプを使用すると、ResourceManager階層外のメソッドを簡単に再定義できます。

def testHash4(rm : ResourceManager)(r : rm.Resource) = 
  assert(r.hash == "9e47088d")

def testDuplicates4(rm : ResourceManager)(r : rm.Resource) = 
  assert(r.duplicates(r))

ここで依存メソッド型の使用に注意してください:2番目の引数の型(rm.Resource)は、最初の引数(rm)の値に依存します。

依存するメソッドの種類なしでこれを行うことは可能ですが、それは非常に厄介であり、メカニズムは非常に直感的ではありません。私はこのコースをほぼ2年間教えてきましたが、その間、誰もプロンプトのない実用的なソリューションを思い付きませんでした。

自分で試してみてください...

// Reimplement the testHash and testDuplicates methods outside
// the ResourceManager hierarchy without using dependent method types
def testHash        // TODO ... 
def testDuplicates  // TODO ...

testHash(rf)
testDuplicates(rf)

それに少し苦労した後、あなたはおそらく私(または多分それはDavid MacIverだった、私たちは誰の言葉を作ったのか覚えていない)これを破滅のパン屋と呼ぶ理由を発見するだろう。

編集:コンセンサスは、運命のパン屋さんがデイヴィッド・マクアイバーの造語だったということです...

おまけ:Scalaの一般的な依存型(およびその一部である依存メソッド型)の形式は、プログラミング言語 Beta ...に触発されました。これらは、ベータの一貫したネストセマンティクスから自然に発生します。私は、この形式の依存型を持っている他のかすかに主流のプログラミング言語さえ知りません。 Coq、Cayenne、Epigram、Agdaなどの言語には、いくつかの点でより一般的な依存型付けがありますが、Scalaとは異なり、サブタイピングを持たない型システムの一部であることで大きく異なります。

111
Miles Sabin
trait Graph {
  type Node
  type Edge
  def end1(e: Edge): Node
  def end2(e: Edge): Node
  def nodes: Set[Node]
  def edges: Set[Edge]
}

他のどこかで、2つの異なるグラフのノードを混同しないことを静的に保証できます。

def shortestPath(g: Graph)(n1: g.Node, n2: g.Node) = ... 

もちろん、これはGraph内で定義されていればすでに機能していますが、Graphを変更できず、「pimp my library」拡張機能を作成していると言います。

2番目の質問について:この機能によって有効にされる型は、完全な依存型よりもfar弱い( Agdaの依存型プログラミング を参照それのフレーバー。)私は前にアナロジーを見たとは思わない。

52
Alexey Romanov

この新しい機能は、concrete型パラメーターの代わりに抽象型メンバーが使用される の場合に必要です。型パラメーターを使用すると、次の単純化された例のように、Scalaの最新バージョンと一部の古いバージョンで family polymorphism 型依存関係を表現できます。

trait C[A]
def f[M](a: C[M], b: M) = b
class C1 extends C[Int]
class C2 extends C[String]

f(new C1, 0)
res0: Int = 0
f(new C2, "")
res1: Java.lang.String = 
f(new C1, "")
error: type mismatch;
 found   : C1
 required: C[Any]
       f(new C1, "")
         ^
6

私は モデルの開発 環境状態での宣言型プログラミングの形式の相互選択のためです。詳細はここでは関係ありません(たとえば、コールバックに関する詳細、およびシリアライザーと組み合わせたアクターモデルとの概念的な類似性)。

関連する問題は、状態値がハッシュマップに格納され、ハッシュキー値によって参照されることです。関数は、環境からの値である不変の引数を入力し、他のそのような関数を呼び出し、環境に状態を書き込むことができます。しかし、関数は環境からの値の読み取りを許可されていません(したがって、関数の内部コードは状態変更の順序に依存せず、したがって宣言のままです)その意味で)。 Scalaでこれを入力するには?

環境クラスには、呼び出す関数を入力し、関数の引数のハッシュキーを入力するオーバーロードメソッドが必要です。したがって、このメソッドは、値へのパブリック読み取りアクセスを提供することなく、ハッシュマップから必要な値を使用して関数を呼び出すことができます(必要に応じて、環境から値を読み取る機能を拒否します)。

しかし、これらのハッシュキーが文字列または整数ハッシュ値である場合、ハッシュマップ要素タイプの静的型付け subsumes to AnyまたはAnyRef(ハッシュマップコードは以下に表示されません)、したがって実行時の不一致つまり、特定のハッシュキーのハッシュマップに任意のタイプの値を入れることができます。

trait Env {
...
  def callit[A](func: Env => Any => A, arg1key: String): A
  def callit[A](func: Env => Any => Any => A, arg1key: String, arg2key: String): A
}

以下をテストしませんでしたが、理論的には classOf を使用して実行時にクラス名からハッシュキーを取得できるため、ハッシュキーは文字列ではなくクラス名( Scalaのバックティックを使用して、クラス名に文字列を埋め込みます)。

trait DependentHashKey {
  type ValueType
}
trait `the hash key string` extends DependentHashKey {
  type ValueType <: SomeType
}

したがって、静的タイプの安全性が実現されます。

def callit[A](arg1key: DependentHashKey)(func: Env => arg1key.ValueType => A): A
3