Contoh yatim piatu di Haskell

87

Saat menyusun aplikasi Haskell saya dengan -Wallopsi tersebut, GHC mengeluh tentang instans yatim piatu, misalnya:

Publisher.hs:45:9:
    Warning: orphan instance: instance ToSElem Result

Jenis kelas ToSElembukan milik saya, ini ditentukan oleh HStringTemplate .

Sekarang saya tahu cara memperbaikinya (pindahkan deklarasi instance ke modul tempat Hasil dideklarasikan), dan saya tahu mengapa GHC lebih memilih untuk menghindari instance yatim piatu , tetapi saya masih percaya bahwa cara saya lebih baik. Saya tidak peduli jika kompilernya merepotkan - daripada saya.

Alasan saya ingin mendeklarasikan ToSEleminstance saya di modul Publisher adalah karena modul Publisherlah yang bergantung pada HStringTemplate, bukan modul lainnya. Saya mencoba untuk menjaga pemisahan kekhawatiran dan menghindari setiap modul bergantung pada HStringTemplate.

Saya pikir salah satu keuntungan kelas tipe Haskell, jika dibandingkan misalnya dengan antarmuka Java, adalah mereka terbuka daripada tertutup dan oleh karena itu instance tidak harus dideklarasikan di tempat yang sama dengan tipe datanya. Saran GHC adalah mengabaikan hal ini.

Jadi, yang saya cari adalah beberapa validasi bahwa pemikiran saya sehat dan bahwa saya akan dibenarkan untuk mengabaikan / menekan peringatan ini, atau argumen yang lebih meyakinkan untuk tidak melakukan sesuatu dengan cara saya.

Dan Dyer
sumber
Diskusi dalam jawaban dan komentar menggambarkan bahwa ada perbedaan besar antara mendefinisikan contoh yatim piatu dalam executable , seperti yang Anda lakukan, vs. di perpustakaan yang terbuka ke orang lain. Pertanyaan yang sangat populer ini mengilustrasikan betapa membingungkan contoh yatim piatu bagi pengguna akhir perpustakaan yang mendefinisikannya.
Christian Conkle

Jawaban:

94

Saya mengerti mengapa Anda ingin melakukan ini, tetapi sayangnya, ini mungkin hanya ilusi bahwa kelas Haskell tampak "terbuka" dengan cara yang Anda katakan. Banyak orang merasa bahwa kemungkinan melakukan ini adalah bug dalam spesifikasi Haskell, untuk alasan yang akan saya jelaskan di bawah. Bagaimanapun, jika itu benar-benar tidak sesuai untuk instance Anda perlu dideklarasikan baik di modul tempat kelas dideklarasikan atau di modul tempat tipe dideklarasikan, itu mungkin merupakan tanda bahwa Anda harus menggunakan a newtypeatau pembungkus lain di sekitar tipe Anda.

Alasan mengapa instance orphan perlu dihindari berjalan jauh lebih dalam daripada kenyamanan kompilator. Topik ini agak kontroversial, seperti yang Anda lihat dari jawaban lain. Untuk menyeimbangkan diskusi, saya akan menjelaskan sudut pandang bahwa seseorang tidak boleh, pernah, menulis contoh yatim piatu, yang menurut saya merupakan pendapat mayoritas di antara Haskeller yang berpengalaman. Pendapat saya sendiri ada di tengah-tengah, yang akan saya jelaskan di akhir.

Masalahnya berasal dari fakta bahwa ketika lebih dari satu deklarasi instance ada untuk kelas dan tipe yang sama, tidak ada mekanisme dalam Haskell standar untuk menentukan mana yang akan digunakan. Sebaliknya, program tersebut ditolak oleh kompilator.

Efek paling sederhana dari itu adalah bahwa Anda dapat memiliki program yang bekerja dengan sempurna yang tiba-tiba akan berhenti mengkompilasi karena perubahan yang dibuat orang lain dalam ketergantungan modul Anda yang jauh.

Lebih buruk lagi, mungkin saja program kerja mulai mogok saat runtime karena perubahan yang jauh. Anda bisa menggunakan metode yang Anda asumsikan berasal dari deklarasi instance tertentu, dan metode itu bisa diganti secara diam-diam dengan instance lain yang hanya cukup berbeda untuk menyebabkan program Anda mulai mogok tanpa alasan.

