Haskellの存在型の明確化


10

私はHaskellの存在型を理解しようとしていて、PDF http://www.ii.uni.wroc.pl/~dabi/courses/ZPF15/rlasocha/prezentacja.pdfに遭遇しました

これまでの理解を訂正してください。

  • 存在する型は、それらに含まれる型には関心がないようですが、パターンマッチングにより、TypeableまたはDataを使用しない限り、型がわからない型が存在すると言われています。
  • タイプを非表示にする場合(たとえば、異種リストの場合)、またはコンパイル時にタイプがわからない場合は、これらを使用します。
  • GADTは、暗黙forall

私の疑問

  • 上記のPDFの20ページでは、関数が特定のバッファーを要求することは不可能であることが以下のコードで言及されています。なぜそうなのですか?関数のドラフトをしているとき、どのデータを使用するのかわからない場合でも、使用するバッファーの種類を正確に把握しています。持つことの何が悪いのか:: Worker MemoryBuffer Int彼らが本当にBufferを抽象化したいのなら、Sum型data Buffer = MemoryBuffer | NetBuffer | RandomBufferを持ち、次のような型を持つことができます:: Worker Buffer Int
data Worker x = forall b. Buffer b => Worker {buffer :: b, input :: x}
data MemoryBuffer = MemoryBuffer

memoryWorker = Worker MemoryBuffer (1 :: Int)
memoryWorker :: Worker Int
  • HaskellはCのような完全型消去言語であるため、どの関数を呼び出すかを実行時にどのように知るのでしょうか。いくつかの情報を保持して巨大な関数のVテーブルを渡し、実行時にVテーブルから把握するようなものですか?もしそうなら、どんな種類の情報を保存するのでしょうか?

回答:


8

GADTは、暗黙のforallを提供することにより、存在タイプを使用してコードに明確でより良い構文を提供します

GADT構文の方が優れているという一般的な合意があると思います。GADTが暗黙のforallsを提供するからではなく、ExistentialQuantification拡張機能で有効にされた元の構文が混乱を招く/誤解を招く可能性があるためです。もちろん、その構文は次のようになります。

data SomeType = forall a. SomeType a

または制約付き:

data SomeShowableType = forall a. Show a => SomeShowableType a

そして、私は、forallここでのキーワードの使用により、タイプを完全に異なるタイプと簡単に混同できるという点でコンセンサスがあると思います。

data AnyType = AnyType (forall a. a)    -- need RankNTypes extension

より良い構文では別のexistsキーワードを使用している可能性があるため、次のように記述します。

data SomeType = SomeType (exists a. a)   -- not valid GHC syntax

GADT構文は、暗黙的または明示的に使用されているかどうかにかかわらずforall、これらのタイプ全体でより均一であり、理解しやすいようです。明示的なforallであっても、次の定義は、任意の型の値を取り、aそれを単相型の内部に置くことができるという考えを超えていますSomeType'

data SomeType' where
    SomeType' :: forall a. (a -> SomeType')   -- parentheses optional

そして、そのタイプと次のタイプの違いを見て、理解するのは簡単です。

data AnyType' where
    AnyType' :: (forall a. a) -> AnyType'

存在する型は、それらに含まれる型には関心がないようですが、パターンマッチングにより、TypeableまたはDataを使用しない限り、型がわからない型が存在すると言われています。

タイプを非表示にする場合(たとえば、異種リストの場合)、またはコンパイル時にタイプがわからない場合は、これらを使用します。

存在型を使用しTypeableたり使用したりDataする必要はありませんが、これらはそれほど遠くないと思います。私は、存在型が指定されていない型の周りに型付けされた「ボックス」を提供すると言う方がより正確だと思います。ボックスはある意味でタイプを「非表示」にします。これにより、ボックスに含まれるタイプを無視して、そのようなボックスの異種リストを作成できます。SomeType'上記のような制約のない実存はほとんど役に立ちませんが、制約のあるタイプであることがわかります。

data SomeShowableType' where
    SomeShowableType' :: forall a. (Show a) => a -> SomeShowableType'

「ボックス」内をのぞいてパターンマッチングを行い、型クラス機能を利用できるようにします。

