2011-12-19 6 views
3

私は例の本の中でScalaのを読んでいますと、以下のような構成があり、ほぼすべての例の背後にある考え方を理解しようとしている:読書Scalaは、例

abstract class Stack[A] { 
    def push(x: A): Stack[A] = new NonEmptyStack[A](x, this) 
    def isEmpty: Boolean 
    def top: A 
    def pop: Stack[A] 
} 
class EmptyStack[A] extends Stack[A] { 
    def isEmpty = true 
    def top = error("EmptyStack.top") 
    def pop = error("EmptyStack.pop") 
} 
class NonEmptyStack[A](elem: A, rest: Stack[A]) extends Stack[A] { 
    def isEmpty = false 
    def top = elem 
    def pop = rest 
} 

をそして、私は2つの相互接続された質問に次のようしています。 1)空の要素と空でない要素を別々のクラスとして表現するのは、Scalaの一般的なプラクティスですか?はいの場合、なぜですか? 2)なぜ、子供たちは両方とも、親クラスでこれを行うのがより賢明で、同じ愚かな方法を「空である」と実装していますか?

私は、ここに関わる最も深い哲学を知りたいと思います。

+0

Erlang、Clojure、Lispに簡単に触れている間に私が以前気付いていた機能的な、より正確には "リスト指向の"プログラミングの基礎の1つに戻るようです。したがって、これには本当に本質的な意味が必要です。第2の質問については、あなたの答えのおかげで、私はそれが、そのようなアプローチのすべての力を目撃するのはあまりにも単純な例であることを理解しています。 – noncom

+0

それは一般的なプラクティスですか?コレクション - おそらく、一般的なアプリケーションのコーディングですが、代わりにOptionを使用します。私にとって、両方の質問には1つの答えがあります - 著者は継承がどのように具体的にどのように抽象クラスとメソッドを使用できるかを示すことを意図しました。 isEmpty、top、popは抽象メソッドとしてデモされているため、継承クラスで実装する必要があり、異なる実装を持つことができます。これは単なる意見であり、回答として投稿していない。それが役に立てば幸い! – aishwarya

+2

また、この部門は次のような質問に答えるのに役立ちます:「スタック上で動作するが、**コンパイル時に**失敗した関数を、空スタックに渡すにはどうすればいいですか?」 – Jamil

答えて

5

1)空と空でないコンテナに対して別々のクラスを持つことは一般的ですが、これは一般に代数データ構造と呼ばれますが、通常はそれほど明白ではありません。たとえば、ScalaのListには、空のリストを表すNilと、1つの要素と別のリストを含む::という2つのクラスがあります。だから、

List(1,2,3) 

通常List[T]特性によって参照しながら、本当にのように見える:: [B] (hd: B, tl: List[B])のインスタンスである:

::(1, ::(2, ::(3, Nil))) 

2)各クラスあなたが気づけばbecuase isEmptyメソッドを実装する必要があり、値がそれぞれの子クラスで異なっています。 Stackのインスタンスが空であるかどうかを把握するための計算を保存するだけで、各子タイプはコンパイル時にこれを既に知っているからです。

2

Listの実装と似ています。私はちょうどここで推測していますが、例はパターンマッチングが説明されている本の後の章で続行されるので、に一致するようにに一致するように、実装にはcaseというキーワードが前に付けられます。 case head :: tail =>の方法で。

1

「空の」サブクラスなしでこれを書く方法を提案しますか?それを書くようにしてください(また、あなたは可変状態を使うことができないことを忘れないでください)、それは非常に難しいことがわかります。私が知っている唯一の方法は私的なコンストラクタが必要です。

3

1)はい、これは代数的データ型に関数型プログラミング言語では一般的または差別組合を、表現するScalaの方法です。代わりに、オプションのデータメンバー(空の場合はサブクラス、空でない場合はサブクラス、nullを使用する)を使用するクラスが1つだけです。これにより、すべてのメソッドがオブジェクトに実際にデータがあるかどうかをチェックする必要があり、より複雑になります。サブクラスを使用すると、システムの仮想メソッドディスパッチ(それはとにかく)がこのチェックを行います。また、存在するデータはと一致するように強制されます。; Stackは、elemおよびrestNonEmptyStack)のいずれかまたは(EmptyStack)のいずれも持ちません。 1つではなく他のものを持つことはできません(NonEmptyStackを意図的にnullにすると仮定しますが、これはScalaでは非常にまれです)。

データタイプの一般的なパターンは、それぞれ異なるデータが添付されているいくつかのケースのうちの1つであり、広く適用されます。ケースの1つにが含まれていないと、のデータがこの一般的なパターンの単純なケースに過ぎません。 Scalaのプログラマとして、この一般的なパターンを使用することはおなじみなので、単純なケースにも適用するのは当然のようです。


2)あなたはそれぞれの子クラスのメソッドのすべてはすぐにすぐになし、さらに計算で例外をスローし、エラーの場合を除きノーさらに計算(と、値を返すことに注意しましょう)。これは、仮想メソッドのディスパッチの考え方に慣れている限り、それらを非常に明白に理解しやすくします。

さらに、これらは非常に効率的です。 それぞれのメソッドが返すべきものを決定するために必要な計算は、システムがあなたのためにやろうとしている仮想メソッドディスパッチです。親クラスにisEmptyを実装するには、インスタンスチェックと分岐のいくつかの形式を追加する必要があります。これは事実上、システムの仮想メソッドディスパッチの実際の単なる形式です。

さらに重要なのは、子クラスの実装がより保守性が高いことだと思います。さらに特殊な種類の空ではないスタックを追加します(空のスタックに余分な参照を格納するスペースを無駄にしたくないかもしれません)。異なるサブクラスで異なる回答を返すために親で分岐する場合、新しいサブクラスを考慮に入れてそれぞれのサブクラスを更新しなければなりません。 これをしないと、コンパイラはおそらく気付かないでしょう。サブクラスですべてのサブクラス固有の動作を実装した場合は、新しいサブクラスにメソッドを実装するだけです。