Pemeriksa tipe mengizinkan penggantian jenis yang sangat salah, dan program masih mengkompilasi

99

Saat mencoba men-debug masalah dalam program saya (2 lingkaran dengan radius yang sama ditarik ke ukuran yang berbeda menggunakan Gloss *), saya menemukan situasi yang aneh. Di file saya yang menangani objek, saya memiliki definisi berikut untuk a Player:

type Coord = (Float,Float)
data Obj =  Player  { oPos :: Coord, oDims :: Coord }

dan di file utama saya, yang mengimpor Objects.hs, saya memiliki definisi berikut:

startPlayer :: Obj
startPlayer = Player (0,0) 10

Ini terjadi karena saya menambahkan dan mengubah bidang untuk pemain, dan lupa memperbarui startPlayersetelahnya (dimensinya ditentukan oleh satu angka untuk mewakili radius, tetapi saya mengubahnya menjadi a Coorduntuk mewakili (lebar, tinggi); seandainya saya pernah membuatnya objek pemain non-lingkaran).

Hal yang menakjubkan adalah, kode di atas mengkompilasi, dan berjalan, meskipun kolom kedua memiliki tipe yang salah.

Saya pertama kali berpikir bahwa mungkin saya memiliki versi berbeda dari file yang terbuka, tetapi setiap perubahan pada file apa pun tercermin dalam program yang dikompilasi.

Selanjutnya saya berpikir bahwa mungkin startPlayertidak digunakan karena suatu alasan. Mengomentari startPlayermenghasilkan kesalahan kompilator, dan bahkan lebih aneh lagi, mengubah 10in startPlayermenyebabkan respons yang sesuai (mengubah ukuran awal Player); sekali lagi, meskipun jenisnya salah. Untuk memastikan bahwa itu membaca definisi data dengan benar, saya memasukkan kesalahan ketik ke dalam file, dan itu memberi saya kesalahan; jadi saya melihat file yang benar.

Saya mencoba menempelkan 2 cuplikan di atas ke dalam file mereka sendiri, dan itu mengeluarkan kesalahan yang diharapkan bahwa bidang kedua Playerdalam startPlayersalah.

Apa yang memungkinkan hal ini terjadi? Anda akan berpikir bahwa inilah hal yang harus dicegah oleh pemeriksa tipe Haskell.


* Jawaban dari masalah awal saya, dua lingkaran dengan jari-jari yang seharusnya sama ditarik ke ukuran yang berbeda, adalah bahwa salah satu jari-jarinya sebenarnya negatif.

Carcigenicate
sumber
26
Seperti yang dicatat oleh @Cubic, Anda harus melaporkan masalah ini ke pengelola Gloss. Pertanyaan Anda dengan baik menggambarkan bagaimana contoh orphan perpustakaan yang tidak tepat mengacaukan kode Anda .
Christian Conkle
1
Selesai. Apakah mungkin untuk mengecualikan contoh? Mereka mungkin memerlukannya agar perpustakaan berfungsi, tetapi saya tidak membutuhkannya. Saya juga memperhatikan bahwa mereka mendefinisikan Num Color. Ini hanya masalah waktu sebelum hal itu mengganggu saya.
Carcigenicate
@Cubic Nah, terlambat. Dan saya hanya mengunduhnya sekitar seminggu yang lalu menggunakan Cabal yang diperbarui dan mutakhir; jadi seharusnya sekarang.
Carcigenicate
2
@ChristianConkle Ada kemungkinan penulis gloss tidak memahami apa yang dilakukan TypeSynonymInstances. Dalam hal apapun, ini benar-benar perlu untuk pergi (baik membuat Pointsebuah newtypeatau menggunakan nama operator lain ala linear)
Cubic
1
@Cubic: TypeSynonymInstances sendiri tidak terlalu buruk (meskipun tidak sepenuhnya tidak berbahaya), tetapi ketika Anda menggabungkannya dengan OverlappingInstances, semuanya menjadi sangat menyenangkan.
John L

