私はScalaのツアー:抽象型を読んでいました。いつ抽象型を使用するのが良いですか?
例えば、
abstract class Buffer {
type T
val element: T
}
むしろそのジェネリック、例えば
abstract class Buffer[T] {
val element: T
}
私はScalaのツアー:抽象型を読んでいました。いつ抽象型を使用するのが良いですか?
例えば、
abstract class Buffer {
type T
val element: T
}
むしろそのジェネリック、例えば
abstract class Buffer[T] {
val element: T
}
回答:
あなたはこの問題についてここで良い見解を持っています:
Scalaの型システムの目的:
Martin
Oderskyとの会話、Bill VennersとFrank SommersによるパートIII(2009年5月18日)
更新(2009年10月):以下の内容は、Bill Vennersによるこの新しい記事で実際に示されています:
Scalaの抽象型メンバーと汎用型パラメーター(最後の要約を参照)
(2009年5月の最初のインタビューの関連抜粋は次のとおりです)
抽象化には常に2つの概念があります。
Javaでも両方がありますが、抽象化する対象によって異なります。
Javaには抽象メソッドがありますが、メソッドをパラメーターとして渡すことはできません。
抽象フィールドはありませんが、値をパラメーターとして渡すことができます。
同様に、抽象型のメンバーはありませんが、型をパラメーターとして指定できます。
したがって、Javaではこれら3つすべてを使用できますが、どのような抽象化の原則をどのような種類のものに使用できるかについては違いがあります。そして、この区別はかなり恣意的であると主張することができます。
私たちは、3種類のメンバーすべてに同じ構成原則を採用することにしました。
したがって、値フィールドだけでなく、抽象フィールドを持つことができます。
メソッド(または「関数」)をパラメーターとして渡すことも、それらを抽象化することもできます。
タイプをパラメーターとして指定することも、それらを抽象化することもできます。
そして、概念的に得られるのは、一方を他方に関してモデル化できるということです。少なくとも原則として、あらゆる種類のパラメータ化をオブジェクト指向の抽象化の形式として表現できます。つまり、ある意味では、Scalaはより直交的で完全な言語であると言えます。
特に、抽象型が購入するものは、これまでに説明したこれらの共分散問題の優れた処理です。
古くからある標準的な問題の1つは、動物と食べ物の問題です。
パズルは、いくつかの食べ物を食べるAnimal
メソッドを持つクラスを持つことeat
でした。
問題は、Animalをサブクラス化し、Cowなどのクラスがある場合、グラスだけを食べ、恣意的な食べ物は食べないことです。たとえば、牛は魚を食べることができませんでした。
あなたが望むのは、牛には草だけを食べ、他のものを食べない食べる方法があるということです。
実際、Javaでそれを行うことはできません。先ほど説明したApple変数にFruitを割り当てる問題など、不健全な状況を構築できることが判明したためです。
その答えは、抽象型をAnimalクラスに追加することです。
あなたが言うには、私の新しいAnimalクラスにはタイプがありSuitableFood
、それは私にはわかりません。
つまり、抽象型です。型の実装は提供しません。次にeat
、食べるだけの方法がありますSuitableFood
。
そして、中Cow
クラスIは言うだろう、OK、私はクラスを拡張牛を持っているAnimal
、とのためにCow type SuitableFood equals Grass
。
だから抽象型は私が知らないスーパークラスの型のこの概念を提供します、そして私はそれから私が知っている何かでサブクラスに後で記入します。
確かにできます。あなたはそれが食べる種類の食物でクラス動物をパラメータ化することができます。
しかし、実際には、さまざまなことを行うと、パラメータが爆発的に増加し、通常はさらに、パラメータの境界が急増します。
1998年のECOOPで、キムブルース、フィルワドラー、そして私は、知らないことを増やすと、典型的なプログラムが二次的に成長することを示した論文を持っていました。
したがって、パラメーターを実行しない非常に適切な理由がありますが、これらの抽象メンバーを使用する必要があります。これらは、この2次の爆発を与えないためです。
thatismattはコメントで尋ねます:
以下は公平な要約だと思いますか:
- で使用されている抽象型は「は、」または「の使用- 」の関係(例えばA
Cow eats Grass
)- ここで、ジェネリックは通常「の」関係です(例
List of Ints
)
関係が抽象型とジェネリックのどちらを使用するかによって異なるかどうかはわかりません。何が違うのですか:
マーティンが「パラメータの爆発、そして通常はさらにパラメータの範囲内で」ということについて話していること、およびジェネリックを使用して抽象型がモデル化されている場合のその後の二次的な成長を理解するには、ペーパー「スケーラブルコンポーネントの抽象化」を検討してください。"執筆者... OOPSLA 2005のMartin Odersky、およびMatthias Zenger 。プロジェクトPalcom(2007年に終了)の出版物で参照されています。
関連する抽出物
抽象型メンバーは、具象型のコンポーネントを抽象化する柔軟な方法を提供します。
抽象型は、SML署名での使用と同様に、コンポーネントの内部に関する情報を非表示にすることができます。継承によってクラスを拡張できるオブジェクト指向のフレームワークでは、パラメーター化の柔軟な手段として使用することもできます(しばしばファミリーポリモーフィズムと呼ばれます。たとえば、このブログエントリ、およびEric Ernstが書いた論文を参照してください)。
(注:再帰可能でタイプセーフな相互再帰クラスをサポートするためのソリューションとして、オブジェクト指向言語用の家族ポリモーフィズムが提案されています。
家族ポリモーフィズムの重要な概念は、相互再帰クラスをグループ化するために使用されるファミリーの概念です)
abstract class MaxCell extends AbsCell {
type T <: Ordered { type O = T }
def setMax(x: T) = if (get < x) set(x)
}
ここで、Tの型宣言は、クラス名Orderedと精製を含む上限型の制限によって制約されています
{ type O = T }
。
上限は、サブクラスでのTの特殊化を、の型メンバーO
であるOrderedのサブタイプに制限しequals T
ます。
この制約があるため、<
クラスOrdered のメソッドは、レシーバーとT型の引数に適用できることが保証されています。
この例は、有界型メンバー自体が有界の一部として表示される場合があることを示しています。
(つまり、ScalaはF制限付きポリモーフィズムをサポートしています)
(注意:Peter Canning、William Cook、Walter Hill、Walter Olthoffの論文から:
限定数量化は、特定の型のすべてのサブタイプに対して均一に機能する関数を入力する手段としてCardelliとWegnerによって導入されました。
彼らは単純な「オブジェクト」モデルを定義しました指定された「属性」のセットを持つすべてのオブジェクトで意味のある型チェック関数に境界付き数量化を使用しました。
オブジェクト指向言語のより現実的な表現は、再帰的に定義された型の要素であるオブジェクトを許可します。
このコンテキストでは、境界付き指定されたメソッドのセットを持つすべてのオブジェクトで意味があるが、Cardelli-Wegnerシステムでは型付けできない関数を簡単に見つけることができます。
オブジェクト指向言語で型付き多態性関数の基礎を提供するために、F限定数量化を導入します)
プログラミング言語には、主に2つの抽象化形式があります。
最初の形式は関数型言語で一般的ですが、2番目の形式は通常オブジェクト指向言語で使用されます。
従来、Javaは値のパラメーター化、および操作のメンバーの抽象化をサポートしています。ジェネリックを備えた最新のJava 5.0では、型のパラメータ化もサポートされています。
Scalaにジェネリックを含めるための引数は2つあります。
まず、抽象型へのエンコードは、手作業で行うのはそれほど簡単ではありません。簡潔さを失うだけでなく、型パラメーターをエミュレートする抽象型名の間で偶然に名前が競合するという問題もあります。
第二に、ジェネリックとアブストラクト型は通常、Scalaプログラムで異なる役割を果たします。
有界多態性を持つシステムでは、抽象型をジェネリックに書き換えると、型の境界の二次展開が必要になる場合があります。
Scalaの抽象型メンバーとジェネリック型パラメーター(Bill Venners)
(強調鉱山)
これまでのところ、抽象型メンバーについての私の見解は、以下の場合、ジェネリック型パラメーターよりも主に良い選択であるということです。
- あなたは人々が特性を介してそれらのタイプの定義を混合できるようにしたいです。
- 定義されているときに型メンバー名を明示的に言及すると、コードが読みやすくなります。
例:
3つの異なるフィクスチャオブジェクトをテストに渡す場合は可能ですが、各パラメーターに1つずつ、3つのタイプを指定する必要があります。したがって、タイプパラメータアプローチを採用した場合、スイートクラスは次のようになる可能性があります。
// Type parameter version
class MySuite extends FixtureSuite3[StringBuilder, ListBuffer, Stack] with MyHandyFixture {
// ...
}
一方、型メンバーのアプローチでは、次のようになります。
// Type member version
class MySuite extends FixtureSuite3 with MyHandyFixture {
// ...
}
抽象型メンバーとジェネリック型パラメーターのもう1つの小さな違いは、ジェネリック型パラメーターが指定されている場合、コードのリーダーには型パラメーターの名前が表示されないことです。したがって、このコード行を見る人がいました。
// Type parameter version
class MySuite extends FixtureSuite[StringBuilder] with StringBuilderFixture {
// ...
}
StringBuilderとして指定された型パラメーターの名前が何であるかは、それを調べなければわかりません。一方、型パラメーターの名前は、抽象型メンバーアプローチのコード内にあります。
// Type member version
class MySuite extends FixtureSuite with StringBuilderFixture {
type FixtureParam = StringBuilder
// ...
}
後者の場合、コードの読者は、それ
StringBuilder
が「フィクスチャーパラメーター」タイプであることがわかります。
「フィクスチャーパラメーター」の意味を理解する必要はありますが、少なくともドキュメントを調べなくても型の名前を取得できます。
Scalaについて読んでいるときにも同じ質問がありました。
ジェネリックを使用する利点は、タイプのファミリーを作成することです。誰もサブクラス化する必要はありませんBuffer
-theyだけで使用できるBuffer[Any]
、Buffer[String]
など、
抽象型を使用すると、サブクラスの作成を余儀なくされます。人々はのようなクラスが必要になりますAnyBuffer
、StringBuffer
など
特定のニーズにどちらが適しているかを判断する必要があります。
Buffer { type T <: String }
または必要にBuffer { type T = String }
応じて変更できます
抽象型を型パラメーターと組み合わせて使用して、カスタムテンプレートを確立できます。
3つの接続された特性を持つパターンを確立する必要があると仮定します。
trait AA[B,C]
trait BB[C,A]
trait CC[A,B]
型パラメーターで言及された引数がAA、BB、CCそのものであるように
なんらかのコードが付属している場合があります。
trait AA[B<:BB[C,AA[B,C]],C<:CC[AA[B,C],B]]
trait BB[C<:CC[A,BB[C,A]],A<:AA[BB[C,A],C]]
trait CC[A<:AA[B,CC[A,B]],B<:BB[CC[A,B],A]]
これは、型パラメーターの結合のため、この単純な方法では機能しません。正しく継承するには、共変にする必要があります
trait AA[+B<:BB[C,AA[B,C]],+C<:CC[AA[B,C],B]]
trait BB[+C<:CC[A,BB[C,A]],+A<:AA[BB[C,A],C]]
trait CC[+A<:AA[B,CC[A,B]],+B<:BB[CC[A,B],A]]
この1つのサンプルはコンパイルされますが、差異ルールに強い要件が設定され、一部の状況では使用できません
trait AA[+B<:BB[C,AA[B,C]],+C<:CC[AA[B,C],B]] {
def forth(x:B):C
def back(x:C):B
}
trait BB[+C<:CC[A,BB[C,A]],+A<:AA[BB[C,A],C]] {
def forth(x:C):A
def back(x:A):C
}
trait CC[+A<:AA[B,CC[A,B]],+B<:BB[CC[A,B],A]] {
def forth(x:A):B
def back(x:B):A
}
コンパイラは、一連の差異チェックエラーで反対します。
その場合、すべてのタイプの要件を追加の特性で収集し、それを超える他の特性をパラメータ化できます。
//one trait to rule them all
trait OO[O <: OO[O]] { this : O =>
type A <: AA[O]
type B <: BB[O]
type C <: CC[O]
}
trait AA[O <: OO[O]] { this : O#A =>
type A = O#A
type B = O#B
type C = O#C
def left(l:B):C
def right(r:C):B = r.left(this)
def join(l:B, r:C):A
def double(l:B, r:C):A = this.join( l.join(r,this), r.join(this,l) )
}
trait BB[O <: OO[O]] { this : O#B =>
type A = O#A
type B = O#B
type C = O#C
def left(l:C):A
def right(r:A):C = r.left(this)
def join(l:C, r:A):B
def double(l:C, r:A):B = this.join( l.join(r,this), r.join(this,l) )
}
trait CC[O <: OO[O]] { this : O#C =>
type A = O#A
type B = O#B
type C = O#C
def left(l:A):B
def right(r:B):A = r.left(this)
def join(l:A, r:B):C
def double(l:A, r:B):C = this.join( l.join(r,this), r.join(this,l) )
}
これで、記述されたパターンの具体的な表現を記述し、すべてのクラスでleftメソッドとjoinメソッドを定義して、無料で右とダブルを取得できます
class ReprO extends OO[ReprO] {
override type A = ReprA
override type B = ReprB
override type C = ReprC
}
case class ReprA(data : Int) extends AA[ReprO] {
override def left(l:B):C = ReprC(data - l.data)
override def join(l:B, r:C) = ReprA(l.data + r.data)
}
case class ReprB(data : Int) extends BB[ReprO] {
override def left(l:C):A = ReprA(data - l.data)
override def join(l:C, r:A):B = ReprB(l.data + r.data)
}
case class ReprC(data : Int) extends CC[ReprO] {
override def left(l:A):B = ReprB(data - l.data)
override def join(l:A, r:B):C = ReprC(l.data + r.data)
}
したがって、抽象型の作成には抽象型と型パラメーターの両方が使用されます。どちらにも弱点と長所があります。抽象型はより具体的であり、任意の型構造を記述できますが、詳細であり、明示的に指定する必要があります。型パラメーターは、一連の型を即座に作成できますが、継承と型の境界についてさらに心配する必要があります。
それらは互いに相乗効果をもたらし、それらの1つだけでは表現できない複雑な抽象化を作成するために組み合わせて使用できます。