Polimorfisme tingkat tinggi di atas tipe yang tidak dikotak

10

Saya memiliki bahasa di mana tipe tidak dikotakkan secara default, dengan inferensi tipe berdasarkan Hindley-Milner. Saya ingin menambahkan polimorfisme tingkat tinggi, terutama untuk bekerja dengan tipe eksistensial.

Saya rasa saya mengerti bagaimana memeriksa jenis ini, tapi saya tidak yakin apa yang harus dilakukan ketika kompilasi. Saat ini, saya mengkompilasi definisi polimorfik dengan menghasilkan spesialisasi, seperti template C ++, sehingga mereka dapat bekerja dengan nilai-nilai unboxed. Misalnya, diberi definisi f<T>, jika program hanya memanggil f<Int32>dan f<Char>, maka hanya spesialisasi yang muncul dalam program yang dikompilasi. (Saya mengasumsikan kompilasi seluruh program untuk saat ini.)

Tetapi ketika melewati fungsi polimorfik sebagai argumen, saya tidak melihat bagaimana saya dapat menghasilkan spesialisasi yang tepat secara statis, karena fungsi tersebut dapat dipilih saat runtime. Apakah saya tidak punya pilihan selain menggunakan representasi kotak? Atau apakah ada cara untuk mengatasi masalah ini?

Pikiran pertama saya adalah entah bagaimana encode Rank n polimorfisme sebagai peringkat 1, tapi saya tidak percaya itu mungkin pada umumnya karena formula dalam logika konstruktif tidak selalu memiliki bentuk normal prenex.

Jon Purdy
sumber
Alternatifnya adalah mengurangi jumlah tinju yang dibutuhkan dengan menyimpan bitmap yang argumen fungsi dan kata-kata dalam memori adalah pointer. Kemudian fungsi / struct polimorfik sebenarnya polimorfik di atas pointer atau kata data yang berubah-ubah, dan struct dapat menyimpan bidang terakhir mereka (bahkan jika itu polimorfik) sebaris. Bitmap tersebut juga dapat digunakan oleh GC untuk menghindari perlunya tagwords untuk tipe non-jumlah.
fread2281
@ fread2281: Saya sebenarnya pernah melakukan sesuatu seperti itu dalam versi bahasa yang lebih lama. Saat ini saya tidak menghasilkan tag untuk jenis non-jumlah, dan tidak ada GC. Saya pikir itu kompatibel dengan pendekatan Neel K juga.
Jon Purdy

Jawaban:

6

Saya sudah memikirkan sedikit tentang ini. Masalah utama adalah bahwa secara umum, kita tidak tahu seberapa besar nilai tipe polimorfik. Jika Anda tidak memiliki informasi ini, Anda harus mendapatkannya entah bagaimana. Monomorphisation mendapatkan informasi ini untuk Anda dengan mengkhususkan diri pada polimorfisme. Boxing mendapatkan informasi ini untuk Anda dengan meletakkan semuanya ke dalam representasi ukuran yang diketahui.

Alternatif ketiga adalah melacak informasi ini. Pada dasarnya, yang dapat Anda lakukan adalah memperkenalkan jenis yang berbeda untuk setiap ukuran data, dan kemudian fungsi polimorfik dapat didefinisikan atas semua jenis ukuran tertentu. Saya akan membuat sketsa sistem seperti di bawah ini.

Jenisκ:: =nKetik konstruktorSEBUAH:: =Sebuah:κ.SEBUAH|α|SEBUAH×B|SEBUAH+B|SEBUAHB|refSEBUAH|PSebuahd(k)|μα:κ.SEBUAH

Di sini, ide tingkat tinggi adalah bahwa jenis tipe memberi tahu Anda berapa banyak kata yang diperlukan untuk meletakkan objek dalam memori. Untuk ukuran apa pun, mudah menjadi polimorfik untuk semua jenis ukuran tertentu. Karena setiap jenis - bahkan yang polimorfik - masih memiliki ukuran yang diketahui, kompilasi tidak lebih sulit daripada untuk C.

Aturan kinding mengubah bahasa Inggris ini menjadi matematika, dan akan terlihat seperti ini: ΓA : n ΓB : m

α:nΓΓα:nΓ,α:nSEBUAH:mΓα:n.SEBUAH:m
ΓA : m ΓB : n
ΓSEBUAH:nΓB:mΓSEBUAH×B:n+mΓSEBUAH:nΓB:nΓSEBUAH+B:n+1
ΓSEBUAH:mΓB:nΓSEBUAHB:1ΓSEBUAH:nΓrefSEBUAH:1
ΓPSebuahd(k):kΓ,α:nSEBUAH:nΓμα:n.SEBUAH:n

