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 startPlayer
setelahnya (dimensinya ditentukan oleh satu angka untuk mewakili radius, tetapi saya mengubahnya menjadi a Coord
untuk 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 startPlayer
tidak digunakan karena suatu alasan. Mengomentari startPlayer
menghasilkan kesalahan kompilator, dan bahkan lebih aneh lagi, mengubah 10
in startPlayer
menyebabkan 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 Player
dalam startPlayer
salah.
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.
Point
sebuahnewtype
atau menggunakan nama operator lain alalinear
)Jawaban:
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 apakah10 :: (Float,Float)
berfungsi, lalu coba:i Num
cari 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.
sumber
10 :: (Float, Float)
menghasilkan(10.0,10.0)
, dan:i Num
berisi barisinstance Num Point -- Defined in ‘Graphics.Gloss.Data.Point’
(Point
adalah alias dari Coord Gloss). Sungguh? Terima kasih. Itu menyelamatkan saya dari malam tanpa tidur.Num
tempat yang masuk akal, sepertiAngle
tipe data yang membatasiDouble
antara-pi
danpi
, 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 sepertiString
/Text
/ByteString
, memungkinkan instance ini masuk akal dari sudut pandang kemudahan penggunaan, tetapi dapat disalahgunakan seperti dalam kasus ini.newtype
deklarasi untukCoord
alih - alihtype
.Num (Float, Float)
atau bahkan(Floating a) => Num (a,a)
tidak memerlukan ekstensi tetapi akan menghasilkan perilaku yang sama.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 contohNum (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!Num
contoh danfromInteger
masalahnyaAnda terkejut bahwa kompilator menerima
10 :: Coord
, yaitu10 :: (Float, Float)
. Masuk akal untuk mengasumsikan bahwa literal numerik like10
akan disimpulkan memiliki tipe "numerik". Keluar dari kotak, literal numerik dapat diartikan sebagaiInt
,Integer
,Float
, atauDouble
. Sekumpulan angka, tanpa konteks lain, tidak tampak seperti angka sebagaimana keempat tipe tersebut adalah angka. Kami tidak membicarakannyaComplex
.Untungnya atau sayangnya, bagaimanapun, Haskell adalah bahasa yang sangat fleksibel. Standar menentukan bahwa literal integer
10
akan diinterpretasikan sebagaifromInteger 10
, yang memiliki tipeNum a => a
. Jadi10
bisa disimpulkan sebagai jenis apa pun yang memilikiNum
instance 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 sepertiNum a => Num (a, a)
atauNum (Float, Float)
. Tidak ada contoh seperti itu diPrelude
, jadi itu pasti telah ditentukan di tempat lain. Dengan menggunakan:i Num
, Anda dengan cepat melihat dari mana asalnya:gloss
paket.Ketik sinonim dan contoh yatim piatu
Tapi tunggu sebentar. Anda tidak menggunakan
gloss
jenis apa pun dalam contoh ini; mengapa contoh digloss
memengaruhi Anda? Jawabannya ada dalam dua langkah.Pertama, sinonim tipe yang diperkenalkan dengan kata kunci
type
tidak membuat tipe baru . Dalam modul Anda, menulisCoord
hanyalah singkatan dari(Float, Float)
. Demikian juga dalamGraphics.Gloss.Data.Point
,Point
artinya(Float, Float)
. Dengan kata lain, AndaCoord
dangloss
'sPoint
secara harfiah setara.Jadi, ketika
gloss
pengelola memilih untuk menulisinstance Num Point where ...
, mereka juga menjadikanCoord
tipe Anda sebagai instanceNum
. Itu sama denganinstance Num (Float, Float) where ...
atauinstance Num Coord where ...
.(Secara default, Haskell tidak mengizinkan sinonim tipe menjadi instance kelas.
gloss
Penulis harus mengaktifkan sepasang ekstensi bahasa,TypeSynonymInstances
danFlexibleInstances
, untuk menulis instance.)Kedua, ini mengejutkan karena ini adalah instance orphan , yaitu deklarasi instance di
instance C A
mana keduanyaC
danA
didefinisikan dalam modul lain. Berikut ini terutama berbahaya karena setiap bagian yang terlibat, yaituNum
,(,)
, danFloat
, berasal dariPrelude
dan cenderung berada dalam lingkup di mana-mana.Harapan Anda adalah yang
Num
didefinisikan dalamPrelude
, dan tuple danFloat
didefinisikan dalamPrelude
, jadi segala sesuatu tentang bagaimana ketiga hal itu bekerja didefinisikanPrelude
. 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
gloss
secara 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 sekitarfromInteger
masalah-tidak, mendefinisikanfromInteger = 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 Kmettlinear
(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
linear
dan banyak pustaka berperilaku baik lainnya, dan sediakan instance orphan dalam modul terpisah yang diakhiri dengan.OrphanInstances
atau.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 ketransformers
. 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
:i
GHCi untuk memeriksanya.Tentukan sendiri
newtype
, bukantype
sinonim, 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.
sumber