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!
Num
contoh dan fromInteger
masalahnya
Anda terkejut bahwa kompilator menerima 10 :: Coord
, yaitu 10 :: (Float, Float)
. Masuk akal untuk mengasumsikan bahwa literal numerik like 10
akan 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 10
akan diinterpretasikan sebagai fromInteger 10
, yang memiliki tipe Num a => a
. Jadi 10
bisa disimpulkan sebagai jenis apa pun yang memiliki Num
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 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: gloss
paket.
Ketik sinonim dan contoh yatim piatu
Tapi tunggu sebentar. Anda tidak menggunakan gloss
jenis apa pun dalam contoh ini; mengapa contoh di gloss
memengaruhi Anda? Jawabannya ada dalam dua langkah.
Pertama, sinonim tipe yang diperkenalkan dengan kata kunci type
tidak membuat tipe baru . Dalam modul Anda, menulis Coord
hanyalah singkatan dari (Float, Float)
. Demikian juga dalam Graphics.Gloss.Data.Point
, Point
artinya (Float, Float)
. Dengan kata lain, Anda Coord
dan gloss
's Point
secara harfiah setara.
Jadi, ketika gloss
pengelola memilih untuk menulis instance Num Point where ...
, mereka juga menjadikan Coord
tipe 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. gloss
Penulis harus mengaktifkan sepasang ekstensi bahasa, TypeSynonymInstances
dan FlexibleInstances
, untuk menulis instance.)
Kedua, ini mengejutkan karena ini adalah instance orphan , yaitu deklarasi instance di instance C A
mana keduanya C
dan A
didefinisikan dalam modul lain. Berikut ini terutama berbahaya karena setiap bagian yang terlibat, yaitu Num
, (,)
, dan Float
, berasal dari Prelude
dan cenderung berada dalam lingkup di mana-mana.
Harapan Anda adalah yang Num
didefinisikan dalam Prelude
, dan tuple dan Float
didefinisikan 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 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 sekitar fromInteger
masalah-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 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 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 :i
GHCi untuk memeriksanya.
Tentukan sendiri newtype
, bukan type
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.