Hai, Habr.
Terakhir kali, kami menjelaskan Has pola, menguraikan masalah yang dipecahkan, dan menulis beberapa contoh spesifik:
 instance HasDbConfig AppConfig where getDbConfig = dbConfig instance HasWebServerConfig AppConfig where getWebServerConfig = webServerConfig instance HasCronConfig AppConfig where getCronConfig = cronConfig 
Itu terlihat bagus. Kesulitan apa yang bisa timbul di sini?

Nah, mari kita pikirkan contoh apa lagi yang mungkin kita butuhkan. Pertama-tama, tipe konkret dengan konfigurasi sendiri adalah kandidat yang baik untuk implementasi (sepele) dari typeclasses ini, yang memberi kita tiga contoh lagi di mana setiap metode diimplementasikan melalui id , misalnya
 instance HasDbConfig DbConfig where getDbConfig = id 
Mereka memungkinkan kita untuk dengan mudah menulis tes individual atau utilitas pembantu yang independen dari seluruh AppConfig .
Ini sudah membosankan, tetapi masih berlanjut. Sangat mudah untuk membayangkan bahwa beberapa tes integrasi memeriksa interaksi sepasang modul, dan kami masih tidak ingin bergantung pada konfigurasi seluruh aplikasi, jadi sekarang kita perlu menulis enam contoh (dua per jenis), masing-masing akan dikurangi menjadi fst atau snd . Misalnya, untuk DbConfig :
 instance HasDbConfig (DbConfig, b) where getDbConfig = fst instance HasDbConfig (a, DbConfig) where getDbConfig = snd 
Horor Diharapkan bahwa kita tidak akan pernah perlu menguji operasi tiga modul secara bersamaan - jika tidak, Anda harus menulis sembilan contoh yang membosankan. Bagaimanapun, saya pribadi sudah sangat tidak nyaman, dan saya lebih suka menghabiskan beberapa jam mengotomatisasi masalah ini daripada beberapa menit menulis selusin baris kode tambahan.
Jika Anda tertarik untuk menyelesaikan masalah ini secara umum, terlebih lagi, ini tergantung pada jenisnya, dan bagaimana semuanya akan berakhir seperti kucing Haskell - Welkom.
Meringkas kelas Has
Pertama, perhatikan bahwa kami memiliki kelas yang berbeda untuk lingkungan yang berbeda. Ini dapat mengganggu pembuatan solusi universal, jadi kami mengambil lingkungan dalam parameter terpisah:
 class Has part record where extract :: record -> part 
Kita dapat mengatakan bahwa Has part record berarti bahwa beberapa nilai part jenis dapat diekstraksi dari nilai record tipe. Dalam istilah-istilah ini, HasDbConfig lama kami yang baik menjadi Has DbConfig , dan juga untuk typeclasses lain yang kami tulis sebelumnya. Ternyata perubahan hampir murni sintaksis, dan, misalnya, jenis salah satu fungsi dari posting kami sebelumnya berubah dari
 doSmthWithDbAndCron :: (MonadReader rm, HasDbConfig r, HasCronConfig r) => ... 
masuk
 doSmthWithDbAndCron :: (MonadReader rm, Has DbConfig r, Has CronConfig r) => ... 
Satu-satunya perubahan adalah beberapa ruang di tempat yang tepat.
Selain itu, kami tidak kehilangan banyak dalam inferensi tipe: timer masih dapat menampilkan nilai pengembalian yang diperlukan dari extract dalam konteks sekitarnya dalam sebagian besar kasus yang ditemukan dalam praktek.
Sekarang kami tidak peduli dengan jenis lingkungan tertentu, mari kita lihat catatan mana yang dapat menerapkan kelas Has part record untuk bagian tetap. Tugas ini memiliki struktur induktif yang baik:
- Setiap jenis memiliki sendiri: Has record recorddiimplementasikan dalam cara yang sepele (extract = id).
- Jika recordadalah produk dari tiperec1danrec2, makaHas part recorddiimplementasikan jika dan hanya jika salah satuHas part rec1atauHas part rec2.
- Jika recordadalah jumlah dari tiperec1danrec2, makaHas part recorddiimplementasikan jika dan hanya jikaHas part rec1danHas part rec2. Meskipun prevalensi praktis dari kasus ini dalam konteks ini tidak jelas, masih perlu disebutkan untuk kelengkapannya.
Jadi, sepertinya kami telah merumuskan sketsa algoritma untuk secara otomatis menentukan apakah Has part record diimplementasikan untuk part dan record data!
Untungnya, alasan induktif semacam itu sangat cocok dengan mekanisme Haskell Generics . Secara singkat dan menyederhanakan, Generics adalah salah satu metode metaprogramming yang digeneralisasi di Haskell, yang dihasilkan dari pengamatan bahwa setiap tipe dapat berupa tipe penjumlahan, tipe produk, atau tipe dasar konstruksi tunggal dengan satu bidang.
Saya tidak akan menulis tutorial lain tentang obat generik, jadi lanjutkan saja ke kode.
Upaya pertama
Kami akan menggunakan metode klasik implementasi Generic dari Has kami melalui kelas tambahan GHas :
 class GHas part grecord where gextract :: grecord p -> part 