Jawaban:

128

Satu-satunya cara ini mungkin dapat dikompilasi adalah jika ada sebuah Num (Float,Float)instance. Ini tidak disediakan oleh pustaka standar, meskipun ada kemungkinan salah satu pustaka yang Anda gunakan menambahkannya karena alasan yang tidak masuk akal. Coba muat proyek Anda di ghci dan lihat apakah 10 :: (Float,Float)berfungsi, lalu coba :i Numcari tahu dari mana instance itu berasal, lalu berteriak pada siapa pun yang mendefinisikannya.

Tambahan: Tidak ada cara untuk mematikan instance. Bahkan tidak ada cara untuk tidak mengekspornya dari modul. Jika ini mungkin, itu akan menyebabkan kode yang lebih membingungkan. Satu-satunya solusi nyata di sini adalah tidak mendefinisikan contoh seperti itu.

Kubik
sumber
53
WOW. 10 :: (Float, Float)menghasilkan (10.0,10.0), dan :i Numberisi baris instance Num Point -- Defined in ‘Graphics.Gloss.Data.Point’( Pointadalah alias dari Coord Gloss). Sungguh? Terima kasih. Itu menyelamatkan saya dari malam tanpa tidur.
Carcigenicate
6
@Carcigenicate Meskipun tampaknya sembrono untuk mengizinkan instance seperti itu, alasannya mengizinkan adalah agar developer dapat menulis instance mereka sendiri di Numtempat yang masuk akal, seperti Angletipe data yang membatasi Doubleantara -pidan pi, atau jika seseorang ingin menulis tipe data mewakili angka empat atau beberapa tipe angka lain yang lebih kompleks, fitur ini sangat memudahkan. Ini juga mengikuti aturan yang sama seperti String/ Text/ ByteString, memungkinkan instance ini masuk akal dari sudut pandang kemudahan penggunaan, tetapi dapat disalahgunakan seperti dalam kasus ini.
bheklilr
4
@bheklilr Saya memahami kebutuhan untuk mengizinkan instance Num. Kata "WOW" berasal dari beberapa hal. Saya tidak tahu Anda dapat membuat contoh alias tipe, membuat contoh Num dari Coord sepertinya kontra intuitif, dan saya tidak memikirkannya. Baiklah, pelajaran yang didapat.
Carcigenicate
3
Anda bisa mengatasi masalah Anda dengan contoh yatim piatu dari perpustakaan Anda dengan menggunakan newtypedeklarasi untuk Coordalih - alih type.
Benjamin Hodgson
3
@Carcigenicate Saya yakin Anda membutuhkan -XTypeSynonymInstances untuk mengizinkan instance untuk jenis sinonim, tetapi itu tidak diperlukan untuk membuat instance yang bermasalah. Sebuah contoh untuk Num (Float, Float)atau bahkan (Floating a) => Num (a,a)tidak memerlukan ekstensi tetapi akan menghasilkan perilaku yang sama.
crockeea
64

Pemeriksa tipe Haskell cukup masuk akal. Masalahnya adalah bahwa penulis perpustakaan yang Anda gunakan telah melakukan sesuatu yang ... kurang masuk akal.

Jawaban singkatnya adalah: Ya, 10 :: (Float, Float)sangat valid jika ada contoh Num (Float, Float). Tidak ada yang "sangat salah" tentang itu dari sudut pandang penyusun atau bahasa. Itu tidak sesuai dengan intuisi kita tentang apa yang dilakukan literal numerik. Karena Anda terbiasa dengan sistem tipe yang menangkap jenis kesalahan yang Anda buat, Anda sangat terkejut dan kecewa!

Numcontoh dan fromIntegermasalahnya

