Hai, Habr.
Hari ini kita akan mempertimbangkan pola FP seperti Has
-class. Ini adalah hal yang agak menarik karena beberapa alasan: pertama, kami akan sekali lagi memastikan bahwa ada pola di FP. Kedua, ternyata penerapan pola ini dapat dipercayakan ke mesin, yang ternyata merupakan trik yang agak menarik dengan typeclasses (dan perpustakaan Hackage), yang sekali lagi menunjukkan kegunaan praktis dari ekstensi sistem tipe di luar Haskell 2010 dan IMHO jauh lebih menarik daripada pola ini sendiri. Ketiga, kesempatan untuk kucing.

Namun, mungkin ada baiknya memulai dengan deskripsi tentang apa kelas Has
, terutama karena tidak ada deskripsi singkat (dan, terutama, yang berbahasa Rusia).
Jadi, bagaimana Haskell menyelesaikan masalah mengelola beberapa lingkungan read-only global yang dibutuhkan beberapa fungsi berbeda? Bagaimana, misalnya, konfigurasi global dari aplikasi diekspresikan?
Solusi yang paling jelas dan langsung adalah bahwa jika suatu fungsi membutuhkan nilai tipe Env
, maka Anda cukup meneruskan nilai tipe Env
ke fungsi ini!
iNeedEnv :: Env -> Foo iNeedEnv env =
Namun, sayangnya, fungsi seperti itu tidak terlalu komposer, terutama dibandingkan dengan beberapa objek lain yang biasa kita gunakan di Haskell. Misalnya, dibandingkan dengan monad.
Sebenarnya, solusi yang lebih umum adalah untuk membungkus fungsi-fungsi yang memerlukan akses ke lingkungan Env
di monad Reader Env
:
import Control.Monad.Reader data Env = Env { someConfigVariable :: Int , otherConfigVariable :: [String] } iNeedEnv :: Reader Env Foo iNeedEnv = do
Ini dapat digeneralisasi lebih banyak lagi, yang cukup menggunakan typeclass MonadReader
dan cukup ubah jenis fungsinya:
iNeedEnv :: MonadReader Env m => m Foo iNeedEnv =
Sekarang tidak masalah bagi kami di mana tumpukan monadik kita berada, selama kita bisa mendapatkan nilai tipe Env
dari itu (dan kita secara eksplisit mengungkapkan ini dalam jenis fungsi kita). Kami tidak peduli jika seluruh tumpukan memiliki fitur lain seperti IO
atau penanganan kesalahan melalui MonadError
:
someCaller :: (MonadIO m, MonadReader Env m, MonadError Err m) => m Bar someCaller = do theFoo <- iNeedEnv ...
Dan, omong-omong, sedikit lebih tinggi, saya benar-benar berbohong ketika saya mengatakan bahwa pendekatan secara eksplisit melewati argumen ke fungsi tidak komposabel seperti monad: tipe fungsional "sebagian diterapkan" r ->
adalah monad, dan, lebih dari itu, cukup contoh yang sah dari kelas MonadReader r
. Pengembangan intuisi yang sesuai ditawarkan kepada pembaca sebagai latihan.
Bagaimanapun, ini adalah langkah bagus menuju modularitas. Mari kita lihat kemana dia menuntun kita.
Kenapa harus
Mari kita bekerja pada beberapa jenis layanan web, yang antara lain memiliki komponen berikut:
- Lapisan akses DB
- server web
- modul cron-like diaktifkan.
Masing-masing modul ini dapat memiliki konfigurasi sendiri:
- rincian akses ke database,
- host dan port untuk server web,
- interval operasi pengatur waktu.
Kita dapat mengatakan bahwa konfigurasi keseluruhan dari seluruh aplikasi adalah kombinasi dari semua pengaturan ini (dan, mungkin, sesuatu yang lain).
Untuk kesederhanaan, anggaplah bahwa API dari setiap modul hanya terdiri dari satu fungsi:
setupDatabase
startServer
runCronJobs
Masing-masing fitur ini memerlukan konfigurasi yang sesuai. Kami sudah belajar bahwa MonadReader
adalah praktik yang baik, tetapi seperti apa tipe lingkungannya?
Solusi yang paling jelas adalah sesuatu
data AppConfig = AppConfig { dbCredentials :: DbCredentials , serverAddress :: (Host, Port) , cronPeriodicity :: Ratio Int } setupDatabase :: MonadReader AppConfig m => m Db startServer :: MonadReader AppConfig m => m Server runCronJobs :: MonadReader AppConfig m => m ()
Kemungkinan besar, fitur-fitur ini akan membutuhkan MonadIO
dan, mungkin, sesuatu yang lain, tetapi ini tidak begitu penting untuk diskusi kita.
Faktanya, kami hanya melakukan hal yang mengerikan. Mengapa Nah, begitu saja:
- Kami telah menambahkan koneksi yang tidak perlu antara komponen yang sangat berbeda. Idealnya, layer DB seharusnya tidak tahu apa-apa tentang beberapa jenis server web. Dan, tentu saja, kita tidak boleh mengkompilasi ulang modul untuk bekerja dengan database ketika mengubah daftar opsi konfigurasi untuk server web.
- Ini tidak akan berfungsi sama sekali jika kita tidak dapat mengedit kode sumber untuk beberapa modul. Misalnya, apa yang harus saya lakukan jika modul cron diimplementasikan di beberapa perpustakaan pihak ketiga yang tidak tahu apa-apa tentang kasus pengguna khusus kami?
- Kami menambahkan peluang untuk membuat kesalahan. Misalnya, apa itu
serverAddress
? Apakah ini alamat yang harus didengarkan oleh server web, atau apakah alamat server database? Menggunakan satu tipe besar untuk semua opsi meningkatkan kemungkinan tabrakan tersebut. - Sekilas kita tidak bisa menyimpulkan fungsi tanda tangan modul mana yang menggunakan bagian mana dari konfigurasi. Semuanya memiliki akses ke semuanya!
Jadi apa solusinya untuk ini semua? Seperti yang Anda tebak dari judul artikel, ini
Has
Pola
Faktanya, setiap modul tidak peduli dengan tipe seluruh lingkungan, asalkan tipe ini memiliki data yang dibutuhkan untuk modul. Ini paling mudah ditunjukkan dengan sebuah contoh.
Pertimbangkan modul untuk bekerja dengan database dan anggap modul menentukan tipe yang berisi semua konfigurasi yang dibutuhkan modul:
data DbConfig = DbConfig { dbCredentials :: DbCredentials , ... }
Has
-pattern direpresentasikan sebagai typeclass berikut:
class HasDbConfig rec where getDbConfig :: rec -> DbConfig
Maka tipe setupDatabase
akan terlihat seperti
setupDatabase :: (MonadReader rm, HasDbConfig r) => m Db
dan dalam tubuh fungsi kita hanya perlu menggunakan asks $ foo . getDbConfig
asks $ foo . getDbConfig
mana kami menggunakan asks foo
sebelumnya karena lapisan abstraksi tambahan yang baru saja kami tambahkan.
Demikian pula, kita akan memiliki HasWebServerConfig
dan HasCronConfig
.
Bagaimana jika beberapa fungsi menggunakan dua modul yang berbeda? Konstan hanya kompatibel!
doSmthWithDbAndCron :: (MonadReader rm, HasDbConfig r, HasCronConfig r) => ...
Bagaimana dengan implementasi typeclasses ini?
Kami masih memiliki AppConfig
di level tertinggi dari aplikasi kami (baru saja modul tidak mengetahuinya), dan untuk itu kita dapat menulis:
data AppConfig = AppConfig { dbConfig :: DbConfig , webServerConfig :: WebServerConfig , cronConfig :: CronConfig } instance HasDbConfig AppConfig where getDbConfig = dbConfig instance HasWebServerConfig AppConfig where getWebServerConfig = webServerCOnfig instance HasCronConfig AppConfig where getCronConfig = cronConfig
Terlihat bagus sejauh ini. Namun, pendekatan ini memiliki satu masalah - terlalu banyak menulis , dan kami akan memeriksanya secara lebih rinci di posting berikutnya.