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
、特定のタイプのを使用してを作成する関数を作成できます。Buffer
MemoryBuffer
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
。