Orang yang menginginkan jaminan bahwa masalah ini tidak akan pernah terjadi pada mereka harus mengikuti aturan bahwa jika ada orang, di mana pun, pernah mendeklarasikan instance dari kelas tertentu untuk jenis tertentu, tidak ada contoh lain yang harus dideklarasikan lagi dalam program apa pun yang ditulis. oleh siapapun. Tentu saja, ada solusi untuk menggunakan a newtypeuntuk mendeklarasikan instance baru, tetapi itu setidaknya selalu merupakan ketidaknyamanan kecil, dan terkadang yang besar. Jadi dalam pengertian ini, mereka yang menulis contoh yatim piatu dengan sengaja menjadi kurang sopan.

Jadi apa yang harus dilakukan tentang masalah ini? Kamp anti-orphan-instance mengatakan bahwa peringatan GHC adalah bug, itu harus berupa kesalahan yang menolak upaya apa pun untuk mendeklarasikan instance orphan. Sementara itu, kita harus menjalankan disiplin diri dan menghindarinya dengan cara apa pun.

Seperti yang Anda lihat, ada orang yang tidak terlalu mengkhawatirkan masalah potensial tersebut. Mereka benar-benar mendorong penggunaan contoh yatim piatu sebagai alat untuk pemisahan masalah, seperti yang Anda sarankan, dan mengatakan bahwa seseorang harus memastikan berdasarkan kasus per kasus bahwa tidak ada masalah. Saya telah cukup sering direpotkan oleh contoh yatim piatu orang lain untuk diyakinkan bahwa sikap ini terlalu angkuh.

Saya pikir solusi yang tepat adalah menambahkan ekstensi ke mekanisme impor Haskell yang akan mengontrol impor instance. Itu tidak akan menyelesaikan masalah sepenuhnya, tapi itu akan memberikan bantuan untuk melindungi program kami dari kerusakan dari kasus yatim piatu yang sudah ada di dunia. Dan kemudian, seiring berjalannya waktu, saya mungkin menjadi yakin bahwa dalam kasus tertentu yang terbatas, mungkin seorang anak yatim piatu mungkin tidak seburuk itu. (Dan godaan itu adalah alasan mengapa beberapa orang di kamp anti-yatim piatu menentang lamaran saya.)

Kesimpulan saya dari semua ini adalah bahwa setidaknya untuk saat ini, saya akan sangat menyarankan agar Anda menghindari mendeklarasikan kasus yatim piatu, untuk mempertimbangkan orang lain jika tidak ada alasan lain. Gunakan a newtype.

Yitz
sumber
4
Secara khusus, ini semakin menjadi masalah dengan pertumbuhan perpustakaan. Dengan> 2200 pustaka di Haskell, dan 10 dari ribuan modul individu, risiko mengambil instance tumbuh secara dramatis.
Don Stewart
16
Re: "Saya pikir solusi yang tepat adalah menambahkan ekstensi ke mekanisme impor Haskell yang akan mengontrol impor instance" Jika ide ini menarik minat siapa pun, mungkin ada baiknya melihat bahasa Scala sebagai contoh; ia memiliki fitur yang sangat mirip dengan ini untuk mengontrol cakupan 'implikasi', yang dapat digunakan sangat mirip dengan instance kelas tipe.
Matt
5
Perangkat lunak saya lebih merupakan aplikasi daripada perpustakaan, jadi kemungkinan menyebabkan masalah bagi pengembang lain hampir tidak ada. Anda dapat mempertimbangkan modul Publisher sebagai aplikasi dan modul lainnya sebagai perpustakaan tetapi jika saya mendistribusikan perpustakaan itu akan tanpa Publisher dan, oleh karena itu, contoh yatim piatu. Tetapi jika saya memindahkan instance ke modul lain, library tersebut akan dikirimkan dengan ketergantungan yang tidak perlu pada HStringTemplate. Jadi dalam hal ini saya pikir anak yatim baik-baik saja, tetapi saya akan memperhatikan saran Anda jika saya menghadapi masalah yang sama dalam konteks yang berbeda.
Dan Dyer
1
Kedengarannya seperti pendekatan yang masuk akal. Satu-satunya hal yang harus diperhatikan adalah jika pembuat modul yang Anda impor menambahkan instance ini di versi yang lebih baru. Jika instance itu sama dengan milik Anda, Anda harus menghapus deklarasi instance Anda sendiri. Jika contoh itu berbeda dari milik Anda, Anda harus meletakkan pembungkus tipe baru di sekitar tipe Anda - yang bisa menjadi pemfaktoran ulang kode Anda yang signifikan.
Yitz
@Matt: memang, secara mengejutkan Scala mendapatkan yang ini tepat di mana Haskell tidak! (kecuali tentu saja Scala tidak memiliki sintaks kelas satu untuk mesin kelas tipe, yang bahkan lebih buruk ...)
Erik Kaplun
44

