1. ホーム
  2. generics

[解決済み】Scala。抽象型とジェネリックス

2022-04-05 07:46:14

質問

を読んでいました。 Scalaのツアー。抽象型 . 抽象型を使った方が良いのはどんな場合か?

例えば、以下のような場合です。

abstract class Buffer {
  type T
  val element: T
}

とか、ジェネリックスとか。

abstract class Buffer[T] {
  val element: T
}

解決方法は?

この問題については、ここに良い視点がありますね。

Scalaの型システムの目的

マーティン・オーダースキーとの対話 その3

ビル・ヴェナーズ、フランク・ソマーズ著 (2009年5月18日発行)

更新(2009年10月):以下の内容は、実際にビル・ベナーズの新しい記事で説明されています。

Scalaの抽象的な型と一般的な型パラメータ (末尾のまとめを参照)


(以下、2009年5月の第1回インタビューの該当箇所、強調)

一般原則

抽象化には、常に2つの概念があります。

  • パラメタリゼーションと
  • 抽象的なメンバーです。

Javaでも両方がありますが、何を抽象化するかによって異なります。

Javaでは、抽象的なメソッドがありますが、メソッドをパラメータとして渡すことはできません。

抽象的なフィールドはありませんが、パラメータとして値を渡すことができます。

同様に、抽象的な型メンバはありませんが、パラメータとして型を指定することは可能です。

Javaでは、この3つをすべて兼ね備えていますが、どのような種類のものにどのような抽象化原理を使えるかという点で区別されているわけです。そして、この区別はかなり恣意的であると言えるでしょう。

Scalaの方法

を持たせることにしました。 3種類のメンバーすべてについて、同じ構築原則を採用しています。 .

つまり、値のパラメータだけでなく、抽象的なフィールドも持つことができるのです。

メソッド(または "関数" )をパラメータとして渡すこともできますし、それらを抽象化して渡すこともできます。

パラメータとして型を指定することができ、またそれを抽象化することもできます。

そして、概念的に得られるのは、一方を他方でモデル化できるということです。少なくとも原理的には、あらゆる種類のパラメタリゼーションをオブジェクト指向の抽象化の一形態として表現することができるのです。ですから、ある意味、Scalaはより直交的で完全な言語と言えるかもしれません。

どうして?

何を、具体的に。 抽象的な型は、共分散の問題をうまく処理することができます。 という話をしました。

昔からある定番の問題としては、動物と食べ物の問題があります。

パズルは、クラス Animal というメソッドを持つ。 eat を食べます。

問題は、Animalをサブクラスにして、Cowのようなクラスがあった場合、彼らはGrassだけを食べ、任意の食べ物を食べられないということです。例えば、CowはFishを食べることができないのです。

欲しいのは、「牛は草だけを食べて、他のものを食べないeatメソッドを持っている」ということです。

実は、Javaではそれができないんです。先ほどお話したAppleの変数にFruitを代入する問題のように、不健全な状況を構築してしまう可能性があることがわかったからです。

答えは、以下の通りです。 Animal クラスに抽象的な型を追加します。 .

あなたは、私の新しいAnimalクラスは、型が SuitableFood というのは、よくわからないのですが。

つまり、抽象的な型なんですね。その型の実装を与えないんですね。では eat を食べるだけのメソッドです。 SuitableFood .

そして、その中の Cow クラスを拡張したCowがあります。 Animal に対して Cow type SuitableFood equals Grass .

だから 抽象型は、スーパークラスで私が知らない型の概念を提供し、それを後でサブクラスで私が知っている何かで埋めることができます。 .

パラメタライズも同じ?

確かにできますね。クラスAnimalに、食べるものの種類をパラメータ化することができます。

しかし 実際には、いろいろなものでそれをやると、パラメータが爆発的に増えてしまうので で、しかも、通常は パラメータの境界 .

1998年のECOOPで、Kim Bruce、Phil Wadler、私の3人は、次のような論文を発表しました。 知らないことを増やしていくと、典型的なプログラムは二次関数的に大きくなっていきます。 .

ですから、パラメータを使わずに、抽象的なメンバを用意した方が、このような二次的な爆発が起きないという、非常に良い理由があります。


その はコメントで聞いてください。

次のようなまとめ方が妥当だと思いますか。

  • 抽象型は「has-a」または「uses-a」の関係で使用されます(例えば Cow eats Grass )
  • ここで、ジェネリックは通常「の」関係である(例. List of Ints )

抽象型を使うか、ジェネリックスを使うかで、それほど関係が違うとは思えません。 何が違うかというと

  • どのように使用されるか、そして
  • パラメータの境界をどのように管理するか。

Martinが言っている「パラメータの爆発」について理解するために、そして通常、さらに言えば パラメータの境界 そして、抽象的な型をジェネリックでモデル化した場合の二次関数的な増加については、以下の論文で考察されています。 スケーラブルなコンポーネントの抽象化 "著... Martin Odersky, and Matthias Zenger for OOPSLA 2005, reference in the OOPSLA 2005. プロジェクトPalcomの出版物 (2007年終了)。

関連抜粋

定義

<ブロッククオート

抽象的な型のメンバ は、コンポーネントの具体的な型を抽象化するための柔軟な方法を提供します。

抽象型は、コンポーネントの内部に関する情報を隠すことができます。 SML のシグネチャを使用します。クラスを継承して拡張できるオブジェクト指向のフレームワークでは、柔軟なパラメータ化の手段としても使われることがあります(しばしばファミリーポリモーフィズムと呼ばれます。 ブログエントリー という論文があります。 エリック・エルンスト ).

(注:ファミリーポリモーフィズムは、オブジェクト指向言語において、再利用可能でありながら型安全な相互再帰的クラスをサポートするための解決策として提案されたものである。

ファミリーポリモーフィズムのキーとなる考え方は、相互に再帰的なクラスをグループ化するために使用されるファミリーの概念である)

境界型抽象化

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 の特殊化を、Ordered のサブタイプでタイプ・メンバである Oequals T .

この制約のため < メソッドは、レシーバと型Tの引数に対して適用できることが保証されています。

この例では、束縛された型メンバー自体が束縛の一部として現れる可能性があることを示しています。

(すなわち、Scalaは F境界型ポリモーフィズム )

(注、ピーター・キャニング、ウィリアム・クック、ウォルター・ヒル、ウォルター・オルソフ紙より。

境界付き数量化は、与えられた型のすべての部分型に対して一様に作用する関数を型付けする手段として、CardelliとWegnerによって導入されたものである。

彼らは単純なquot;object"モデルを定義し、指定された一連の属性を持つすべてのオブジェクト上で意味をなす関数を型チェックするために境界付き数量化を使用しました。

オブジェクト指向言語のより現実的な表現として、オブジェクトの要素である 再帰的に定義された型 .

このような状況において、有界定量化はもはや本来の目的を果たさない。指定されたメソッドのセットを持つすべてのオブジェクトで意味をなすが、カーデリ・ウェグナーシステムで型付けできない関数を見つけるのは簡単である。

オブジェクト指向言語における型付き多相関数の基礎を提供するために、F-bounded quantificationを導入する)

同じコインの2つの顔

プログラミング言語における抽象化には、主に2つの形態があります。

  • パラメタリゼーションと
  • 抽象的なメンバー

最初の形式は関数型言語で典型的なものであり、2番目の形式はオブジェクト指向言語で典型的に使用されるものである。

従来、Javaは値に対するパラメタライズと、操作に対するメンバの抽象化をサポートしていました。 ジェネリックスを搭載した最近のJava 5.0では、型のパラメタライズもサポートされています。

Scalaにジェネリックを入れることのメリットは、2つあります。

  • まず、抽象型へのエンコーディングは、手作業で行うにはそれほど簡単ではありません。 簡潔さが失われるだけでなく、偶発的な名前の問題もあります。 型パラメータをエミュレートした抽象的な型名同士の衝突が発生します。

  • 第二に、Scalaのプログラムでは、ジェネリックと抽象型は通常、異なる役割を果たします。

    • ジェネリック が必要な場合に使用されます。 型インスタンス化 一方
    • 抽象型 は、通常、次のような場合に使用されます。 抽象的な 型は、クライアントコードから .

      後者は、特に2つの状況で発生する。
    • SMLスタイルのモジュールシステムで知られているカプセル化の一種を得るために、クライアントコードから型メンバーの正確な定義を隠したい場合があります。
    • あるいは、ファミリーポリモーフィズムを得るために、サブクラスで共変的に型をオーバーライドしたいと思うかもしれません。

ポリモーフィズムが制限されたシステムにおいて、抽象型をジェネリックに書き換えると、次のようなことが起こるかもしれません。 型枠の二次拡張 .


2009年10月更新

Scalaの抽象的な型と一般的な型のパラメータ (Bill Venners)

(強調)

<ブロッククオート

について、これまで私が見てきたところでは 抽象型メンバ は、主に一般的な型パラメータよりも良い選択である場合です。

  • を使用させたい場合 これらの型の定義に、trait を通して混ぜる。 .
  • を考えているのですね。 タイプ・メンバを定義する際に、その名前を明示することで、コードの可読性を高めることができます。 .

3 つの異なるフィクスチャオブジェクトをテストに渡したい場合は、そうすることができます。しかし、それぞれのパラメータについて 3 つの型を指定する必要があります。したがって、もし私が型パラメータのアプローチをとっていたら、あなたのスイートクラスはこのようになっていたことでしょう。

// Type parameter version
class MySuite extends FixtureSuite3[StringBuilder, ListBuffer, Stack] with MyHandyFixture {
  // ...
}

一方、タイプ・メンバのアプローチでは、次のようになります。

// Type member version
class MySuite extends FixtureSuite3 with MyHandyFixture {
  // ...
}

もうひとつ、抽象型メンバと汎用型パラメータの細かい違いとして、汎用型パラメータが指定された場合、コードの読者には型パラメータの名前が表示されないことが挙げられます。したがって、誰かがこのコードの行を見たとします。

// Type parameter version
class MySuite extends FixtureSuite[StringBuilder] with StringBuilderFixture {
  // ...
}

StringBuilderと指定された型パラメーターの名前が何なのか、調べないとわからないのです。しかし、型パラメーターの名前は、抽象的な型メンバーのアプローチで、コードのすぐそばにあるのです。

// Type member version
class MySuite extends FixtureSuite with StringBuilderFixture {
  type FixtureParam = StringBuilder
  // ...
}

後者の場合、コードを読んだ人は、次のようにわかります。 StringBuilder はフィクスチャパラメータタイプです。

それでもまだ、"fixture parameter"の意味を理解する必要がありますが、少なくともドキュメントを見ずに型の名前を得ることができます。