Di sini grecord adalah representasi Generic dari tipe record kami.
Implementasi GHas mengikuti struktur induktif yang kami catat di atas:
 instance GHas record (K1 i record) where gextract (K1 x) = x instance GHas part record => GHas part (M1 it record) where gextract (M1 x) = gextract x instance GHas part l => GHas part (l :*: r) where gextract (l :*: _) = gextract l instance GHas part r => GHas part (l :*: r) where gextract (_ :*: r) = gextract r 
- K1sesuai dengan kasus dasar.
- M1- Metadata khusus-generik yang tidak kita perlukan dalam tugas, jadi kita abaikan saja dan lewati.
- Contoh pertama untuk tipe produk l :*: rsesuai dengan kasus ketika bagian "kiri" dari produk memiliki nilai tipepartkita butuhkan (mungkin, secara rekursif).
- Demikian pula, contoh kedua untuk jenis produk l :*: rsesuai dengan kasus ketika bagian "kanan" dari produk memiliki nilai jenispartkita butuhkan (secara alami, juga, mungkin, secara rekursif).
Kami hanya mendukung jenis produk di sini. Kesan subjektif saya adalah bahwa jumlah tidak terlalu sering digunakan dalam konteks untuk MonadReader dan kelas yang serupa, sehingga MonadReader dapat diabaikan untuk menyederhanakan pertimbangan.
Selain itu, penting untuk dicatat bahwa masing-masing -ary type-product (a1, ..., an) dapat direpresentasikan sebagai komposisi pasangan (a1, (a2, (a3, (..., an)))) , jadi saya membiarkan diri saya mengaitkan jenis produk dengan pasangan.
Dengan GHas kami, Anda dapat menulis implementasi default untuk Has yang menggunakan generik:
 class Has part record where extract :: record -> part default extract :: Generic record => record -> part extract = gextract . from 
Selesai
Atau tidak?
Masalah
Jika kami mencoba mengkompilasi kode ini, kami akan melihat bahwa kode itu tidak taypechaetsya bahkan tanpa ada upaya untuk menggunakan implementasi ini secara default, melaporkan beberapa contoh yang tumpang tindih di sana. Lebih buruk lagi, contoh ini sama dalam beberapa hal. Sepertinya sudah waktunya untuk mencari tahu bagaimana mekanisme untuk menyelesaikan instance dalam Haskell bekerja.
Semoga kita punya
 instance context => Foo barPattern bazPattern where ... 
(Ngomong-ngomong, benda setelah => ini disebut kepala instance.)
Tampaknya wajar untuk membaca ini sebagai
Mari kita pilih contoh untuk Foo bar baz . Jika context puas, maka Anda dapat memilih contoh ini asalkan bar dan baz sesuai dengan barPattern dan bazPattern .
Namun, ini adalah salah tafsir, dan justru sebaliknya:
Mari kita pilih contoh untuk Foo bar baz . Jika bar dan baz berhubungan dengan barPattern dan bazPattern , maka kami memilih instance ini dan menambahkan context ke daftar konstanta yang harus diselesaikan.
Sekarang sudah jelas apa masalahnya. Mari kita lihat lebih dekat pasangan contoh berikut:
 instance GHas part l => GHas part (l :*: r) where gextract (l :*: _) = gextract l instance GHas part r => GHas part (l :*: r) where gextract (_ :*: r) = gextract r 