Anda terkejut bahwa kompilator menerima 10 :: Coord, yaitu 10 :: (Float, Float). Masuk akal untuk mengasumsikan bahwa literal numerik like 10akan disimpulkan memiliki tipe "numerik". Keluar dari kotak, literal numerik dapat diartikan sebagai Int, Integer, Float, atau Double. Sekumpulan angka, tanpa konteks lain, tidak tampak seperti angka sebagaimana keempat tipe tersebut adalah angka. Kami tidak membicarakannya Complex.

Untungnya atau sayangnya, bagaimanapun, Haskell adalah bahasa yang sangat fleksibel. Standar menentukan bahwa literal integer 10akan diinterpretasikan sebagai fromInteger 10, yang memiliki tipe Num a => a. Jadi 10bisa disimpulkan sebagai jenis apa pun yang memiliki Numinstance yang ditulis untuknya. Saya menjelaskan ini lebih detail di jawaban lain .

Jadi ketika Anda memposting pertanyaan Anda, Haskeller yang berpengalaman segera melihat bahwa untuk 10 :: (Float, Float)dapat diterima, harus ada contoh seperti Num a => Num (a, a)atau Num (Float, Float). Tidak ada contoh seperti itu di Prelude, jadi itu pasti telah ditentukan di tempat lain. Dengan menggunakan :i Num, Anda dengan cepat melihat dari mana asalnya: glosspaket.

Ketik sinonim dan contoh yatim piatu

Tapi tunggu sebentar. Anda tidak menggunakan glossjenis apa pun dalam contoh ini; mengapa contoh di glossmemengaruhi Anda? Jawabannya ada dalam dua langkah.

Pertama, sinonim tipe yang diperkenalkan dengan kata kunci typetidak membuat tipe baru . Dalam modul Anda, menulis Coordhanyalah singkatan dari (Float, Float). Demikian juga dalam Graphics.Gloss.Data.Point, Pointartinya (Float, Float). Dengan kata lain, Anda Coorddan gloss's Pointsecara harfiah setara.

Jadi, ketika glosspengelola memilih untuk menulis instance Num Point where ..., mereka juga menjadikan Coordtipe Anda sebagai instance Num. Itu sama dengan instance Num (Float, Float) where ...atau instance Num Coord where ....

(Secara default, Haskell tidak mengizinkan sinonim tipe menjadi instance kelas. glossPenulis harus mengaktifkan sepasang ekstensi bahasa, TypeSynonymInstancesdan FlexibleInstances, untuk menulis instance.)

Kedua, ini mengejutkan karena ini adalah instance orphan , yaitu deklarasi instance di instance C Amana keduanya Cdan Adidefinisikan dalam modul lain. Berikut ini terutama berbahaya karena setiap bagian yang terlibat, yaitu Num, (,), dan Float, berasal dari Preludedan cenderung berada dalam lingkup di mana-mana.

Harapan Anda adalah yang Numdidefinisikan dalam Prelude, dan tuple dan Floatdidefinisikan dalam Prelude, jadi segala sesuatu tentang bagaimana ketiga hal itu bekerja didefinisikan Prelude. Mengapa mengimpor modul yang sama sekali berbeda mengubah sesuatu? Idealnya tidak, tetapi contoh yatim piatu menghancurkan intuisi itu.

(Perhatikan bahwa GHC memperingatkan tentang kejadian yatim piatu — penulisnya glosssecara khusus mengabaikan peringatan itu. Itu seharusnya menimbulkan tanda bahaya dan setidaknya memicu peringatan dalam dokumentasi.)

Instance kelas bersifat global dan tidak dapat disembunyikan

Selain itu, instance kelas bersifat global : setiap instance yang ditentukan dalam modul apa pun yang diimpor secara transit dari modul Anda akan berada dalam konteks dan tersedia untuk pemeriksa ketik saat melakukan resolusi instance. Ini membuat penalaran global nyaman, karena kita dapat (biasanya) berasumsi bahwa fungsi kelas (+)akan selalu sama untuk tipe tertentu. Namun, ini juga berarti bahwa keputusan lokal memiliki pengaruh global; mendefinisikan instance kelas secara permanen mengubah konteks kode downstream, tanpa cara untuk menutupi atau menyembunyikannya di balik batasan modul.