showIt :: SomeShowableType' -> String
showIt (SomeShowableType' x) = show x

これは、Typeableまたはだけでなく、すべての型クラスで機能することに注意してくださいData

スライドデッキの20ページに関する混乱について、著者は、存在 Workerとる関数Worker特定のBufferインスタンスを要求することは不可能であると述べています。次のようにWorker、特定のタイプのを使用してを作成する関数を作成できます。BufferMemoryBuffer

class Buffer b where
  output :: String -> b -> IO ()
data Worker x = forall b. Buffer b => Worker {buffer :: b, input :: x}
data MemoryBuffer = MemoryBuffer
instance Buffer MemoryBuffer

memoryWorker = Worker MemoryBuffer (1 :: Int)
memoryWorker :: Worker Int

しかし、Worker引数として取る関数を書く場合、それは一般的なBuffer型クラス機能(例えば、関数output)しか使用できません:

doWork :: Worker Int -> IO ()
doWork (Worker b x) = output (show x) b

bパターンマッチングを介しても、特定の種類のバッファであることを要求することはできません。

doWorkBroken :: Worker Int -> IO ()
doWorkBroken (Worker b x) = case b of
  MemoryBuffer -> error "try this"       -- type error
  _            -> error "try that"

最後に、存在する型に関する実行時の情報は、関係する型クラスの暗黙の「辞書」引数を通じて利用可能になります。上記のWorkerタイプには、バッファと入力用のフィールドがあることに加えて、Bufferディクショナリを指す非表示の暗黙的なフィールドもあります(適切なoutput関数へのポインタが含まれているだけなので、v-tableと似ていますが、それほど大きくありません)。

内部的には、タイプクラスBufferは関数フィールドを持つデータタイプとして表され、インスタンスはこのタイプの「辞書」です。

data Buffer' b = Buffer' { output' :: String -> b -> IO () }

dBuffer_MemoryBuffer :: Buffer' MemoryBuffer
dBuffer_MemoryBuffer = Buffer' { output' = undefined }

存在タイプには、この辞書の非表示フィールドがあります。

data Worker' x = forall b. Worker' { dBuffer :: Buffer' b, buffer' :: b, input' :: x }

そして、doWork存在Worker'値を操作するような関数は次のように実装されます:

doWork' :: Worker' Int -> IO ()
doWork' (Worker' dBuf b x) = output' dBuf (show x) b

関数が1つしかない型クラスの場合、ディクショナリは実際にはnewtypeに最適化されるため、この例では、存在するWorker型にはoutput、バッファーの関数への関数ポインターで構成される隠しフィールドが含まれており、これが必要な唯一の実行時情報ですによってdoWork


存在はデータ宣言のランク1のようなものですか?実在性は、OOP言語のように、Haskellで仮想関数を処理する方法ですか?
Pawan Kumar

1
おそらくAnyType、ランク2タイプを呼び出すべきではありませんでした。それは混乱を招くだけなので、削除しました。コンストラクターAnyTypeはランク2関数のように動作し、コンストラクターSomeTypeはランク1関数のように動作します(ほとんどの存在しない型と同様)が、これは非常に役立つ特性化ではありません。どちらかといえば、これらの型が興味深いのは、それらが数量化された型を「含む」にもかかわらず、それらがランク0(つまり、型変数に対して数量化されていないため、単相)であることです。
KA Buhr

1
実在型ではなく型クラス(特にそのメソッド関数)は、おそらく仮想関数と同等の最も直接的なHaskellです。技術的な意味では、OOP言語のクラスとオブジェクトは実存型と値として見ることができますが、実際には、HaskellにOOPの「仮想関数」スタイルのポリモーフィズムを実装するには、合計型などの実在型よりも優れた方法が多くあります。型クラス、および/またはパラメトリック多態性。
KA Buhr

4

上記のPDFの20ページでは、関数が特定のバッファーを要求することは不可能であることが以下のコードで言及されています。なぜそうなのですか?

Worker定義されているように、は1つの引数、つまり「入力」フィールドの型(型変数x)のみを取るためです。例えばWorker Intタイプです。タイプ変数はb、代わりにのパラメータではなく、Workerいわば「ローカル変数」の一種です。そのまま渡すことはできませんWorker Int String。型エラーが発生します。

代わりに定義した場合:

data Worker x b = Worker {buffer :: b, input :: x}

その後Worker Int Stringは機能しますが、型はもはや存在しません-常にバッファ型も渡す必要があります。

HaskellはCのような完全型消去言語であるため、どの関数を呼び出すかを実行時にどのように知るのでしょうか。いくつかの情報を保持して巨大な関数のVテーブルを渡し、実行時にVテーブルから把握するようなものですか?もしそうなら、どんな種類の情報を保存するのでしょうか?

これは大体正しいです。簡単に言えばWorker、GHCはコンストラクターを適用するたびにb、の引数から型を推測Workerし、インスタンスを検索しますBuffer b。それが見つかった場合、GHCはオブジェクト内のインスタンスへの追加のポインターを含めます。最も単純な形式では、これは、仮想関数が存在するときにOOPの各オブジェクトに追加される「vtableへのポインター」とそれほど変わりません。

ただし、一般的なケースでは、はるかに複雑になる可能性があります。コンパイラは、コードを高速化する場合、別の表現を使用して、1つの表現ではなく(たとえば、すべてのインスタンスメソッドに直接ポインタを追加する)ポインタを追加します。また、コンパイラーは制約を満たすために複数のインスタンスを使用する必要がある場合もあります。たとえば、インスタンスをEq [Int]... に格納する必要がある場合は、1つではなく2つあります。1 Intつはリスト用で、もう1つはリスト用で、2つを組み合わせる必要があります(実行時、最適化を禁止)。

GHCがそれぞれの場合に何をするかを正確に推測することは困難です。それは、トリガーする場合とトリガーしない場合がある大量の最適化に依存します。

型クラスの「辞書ベース」の実装をグーグルで調べて、何が起こっているのかをもっと見ることができます。また、GHCに内部最適化コアを印刷して、-ddump-simpl構築、保存、および受け渡される辞書を観察するよう依頼することもできます。警告する必要があります:コアはかなり低レベルであり、最初は読みにくい場合があります。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.