Mereka memiliki kepala contoh yang sama, jadi tidak heran mereka berpotongan! Selain itu, tidak ada yang lebih spesifik dari yang lain.
Selain itu, tidak ada cara untuk memperbaiki kasus-kasus ini sehingga tidak lagi tumpang tindih. Nah, selain menambahkan lebih banyak parameter GHas .
Tipe ekspresif bergegas menyelamatkan!
Solusi untuk masalah ini adalah dengan pra-menghitung "jalan" ke nilai yang menarik bagi kami, dan menggunakan jalur ini untuk memandu pilihan contoh.
Karena kami sepakat untuk tidak mendukung tipe penjumlahan, jalur dalam arti literal merupakan urutan belokan kiri atau kanan dalam tipe produk (yaitu, pilihan komponen pertama atau kedua dari suatu pasangan), berakhir dengan penunjuk “HERE” yang besar, segera setelah kami menemukan jenis yang diinginkan . Kami menulis ini:
 data Path = L Path | R Path | Here deriving (Show) 
Sebagai contohPertimbangkan jenis-jenis berikut:
 data DbConfig = DbConfig { dbAddress :: DbAddress , dbUsername :: Username , dbPassword :: Password } data AppConfig = AppConfig { dbConfig :: DbConfig , webServerConfig :: WebServerConfig , cronConfig :: CronConfig } 
Apa saja contoh jalur dari AppConfig ?
- Untuk DbConfig⟶L Here.
- Ke WebServerConfig⟶R (L Here).
- Ke CronConfig⟶R (R Here).
- Ke DbAddress⟶L (L Here).
 Apa yang bisa menjadi hasil pencarian untuk nilai dari tipe yang diinginkan? Dua opsi sudah jelas: kita dapat menemukannya atau tidak menemukannya. Tetapi pada kenyataannya, semuanya sedikit lebih rumit: kita dapat menemukan lebih dari satu nilai dari jenis ini. Tampaknya perilaku yang paling masuk akal dalam kasus kontroversial ini juga akan menjadi pesan kesalahan. Setiap pilihan nilai tertentu akan memiliki jumlah keacakan tertentu.
Memang, perhatikan contoh layanan web standar kami. Jika seseorang ingin mendapatkan nilai jenis (Host, Port) , haruskah itu alamat server database atau alamat server web? Lebih baik tidak mengambil risiko.
Bagaimanapun, mari kita ungkapkan ini dalam kode:
 data MaybePath = NotFound | Conflict | Found Path deriving (Show) 
Kami memisahkan NotFound dan Conflict , karena penanganan kasus-kasus ini pada dasarnya berbeda: jika kami mendapatkan NotFound di salah satu cabang dari jenis produk kami, maka tidak ada salahnya untuk menemukan nilai yang diinginkan di beberapa cabang lain, sementara Conflict di cabang mana pun segera berarti penuh sebuah kegagalan.
Sekarang kami mempertimbangkan kasus khusus jenis produk (yang, seperti yang kami sepakati, kami anggap sebagai pasangan). Bagaimana menemukan nilai tipe yang diinginkan di dalamnya? Anda dapat menjalankan pencarian secara rekursif di setiap komponen pasangan, mendapatkan hasil p1 dan p2 masing-masing, dan kemudian menggabungkannya.
Karena kita berbicara tentang pilihan instance timeclasses yang terjadi selama kompilasi, kita sebenarnya membutuhkan perhitungan compiltime, yang diekspresikan dalam Haskell melalui perhitungan pada jenis (bahkan jika tipe diwakili melalui istilah yang diangkat di alam semesta menggunakan DataKinds ). Dengan demikian, fungsi seperti pada tipe direpresentasikan sebagai tipe keluarga:
 type family Combine p1 p2 where Combine ('Found path) 'NotFound = 'Found ('L path) Combine 'NotFound ('Found path) = 'Found ('R path) Combine 'NotFound 'NotFound = 'NotFound Combine _ _ = 'Conflict 