Silakan tekan peringatan ini!

Anda berada di perusahaan yang baik. Conal melakukannya di "TypeCompose". "chp-mtl" dan "chp-transformers" melakukannya, "control-monad-exception-mtl" dan "control-monad-exception-monadsfd" melakukannya, dll.

btw Anda mungkin sudah tahu ini, tetapi bagi mereka yang tidak dan tersandung pertanyaan Anda pada pencarian:

{-# OPTIONS_GHC -fno-warn-orphans #-}

Edit:

Saya mengakui masalah yang disebutkan Yitz dalam jawabannya sebagai masalah nyata. Namun saya melihat tidak menggunakan contoh yatim piatu sebagai masalah juga, dan saya mencoba untuk memilih "paling sedikit dari semua kejahatan", yang merupakan cara yang bijaksana untuk menggunakan contoh yatim piatu.

Saya hanya menggunakan tanda seru dalam jawaban singkat saya karena pertanyaan Anda menunjukkan bahwa Anda sudah sangat menyadari masalah tersebut. Kalau tidak, saya akan kurang antusias :)

Sedikit pengalihan, tapi yang saya yakini adalah solusi sempurna di dunia yang sempurna tanpa kompromi:

Saya percaya bahwa masalah yang disebutkan Yitz (tidak mengetahui contoh mana yang dipilih) dapat diselesaikan dalam sistem pemrograman "holistik" di mana:

  • Anda tidak hanya mengedit file teks secara primitif, tetapi lebih dibantu oleh lingkungan (misalnya, penyelesaian kode hanya menyarankan hal-hal dari jenis yang relevan dll)
  • Bahasa "level bawah" tidak memiliki dukungan khusus untuk kelas-tipe, dan sebagai gantinya tabel fungsi diteruskan secara eksplisit
  • Tapi, lingkungan pemrograman "tingkat yang lebih tinggi" menampilkan kode dengan cara yang mirip dengan bagaimana Haskell disajikan sekarang (Anda biasanya tidak akan melihat tabel fungsi diteruskan), dan memilih kelas tipe eksplisit untuk Anda ketika mereka jelas (untuk contoh semua case Functor hanya memiliki satu pilihan) dan ketika ada beberapa contoh (Zipping list Applicative atau list-monad Applicative, First / Last / lift mungkin Monoid) memungkinkan Anda memilih instans mana yang akan digunakan.
  • Bagaimanapun, bahkan ketika instans dipilih untuk Anda secara otomatis, lingkungan dengan mudah memungkinkan Anda untuk melihat instans mana yang digunakan, dengan antarmuka yang mudah (antarmuka hyperlink atau arahkan kursor atau semacamnya)

Kembali dari dunia fantasi (atau semoga masa depan), sekarang: Saya sarankan mencoba menghindari kejadian yatim piatu sambil tetap menggunakannya saat Anda "benar-benar perlu"

yairchu
sumber
5
Ya, tapi bisa dibilang setiap kejadian itu adalah kesalahan dari beberapa urutan. Contoh buruk di control-monad-exception-mtl dan monads-fd untuk Either muncul di pikiran. Akan kurang menonjol jika masing-masing modul tersebut dipaksa untuk menentukan tipe mereka sendiri atau menyediakan pembungkus tipe baru. Hampir setiap kasus yatim piatu pusing menunggu untuk terjadi, dan jika tidak ada hal lain yang memerlukan kewaspadaan konstan Anda untuk memastikan bahwa itu diimpor atau tidak sesuai.
Edward KMETT
2
Terima kasih. Saya pikir saya akan menggunakannya dalam situasi khusus ini, tetapi berkat Yitz, saya sekarang memiliki apresiasi yang lebih baik tentang masalah apa yang dapat mereka sebabkan.
Dan Dyer
37

