GADT menyediakan sintaks yang jelas & lebih baik untuk kode menggunakan Jenis Eksistensial dengan menyediakan forall's implisit
Saya pikir ada kesepakatan umum bahwa sintaks GADT lebih baik. Saya tidak akan mengatakan bahwa itu karena GADTs menyediakan foralls implisit, tetapi lebih karena sintaks asli, diaktifkan dengan ExistentialQuantification
ekstensi, berpotensi membingungkan / menyesatkan. Sintaks itu, tentu saja, terlihat seperti:
data SomeType = forall a. SomeType a
atau dengan batasan:
data SomeShowableType = forall a. Show a => SomeShowableType a
dan saya pikir konsensusnya adalah bahwa penggunaan kata kunci di forall
sini memungkinkan jenisnya mudah dikacaukan dengan jenis yang sama sekali berbeda:
data AnyType = AnyType (forall a. a) -- need RankNTypes extension
Sintaks yang lebih baik mungkin menggunakan exists
kata kunci terpisah , jadi Anda akan menulis:
data SomeType = SomeType (exists a. a) -- not valid GHC syntax
Sintaks GADT, apakah digunakan dengan implisit atau eksplisit forall
, lebih seragam di semua tipe ini, dan tampaknya lebih mudah dipahami. Bahkan dengan eksplisit forall
, definisi berikut menemukan gagasan bahwa Anda dapat mengambil nilai dari jenis apa pun a
dan memasukkannya ke dalam monomorfik SomeType'
:
data SomeType' where
SomeType' :: forall a. (a -> SomeType') -- parentheses optional
dan mudah untuk melihat dan memahami perbedaan antara tipe itu dan:
data AnyType' where
AnyType' :: (forall a. a) -> AnyType'
Jenis Eksistensial tampaknya tidak tertarik pada jenis yang dikandungnya tetapi pola yang cocok dengan mereka mengatakan bahwa ada beberapa jenis kita tidak tahu apa jenisnya sampai & kecuali kita menggunakan Typeable atau Data.
Kami menggunakannya ketika kami ingin Sembunyikan jenis (mis: untuk Daftar Heterogen) atau kami tidak benar-benar tahu jenis apa pada Waktu Kompilasi.
Saya kira ini tidak terlalu jauh, meskipun Anda tidak harus menggunakan Typeable
atau Data
menggunakan tipe eksistensial. Saya pikir akan lebih akurat untuk mengatakan tipe eksistensial menyediakan "kotak" yang diketik dengan baik di sekitar tipe yang tidak ditentukan. Kotak itu memang "menyembunyikan" jenis itu dalam arti tertentu, yang memungkinkan Anda membuat daftar kotak yang heterogen, mengabaikan jenis yang dikandungnya. Ternyata eksistensial tanpa SomeType'
kendala , seperti di atas cukup berguna, tetapi tipe dibatasi:
data SomeShowableType' where
SomeShowableType' :: forall a. (Show a) => a -> SomeShowableType'
memungkinkan Anda untuk mencocokkan pola untuk mengintip ke dalam "kotak" dan membuat fasilitas kelas tipe tersedia:
showIt :: SomeShowableType' -> String
showIt (SomeShowableType' x) = show x
Perhatikan bahwa ini berfungsi untuk semua tipe kelas, bukan hanya Typeable
atau Data
.
Berkenaan dengan kebingungan Anda tentang halaman 20 dari slide deck, penulis mengatakan bahwa tidak mungkin untuk suatu fungsi yang memerlukan eksistensial Worker
untuk menuntut Worker
memiliki Buffer
instance tertentu . Anda dapat menulis fungsi untuk membuat Worker
menggunakan jenis tertentu Buffer
, seperti 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
tetapi jika Anda menulis fungsi yang menggunakan Worker
argumen, ia hanya dapat menggunakan Buffer
fasilitas kelas tipe umum (mis., fungsi output
):
doWork :: Worker Int -> IO ()
doWork (Worker b x) = output (show x) b
Itu tidak dapat mencoba untuk meminta b
jenis buffer tertentu, bahkan melalui pencocokan pola:
doWorkBroken :: Worker Int -> IO ()
doWorkBroken (Worker b x) = case b of
MemoryBuffer -> error "try this" -- type error
_ -> error "try that"
Akhirnya, informasi runtime tentang tipe-tipe eksistensial tersedia melalui argumen "kamus" implisit untuk typeclasses yang terlibat. The Worker
tipe di atas, selain produk yang memiliki kolom untuk buffer dan masukan, juga memiliki medan implisit tak terlihat yang menunjuk ke Buffer
kamus (agak seperti v-meja, meskipun itu tidak besar, karena hanya berisi pointer ke sesuai output
fungsi).
Secara internal, kelas tipe Buffer
direpresentasikan sebagai tipe data dengan bidang fungsi, dan instance adalah "kamus" dari tipe ini:
data Buffer' b = Buffer' { output' :: String -> b -> IO () }
dBuffer_MemoryBuffer :: Buffer' MemoryBuffer
dBuffer_MemoryBuffer = Buffer' { output' = undefined }
Jenis eksistensial memiliki bidang tersembunyi untuk kamus ini:
data Worker' x = forall b. Worker' { dBuffer :: Buffer' b, buffer' :: b, input' :: x }
dan fungsi seperti doWork
itu beroperasi pada Worker'
nilai-nilai eksistensial diimplementasikan sebagai:
doWork' :: Worker' Int -> IO ()
doWork' (Worker' dBuf b x) = output' dBuf (show x) b
Untuk kelas tipe dengan hanya satu fungsi, kamus sebenarnya dioptimalkan untuk tipe baru, jadi dalam contoh ini, Worker
tipe eksistensial mencakup bidang tersembunyi yang terdiri dari penunjuk fungsi ke output
fungsi buffer, dan itulah satu-satunya informasi runtime yang diperlukan oleh doWork
.
AnyType
tipe peringkat-2; itu hanya membingungkan, dan saya sudah menghapusnya. KonstruktorAnyType
bertindak seperti fungsi peringkat-2, dan konstruktorSomeType
bertindak sebagai fungsi peringkat-1 (seperti kebanyakan tipe non- eksistensial), tetapi itu bukan karakterisasi yang sangat membantu. Jika ada, apa yang membuat tipe-tipe ini menarik adalah mereka berada pada peringkat 0 (yaitu, tidak dikuantifikasi atas variabel tipe dan monomorfik) sendiri meskipun mereka "mengandung" tipe-tipe terkuantifikasi.Karena
Worker
, sebagaimana didefinisikan, hanya membutuhkan satu argumen, jenis bidang "input" (tipe variabelx
). MisalnyaWorker Int
adalah tipe. Variabel tipeb
, sebaliknya, bukan parameter dariWorker
, tetapi merupakan semacam "variabel lokal", jadi untuk berbicara. Itu tidak bisa diteruskan seperti diWorker Int String
- yang akan memicu kesalahan ketik.Jika kita mendefinisikan:
maka
Worker Int String
akan berfungsi, tetapi jenisnya tidak lagi eksistensial - kita sekarang selalu harus melewati jenis penyangga juga.Ini kira-kira benar. Secara singkat, setiap kali Anda menerapkan konstruktor
Worker
, GHC menyimpulkanb
jenis dari argumenWorker
, dan kemudian mencari contohBuffer b
. Jika itu ditemukan, GHC menyertakan pointer tambahan ke instance dalam objek. Dalam bentuknya yang paling sederhana, ini tidak terlalu berbeda dari "pointer to vtable" yang ditambahkan ke setiap objek dalam OOP ketika ada fungsi virtual.Dalam kasus umum, ini bisa menjadi jauh lebih kompleks. Compiler mungkin menggunakan representasi yang berbeda dan menambahkan lebih banyak pointer daripada satu (katakanlah, langsung menambahkan pointer ke semua metode contoh), jika itu mempercepat kode. Juga, kadang-kadang kompiler perlu menggunakan beberapa instance untuk memenuhi batasan. Misalnya, jika kita perlu menyimpan instance untuk
Eq [Int]
... maka tidak hanya ada satu tetapi dua: satu untukInt
dan satu untuk daftar, dan keduanya perlu digabungkan (pada saat run time, kecuali optimasi).Sulit untuk menebak dengan tepat apa yang dilakukan GHC dalam setiap kasus: itu tergantung pada satu ton optimisasi yang mungkin atau mungkin tidak memicu.
Anda bisa mencoba googling untuk implementasi kelas tipe "berbasis kamus" untuk melihat lebih banyak tentang apa yang terjadi. Anda juga dapat meminta GHC untuk mencetak Core yang dioptimalkan internal dengan
-ddump-simpl
dan mengamati kamus yang dibangun, disimpan, dan diedarkan. Saya harus memperingatkan Anda: Core agak rendah, dan mungkin sulit dibaca pada awalnya.sumber