Fungsi ini mewakili beberapa kasus:
- Jika salah satu pencarian rekursif berhasil, dan yang lainnya NotFoundkeNotFound, maka kami mengambil jalur dari pencarian yang sukses dan menambahkan belokan ke arah yang benar.
- Jika kedua pencarian rekursif berakhir dengan NotFound, maka jelas seluruh pencarian berakhir denganNotFound.
- Dalam kasus lain, kita mendapatkan Conflict.
Sekarang kita akan menulis fungsi tipe-level yang mengambil part akan ditemukan, dan representasi Generic dari tipe untuk menemukan part , dan mencari:
 type family Search part (grecord :: k -> *) :: MaybePath where Search part (K1 _ part) = 'Found 'Here Search part (K1 _ other) = 'NotFound Search part (M1 _ _ x) = Search part x Search part (l :*: r) = Combine (Search part l) (Search part r) Search _ _ = 'NotFound 
Perhatikan bahwa kami mendapatkan sesuatu yang sangat mirip dalam arti dengan upaya kami sebelumnya dengan GHas . Ini diharapkan, karena kami sebenarnya mereproduksi algoritma yang kami coba ekspresikan melalui timeclasses.
GHas , yang tersisa bagi kita adalah menambahkan parameter tambahan ke kelas ini, yang bertanggung jawab untuk jalur yang ditemukan sebelumnya, dan yang akan berfungsi untuk memilih contoh spesifik:
 class GHas (path :: Path) part grecord where gextract :: Proxy path -> grecord p -> part 