Anak yatim piatu memang mengganggu, tapi menurut saya terkadang diperlukan. Saya sering menggabungkan perpustakaan di mana sebuah tipe berasal dari satu perpustakaan dan kelas berasal dari perpustakaan lain. Tentu saja penulis pustaka ini tidak dapat diharapkan untuk memberikan contoh untuk setiap kombinasi tipe dan kelas yang memungkinkan. Jadi saya harus menyediakannya, jadi mereka yatim piatu.

Gagasan bahwa Anda harus membungkus tipe dalam tipe baru ketika Anda perlu memberikan contoh adalah ide dengan manfaat teoritis, tetapi itu terlalu membosankan dalam banyak keadaan; Ini adalah jenis ide yang diajukan oleh orang-orang yang tidak menulis kode Haskell untuk mencari nafkah. :)

Jadi lanjutkan dan berikan contoh yatim piatu. Mereka tidak berbahaya.
Jika Anda dapat merusak ghc dengan instance orphan maka itu adalah bug dan harus dilaporkan seperti itu. (Bug yang dimiliki / dimiliki ghc tentang tidak mendeteksi banyak instance tidaklah sulit untuk diperbaiki.)

Tetapi ketahuilah bahwa suatu saat di masa mendatang, orang lain mungkin menambahkan beberapa contoh seperti yang sudah Anda miliki, dan Anda mungkin mendapatkan kesalahan (waktu kompilasi).

agustus
sumber
2
Contoh yang bagus adalah (Ord k, Arbitrary k, Arbitrary v) ⇒ Arbitrary (Map k v)saat menggunakan QuickCheck.
Erik Kaplun
17

Dalam hal ini, menurut saya penggunaan orphan instance baik-baik saja. Aturan umum bagi saya adalah - Anda dapat mendefinisikan sebuah instance jika Anda "memiliki" kelas tipe atau jika Anda "memiliki" tipe datanya (atau beberapa komponen darinya - misalnya, instance untuk Maybe MyData juga baik-baik saja, setidaknya terkadang). Dalam batasan tersebut, di mana Anda memutuskan untuk meletakkan contoh adalah urusan Anda sendiri.

Ada satu pengecualian lebih lanjut - jika Anda tidak memiliki kelas tipe atau tipe datanya, tetapi menghasilkan biner dan bukan pustaka, maka tidak masalah juga.

sclv
sumber
5

(Saya tahu saya terlambat ke pesta tetapi ini mungkin masih berguna untuk orang lain)

Anda dapat menyimpan instance orphan dalam modul mereka sendiri, lalu jika ada yang mengimpor modul itu secara khusus karena mereka membutuhkannya dan mereka dapat menghindari mengimpornya jika menyebabkan masalah.

Trystan Spangler
sumber
3

Sejalan dengan ini, saya memahami perpustakaan WRT posisi kamp instans anti-yatim piatu, tetapi untuk target yang dapat dieksekusi, bukankah instans yatim piatu baik-baik saja?

mxc
sumber
3
Dalam hal tidak sopan kepada orang lain, Anda benar. Tetapi Anda membuka diri terhadap potensi masalah di masa depan jika contoh yang sama pernah didefinisikan di masa depan di suatu tempat dalam rantai ketergantungan Anda. Jadi dalam kasus ini, terserah Anda untuk memutuskan apakah itu sepadan dengan risikonya.
Yitz
5
Di hampir semua kasus penerapan orphan instance dalam executable, itu untuk mengisi celah yang Anda inginkan sudah ditentukan untuk Anda. Jadi, jika instance muncul di upstream, error kompilasi yang dihasilkan hanyalah sinyal yang berguna untuk memberi tahu Anda bahwa Anda dapat menghapus deklarasi instance tersebut.
Ben