Jadi forall quantifier mengharuskan Anda memberikan jenis yang Anda jangkau. Demikian juga, pasangan adalah jenis pasangan tanpa kotak, yang hanya menjabarkan sebelah dalam memori (seperti tipe C struct). Disjoint unions mengambil dua nilai dengan ukuran yang sama, dan kemudian menambahkan kata untuk tag diskriminator. Fungsi adalah penutupan, diwakili seperti biasa oleh pointer ke catatan lingkungan dan kode.SEBUAH×BSEBUAHB

Referensi menarik - pointer selalu satu kata, tetapi mereka dapat menunjuk ke nilai dari berbagai ukuran. Hal ini memungkinkan pemrogram menerapkan polimorfisme ke objek sewenang-wenang dengan tinju, tetapi tidak mengharuskan mereka untuk melakukannya. Akhirnya, setelah ukuran eksplisit dimainkan, sering kali berguna untuk memperkenalkan jenis padding, yang menggunakan ruang tetapi tidak melakukan apa-apa. (Jadi jika Anda ingin mengambil persatuan int dan sepasang int, Anda perlu menambahkan pad int pertama, sehingga tata letak objek seragam.)

Tipe rekursif memiliki aturan formasi standar, tetapi perhatikan bahwa kejadian rekursif harus memiliki ukuran yang sama, yang berarti Anda biasanya harus menempelkannya dalam sebuah pointer untuk membuat kinding bekerja. Misalnya, tipe data daftar dapat direpresentasikan sebagai

μα:1.ref(PSebuahd(2)+sayant×α)

Jadi ini menunjuk ke nilai daftar kosong, atau sepasang int dan pointer ke daftar tertaut lainnya.

Jenis memeriksa sistem seperti ini juga tidak terlalu sulit; algoritma dalam makalah ICFP saya dengan Joshua Dunfield, Pengetikan Dua Arah Lengkap dan Mudah untuk Polimorfisme Peringkat Tinggi berlaku untuk kasus ini dengan hampir tidak ada perubahan.

Neel Krishnaswami
sumber
Keren, saya pikir ini dengan rapi menutupi kasus penggunaan saya. Saya sadar menggunakan jenis untuk alasan tentang representasi nilai (seperti GHC *vs #), tetapi tidak mempertimbangkan melakukannya dengan cara ini. Tampaknya masuk akal untuk membatasi quantifiers peringkat lebih tinggi ke jenis ukuran yang diketahui, dan saya pikir ini juga akan membuat saya menghasilkan spesialisasi per-ukuran secara statis, tanpa perlu mengetahui jenis yang sebenarnya. Sekarang, saatnya membaca kembali makalah itu. :)
Jon Purdy
1

Ini tampaknya lebih dekat dengan masalah kompilasi daripada masalah "ilmu komputer teoretis", jadi Anda mungkin lebih baik bertanya di tempat lain.

Dalam kasus umum, memang, saya pikir tidak ada solusi lain selain menggunakan representasi kotak. Tetapi saya juga berharap bahwa dalam praktiknya ada banyak pilihan alternatif yang berbeda, tergantung pada spesifik situasi Anda.

Misalnya representasi tingkat rendah dari argumen tanpa kotak biasanya dapat dikategorikan ke dalam beberapa alternatif, misalnya integer-atau-mirip, floating-point, atau pointer. Jadi untuk suatu fungsi f<T>, mungkin Anda benar-benar hanya perlu menghasilkan 3 implementasi tanpa kotak yang berbeda dan Anda dapat mewakili yang polimorfik sebagai tuple dari 3 fungsi tersebut, jadi instantiasi T ke Int32 hanya memilih elemen pertama tuple, ...

Stefan
sumber
Terima kasih atas bantuan Anda. Saya tidak benar-benar yakin ke mana harus bertanya, karena sebuah kompiler membentang dari teori tingkat tinggi ke teknik tingkat rendah, tetapi saya pikir orang-orang di sekitar sini akan memiliki beberapa ide. Sepertinya tinju memang mungkin pendekatan yang paling fleksibel di sini. Setelah membaca jawaban Anda dan memikirkannya lebih lanjut, satu-satunya solusi masuk akal lain yang dapat saya berikan adalah dengan memberikan fleksibilitas dan memerlukan argumen polimorfik agar diketahui secara statis, misalnya dengan menyerahkannya sebagai parameter tipe sendiri. Ini pengorbanan semua jalan turun. : P
Jon Purdy
4
Pertanyaan OP berisi masalah TCS yang benar-benar valid, seperti bagaimana melakukan inferensi tipe ketika Damas-Hindley-Milner diperluas dengan tipe peringkat yang lebih tinggi. Secara umum peringkat-2 polimorfisme memiliki tipe-inferensi yang dapat ditentukan tetapi untuk peringkat k> 2 tipe-inferensi tidak dapat diputuskan. Apakah batasan Damas-Hindley-Milner mengubah ini, saya tidak tahu. Akhirnya hampir semua yang dilakukan oleh kompiler modern harus menjadi bagian dari TCS, tetapi biasanya bukan karena implementor kompiler berada di depan para ahli teori.
Martin Berger