Kami juga menambahkan argumen tambahan untuk gextract sehingga kompiler dapat memilih contoh yang benar untuk jalur yang diberikan (yang harus disebutkan dalam tanda tangan fungsi untuk ini).
Sekarang menulis instance cukup mudah:
 instance GHas 'Here record (K1 i record) where gextract _ (K1 x) = x instance GHas path part record => GHas path part (M1 it record) where gextract proxy (M1 x) = gextract proxy x instance GHas path part l => GHas ('L path) part (l :*: r) where gextract _ (l :*: _) = gextract (Proxy :: Proxy path) l instance GHas path part r => GHas ('R path) part (l :*: r) where gextract _ (_ :*: r) = gextract (Proxy :: Proxy path) r 
Memang, kami cukup memilih turunan yang diinginkan berdasarkan jalur di path yang kami hitung sebelumnya.
Bagaimana sekarang untuk menulis implementasi default kami dari extract :: record -> part fungsi extract :: record -> part di kelas Has ? Kami memiliki beberapa kondisi:
- recordharus mengimplementasikan- Genericsehingga mekanisme generic dapat diterapkan, sehingga kita mendapatkan- Generic record.
- Fungsi Searchharus menemukanpartdalamrecord(atau lebih tepatnya, dalam representasiGenericrecord, yang dinyatakan sebagaiRep record). Dalam kode, ini terlihat sedikit lebih tidak biasa:Search part (Rep record) ~ 'Found path. Catatan ini berarti batasan bahwa hasil bagianSearch part (Rep record)harus sama dengan'Found pathuntuk beberapapath(yang, pada kenyataannya, menarik bagi kami).
- Kita harus dapat menggunakan GHasbersama denganpart, representasi generik darirecorddanpathdari langkah terakhir, yang berubah menjadiGHas path part (Rep record).
Kami akan bertemu dengan dua konstanta terakhir beberapa kali lagi, jadi berguna untuk menempatkannya dalam sinonim const yang terpisah:
 type SuccessfulSearch part record path = (Search part (Rep record) ~ 'Found path, GHas path part (Rep record)) 
Dengan sinonim ini, kami dapat
 class Has part record where extract :: record -> part default extract :: forall path. (Generic record, SuccessfulSearch part record path) => record -> part extract = gextract (Proxy :: Proxy path) . from 
Sekarang semuanya!
Menggunakan Generic Has
Untuk melihat semua ini dalam tindakan, kami akan menulis beberapa contoh umum untuk boneka:
 instance SuccessfulSearch a (a0, a1) path => Has a (a0, a1) instance SuccessfulSearch a (a0, a1, a2) path => Has a (a0, a1, a2) instance SuccessfulSearch a (a0, a1, a2, a3) path => Has a (a0, a1, a2, a3) 
Di sini SuccessfulSearch a (a0, ..., an) path bertanggung jawab atas fakta bahwa a terjadi di antara a0, ..., an tepat sekali.
Semoga sekarang kita memiliki yang baik
 data AppConfig = AppConfig { dbConfig :: DbConfig , webServerConfig :: WebServerConfig , cronConfig :: CronConfig } 
dan kami ingin menampilkan Has DbConfig , Has WebServerConfig dan Has CronConfig . Cukup memasukkan DeriveAnyClass dan DeriveAnyClass dan menambahkan deklarasi deriving benar:
 data AppConfig = AppConfig { dbConfig :: DbConfig , webServerConfig :: WebServerConfig , cronConfig :: CronConfig } deriving (Generic, Has DbConfig, Has WebServerConfig, Has CronConfig) 
Kami beruntung (atau kami cukup berwawasan luas) untuk mengatur argumen untuk Has sehingga nama tipe bersarang DeriveAnyClass , sehingga kami dapat mengandalkan mekanisme DeriveAnyClass untuk meminimalkan coretan.
Keselamatan adalah yang utama
Bagaimana jika kita tidak memiliki tipe apa pun?
 data AppConfig = AppConfig { dbConfig :: DbConfig , webServerConfig :: WebServerConfig } deriving (Generic, Has CronConfig) 
Tidak, kami mendapatkan kesalahan tepat pada titik definisi tipe:
 Spec.hs:35:24: error: • Couldn't match type ''NotFound' with ''Found path0' arising from the 'deriving' clause of a data type declaration • When deriving the instance for (Has CronConfig AppConfig) | 35 | } deriving (Generic, Has CronConfig) | ^^^^^^^^^^^^^^ 
Bukan pesan kesalahan yang ramah, tetapi bahkan dari itu Anda masih bisa mengerti apa masalahnya: frekuensi aneh NotFound frekuensi aneh CronConfig .
Bagaimana jika kita memiliki beberapa bidang dengan tipe yang sama?
 data AppConfig = AppConfig { prodDbConfig :: DbConfig , qaDbConfig :: DbConfig , webServerConfig :: WebServerConfig , cronConfig :: CronConfig } deriving (Generic, Has DbConfig) 
Tidak, seperti yang diharapkan:
 Spec.hs:37:24: error: • Couldn't match type ''Conflict' with ''Found path0' arising from the 'deriving' clause of a data type declaration • When deriving the instance for (Has DbConfig AppConfig) | 37 | } deriving (Generic, Has DbConfig) | ^^^^^^^^^^^^ 
Segalanya tampak sangat baik.
Untuk meringkas
Jadi, kami akan mencoba merumuskan secara singkat metode yang diusulkan.
Misalkan kita memiliki semacam typklass, dan kami ingin secara otomatis menampilkan instansinya sesuai dengan beberapa aturan rekursif. Maka kita dapat menghindari ambiguitas (dan umumnya mengungkapkan aturan-aturan ini jika tidak bersifat sementara dan tidak cocok dengan mekanisme standar untuk menyelesaikan kasus) sebagai berikut:
- Kami menyandikan aturan rekursif dalam bentuk tipe data induktif T
- Kami akan menulis fungsi pada tipe (dalam bentuk tipe keluarga) untuk perhitungan awal dari nilai vtipeT(atau, dalam hal Haskell, tipevtipeT- di mana tipe dependen saya), yang menggambarkan urutan spesifik dari langkah-langkah yang perlu diambil.
- Gunakan vini sebagai argumen tambahan untukGenerichelper untuk menentukan urutan spesifik instance yang sekarang cocok dengan nilaiv.
Yah, itu dia!
Dalam posting berikut, kita akan melihat beberapa ekstensi elegan (serta batasan elegan) dari pendekatan ini.
Oh, dan ya. Sangat menarik untuk melacak urutan generalisasi kami.
- Dimulai dengan Env -> Foo.
- Tidak cukup umum, bungkus dalam Reader Envmonad.
- Tidak cukup umum, tulis ulang dengan MonadReader Env m.
- Tidak cukup umum, tulis ulang MonadReader rm, HasEnv r.
- Tidak cukup umum, MonadReader rm, Has Env rmenulisMonadReader rm, Has Env rdan menambahkan generik sehingga kompiler melakukan semuanya di sana.
- Sekarang norma.