Anda tidak dapat menggunakan daftar impor untuk menghindari mengimpor contoh . Demikian pula, Anda tidak dapat menghindari mengekspor instance dari modul yang Anda tentukan.

Ini adalah area desain bahasa Haskell yang bermasalah dan banyak dibahas. Ada diskusi menarik tentang masalah terkait di utas reddit ini . Lihat, misalnya, komentar Edward Kmett tentang mengizinkan kontrol visibilitas sebagai contoh: "Anda pada dasarnya membuang kebenaran hampir semua kode yang telah saya tulis."

(Ngomong-ngomong, seperti yang ditunjukkan jawaban ini , Anda dapat mematahkan asumsi instans global dalam beberapa hal dengan menggunakan instans orphan!)

Apa yang harus dilakukan — untuk pelaksana perpustakaan

Pikirkan dua kali sebelum menerapkan Num. Anda tidak dapat bekerja di sekitar fromIntegermasalah-tidak, mendefinisikan fromInteger = error "not implemented"tidak tidak membuatnya lebih baik. Apakah pengguna Anda akan bingung atau terkejut — atau lebih buruk lagi, tidak pernah menyadarinya — jika literal integer mereka secara tidak sengaja disimpulkan memiliki jenis yang Anda buat? Apakah menyediakan (*)dan (+)itu penting — terutama jika Anda harus meretasnya?

Pertimbangkan untuk menggunakan operator aritmatika alternatif yang ditentukan di perpustakaan seperti Conal Elliott vector-space(untuk jenis jenis *) atau Edward Kmett linear(untuk jenis jenis * -> *). Inilah yang cenderung saya lakukan sendiri.

Gunakan -Wall. Jangan terapkan instance orphan, dan jangan nonaktifkan peringatan orphan instance.

Bergantian, ikuti petunjuk dari lineardan banyak pustaka berperilaku baik lainnya, dan sediakan instance orphan dalam modul terpisah yang diakhiri dengan .OrphanInstancesatau .Instances. Dan jangan impor modul itu dari modul lain . Kemudian pengguna dapat mengimpor yatim piatu secara eksplisit jika mereka mau.

Jika Anda mendapati diri Anda mendefinisikan yatim piatu, pertimbangkan untuk meminta pengelola hulu untuk menerapkannya, jika memungkinkan dan sesuai. Saya dulu sering menulis contoh orphan Show a => Show (Identity a), sampai mereka menambahkannya ke transformers. Saya bahkan mungkin telah melaporkan bug tentang itu; Saya tidak ingat.

Apa yang harus dilakukan — untuk konsumen perpustakaan

Anda tidak punya banyak pilihan. Hubungi — dengan sopan dan konstruktif! —Ke pengelola perpustakaan. Arahkan mereka ke pertanyaan ini. Mereka mungkin punya alasan khusus untuk menulis anak yatim piatu yang bermasalah, atau mereka mungkin tidak menyadarinya.

Lebih luas lagi: Waspadai kemungkinan ini. Ini adalah salah satu dari sedikit area Haskell di mana terdapat pengaruh global yang sebenarnya; Anda harus memeriksa bahwa setiap modul yang Anda impor, dan setiap modul yang diimpor modul tersebut, tidak mengimplementasikan orphan instance. Anotasi jenis terkadang dapat mengingatkan Anda akan masalah, dan tentu saja Anda dapat menggunakan :iGHCi untuk memeriksanya.

Tentukan sendiri newtype, bukan typesinonim, jika itu cukup penting. Anda bisa yakin tidak ada yang akan mengacaukannya.

Jika Anda sering mengalami masalah yang berasal dari pustaka sumber terbuka, Anda tentu saja dapat membuat pustaka versi Anda sendiri, tetapi pemeliharaan dapat dengan cepat memusingkan.

Christian Conkle
sumber