依存するメソッドタイプの説得力のある使用例は何ですか?


127

以前は実験的な機能であった従属メソッドタイプが、デフォルトでtrunk有効になりました。これにより、Scalaコミュニティに興奮が生じたようです。

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

依存するメソッドタイプの代替案より明らかに有利なユースケースの実用的で有用な例は何ですか?

以前は不可能/簡単でなかった、それらを使って何ができるか?

彼らは、既存の型システムの機能に対して何を購入するのですか?

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



リンクをありがとう、ダン!依存型は一般的に認識していますが、依存メソッド型の概念は比較的新しいものです。
missingfaktor '22 / 10/22

「依存メソッドタイプ」は、メソッドの1つ以上の入力タイプ(メソッドが呼び出されるオブジェクトのタイプを含む)に依存するタイプにすぎないようです。依存型の一般的な考えを超えて、そこには何もおかしくない。おそらく何かが足りないのでしょうか?
Dan Burton

いいえ、あなたはしませんでしたが、どうやら私はしました。:-)私は前に2つの間のリンクを見ませんでした。でも今ははっきりしている。
missingfaktor

回答:


112

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

だから問題は何ですか?Scalaのネストされた型は、それらを囲むインスタンスに依存します。その結果、依存するメソッドタイプがない場合、そのインスタンスの外部でそれらを使用しようとすると、イライラするほど難しくなる可能性があります。これにより、最初はエレガントで魅力的であるように見えるデザインを、悪夢のように厳格でリファクタリングが困難な怪物に変えることができます。

Advanced 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/ Resourceで洗練されているFileManager/ 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だったか、誰がこの用語を作ったか思い出せない)が、これをDoomのベーカリーと呼んでいるのがわかるでしょう。

編集:コンセンサスは、Doom Baker of DoomがDavid MacIverの造語であったということです...

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


2
この言葉を作ったのはデビッド・マクアイバーでしたが、いずれにせよ、それは非常に説明的です。これは、依存するメソッドタイプが非常に魅力的な理由の素晴らしい説明です。よくやった!
Daniel Spiewak、2011年

それはかなり前に#scalaで私たち2人の間の会話で最初に思い付きました...私が言ったように私が誰が最初にそれを言ったかが思い出せません。
Miles Sabin

私の記憶は私に悪戯をしていたようです...コンセンサスはそれがDavid MacIverの造語だったということです。
Miles Sabin、2011年

ええ、私は当時(#scalaに)そこにいませんでしたが、ホルヘはそこにいて、そこで情報を取得していました。
Daniel Spiewak、2011年

抽象型メンバーの改良を利用して、testHash4関数をかなり簡単に実装できました。def testHash4[R <: ResourceManager#BasicResource](rm: ResourceManager { type Resource = R }, r: R) = assert(r.hash == "9e47088d")ただし、これは依存型の別の形式と見なすことができると思います。
Marco van Hilst、2015年

53
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番目の質問について:この機能によって有効にされる型は、完全な依存型よりもはるかに弱いです(その種類については、Agdaの依存型プログラミングを参照してください)。


6

この新しい機能は、型パラメーターの代わり具象 抽象型メンバーを使用する場合に必要です。型パラメーターが使用される場合、ファミリーのポリモーフィズム型依存関係は、次の簡略化された例のように、Scalaの最新バージョンと一部の古いバージョンで表すことができます。

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, "")
         ^

これは無関係です。型メンバを使用すると、同じ結果のために改良を使用することができますtrait C {type A}; def f[M](a: C { type A = M}, b: M) = 0;class CI extends C{type A=Int};class CS extends C{type A=String}など
nafg

いずれにしても、これは依存するメソッドタイプとは関係ありません。たとえば、アレクセイの例を見てみましょう(stackoverflow.com/a/7860821/333643)。あなたのアプローチ(私がコメントした改良バージョンを含む)を使用しても、目標は達成されません。n1.Node =:= n2.Nodeが保証されますが、両方が同じグラフにあることは保証されません。IIUC DMTはこれを保証します。
nafg 2013年

@nafg指摘してくれてありがとう。明確にするために、具象という単語を追加しました。これは、型メンバーの洗練のケースについて言及していないことを明確にするためです。私の知る限り、これは、他のユースケースでより強力になる可能性があるという点(私は知っていました)にもかかわらず、依存するメソッドタイプの有効なユースケースです。それとも、2番目のコメントの基本的な本質を見逃しましたか?
シェルビームーアIII

3

私は、環境状態を伴う宣言型プログラミングの形式のインターオプションのモデル開発しています。詳細はここでは関係ありません(たとえば、シリアライザと組み合わせたアクターモデルとのコールバックや概念の類似性に関する詳細)。

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

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

しかし、これらのハッシュキーは、ハッシュマップの要素型の静的タイピング、文字列または整数のハッシュ値である場合に包摂どれか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

引数キーを単一の値で渡す必要がある場合はテストしませんでしたが、たとえば2引数のオーバーロード用にタプルを使用できると想定していますdef callit[A](argkeys: Tuple[DependentHashKey,DependentHashKey])(func: Env => argkeys._0.ValueType => argkeys._1.ValueType => A): A。要素の型はコレクションの型に含まれる(コンパイル時には不明)ため、引数キーのコレクションは使用しません。
シェルビームーアIII

「ハッシュマップ要素タイプの静的型付けはAnyまたはAnyRefを包括する」-私は従いません。要素タイプとは、キータイプまたは値タイプ(つまり、HashMapへの最初または2番目のタイプの引数)を意味しますか?そして、なぜそれが包含されるのでしょうか?
ロビングリーン

@RobinGreenハッシュテーブル内の値のタイプ。不正解です。Scalaには共用体(論理和)型がないため、共通のスーパータイプに包含しない限り、Scalaのコレクションに複数の型を置くことはできないためです。Scalaでの包摂に関する私のQ&Aを参照してください。
シェルビームーアIII
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.