Pola aktual sebenarnya jauh lebih umum daripada sekadar akses data. Ini adalah cara ringan untuk menciptakan bahasa khusus domain yang memberi Anda AST, dan kemudian memiliki satu atau lebih penerjemah untuk "mengeksekusi" AST sesuka Anda.
Bagian monad gratis hanyalah cara praktis untuk mendapatkan AST yang dapat Anda rakit menggunakan fasilitas monad standar Haskell (seperti notasi) tanpa harus menulis banyak kode khusus. Ini juga memastikan bahwa DSL Anda dapat dikomposisikan : Anda dapat mendefinisikannya dalam bagian-bagian dan kemudian menyatukan bagian-bagiannya dengan cara yang terstruktur, memungkinkan Anda mengambil keuntungan dari fungsi-fungsi abstraksi seperti Haskell yang normal.
Menggunakan monad gratis memberi Anda struktur DSL komposabel; Yang harus Anda lakukan adalah menentukan bagian. Anda cukup menulis tipe data yang mencakup semua tindakan di DSL Anda. Tindakan ini bisa melakukan apa saja, bukan hanya akses data. Namun, jika Anda menentukan semua data Anda diakses sebagai tindakan, Anda akan mendapatkan AST yang menentukan semua pertanyaan dan perintah untuk penyimpanan data. Anda kemudian dapat menafsirkan ini sesuka Anda: jalankan melawan live database, jalankan melawan tiruan, cukup catat perintah untuk debugging atau bahkan coba optimalkan kueri.
Mari kita lihat contoh yang sangat sederhana untuk, katakanlah, toko nilai kunci. Untuk saat ini, kami hanya akan memperlakukan kunci dan nilai sebagai string, tetapi Anda dapat menambahkan tipe dengan sedikit usaha.
data DSL next = Get String (String -> next)
| Set String String next
| End
The nextparameter memungkinkan kita menggabungkan tindakan. Kita bisa menggunakan ini untuk menulis program yang mendapat "foo" dan menetapkan "bar" dengan nilai itu:
p1 = Get "foo" $ \ foo -> Set "bar" foo End
Sayangnya, ini tidak cukup untuk DSL yang berarti. Karena kami digunakan nextuntuk komposisi, jenisnya p1sama dengan program kami (yaitu 3 perintah):
p1 :: DSL (DSL (DSL next))
Dalam contoh khusus ini, menggunakan nextseperti ini tampaknya sedikit aneh, tetapi penting jika kita ingin tindakan kita memiliki variabel tipe yang berbeda. Kami mungkin ingin mengetik getdan set, misalnya.
Perhatikan bagaimana nextbidang berbeda untuk setiap tindakan. Ini mengisyaratkan bahwa kita dapat menggunakannya untuk membuat DSLfunctor:
instance Functor DSL where
fmap f (Get name k) = Get name (f . k)
fmap f (Set name value next) = Set name value (f next)
fmap f End = End
Sebenarnya, ini adalah satu - satunya cara yang valid untuk menjadikannya Functor, jadi kita dapat menggunakan derivinguntuk membuat instance secara otomatis dengan mengaktifkan DeriveFunctorekstensi.
Langkah selanjutnya adalah Freetipe itu sendiri. Itulah yang kami gunakan untuk mewakili struktur AST kami , membangun di atas DSLtipe tersebut. Anda dapat menganggapnya seperti daftar di tingkat tipe , di mana "kontra" hanya membuat fungsi seperti DSL:
-- compare the two types:
data Free f a = Free (f (Free f a)) | Return a
data List a = Cons a (List a) | Nil
Jadi kita bisa gunakan Free DSL nextuntuk memberikan program dengan ukuran berbeda dengan tipe yang sama:
p2 = Free (Get "foo" $ \ foo -> Free (Set "bar" foo (Free End)))
Yang memiliki tipe yang jauh lebih bagus:
p2 :: Free DSL a
Namun, ekspresi aktual dengan semua konstruktornya masih sangat canggung untuk digunakan! Di sinilah bagian monad masuk. Seperti namanya "monad bebas" menyiratkan, Freeadalah monad-selama f(dalam hal ini DSL) adalah functor:
instance Functor f => Monad (Free f) where
return = Return
Free a >>= f = Free (fmap (>>= f) a)
Return a >>= f = f a
Sekarang kita berada di suatu tempat: kita dapat menggunakan donotasi untuk membuat ekspresi DSL kita lebih baik. Satu-satunya pertanyaan adalah untuk apa memasukkan next? Nah, idenya adalah menggunakan Freestruktur untuk komposisi, jadi kami hanya akan menempatkan Returnuntuk setiap bidang berikutnya dan biarkan notasi melakukan semua plumbing:
p3 = do foo <- Free (Get "foo" Return)
Free (Set "bar" foo (Return ()))
Free End
Ini lebih baik, tapi masih agak canggung. Kami punya Freedan di Returnsemua tempat. Untungnya, ada pola kita dapat memanfaatkan: cara kita "angkat" tindakan DSL ke Freeselalu sama-kita bungkus dalam Freedan menerapkan Returnuntuk next:
liftFree :: Functor f => f a -> Free f a
liftFree action = Free (fmap Return action)
Sekarang, dengan menggunakan ini, kita dapat menulis versi bagus dari setiap perintah kita dan memiliki DSL penuh:
get key = liftFree (Get key id)
set key value = liftFree (Set key value ())
end = liftFree End
Dengan menggunakan ini, inilah cara kami dapat menulis program kami:
p4 :: Free DSL a
p4 = do foo <- get "foo"
set "bar" foo
end
Trik yang rapi adalah bahwa meskipun p4terlihat seperti program imperatif, itu sebenarnya ekspresi yang memiliki nilai
Free (Get "foo" $ \ foo -> Free (Set "bar" foo (Free End)))
Jadi, bagian dari pola monad gratis telah memberi kita DSL yang menghasilkan pohon sintaksis dengan sintaksis yang bagus. Kita juga dapat menulis sub-pohon komposer dengan tidak menggunakan End; misalnya, kita dapat memiliki followyang mengambil kunci, mendapatkan nilainya dan kemudian menggunakannya sebagai kunci itu sendiri:
follow :: String -> Free DSL String
follow key = do key' <- get key
get key'
Sekarang followdapat digunakan dalam program kami seperti getatau set:
p5 = do foo <- follow "foo"
set "bar" foo
end
Jadi kami mendapatkan komposisi dan abstraksi yang bagus untuk DSL kami juga.
Sekarang kita memiliki pohon, kita sampai pada bagian kedua dari pola: penafsir. Kita dapat menafsirkan pohon itu bagaimanapun kita suka hanya dengan pencocokan pola di atasnya. Ini akan memungkinkan kami menulis kode terhadap penyimpanan data nyata IO, serta hal-hal lain. Berikut adalah contoh terhadap penyimpanan data hipotetis:
runIO :: Free DSL a -> IO ()
runIO (Free (Get key k)) =
do res <- getKey key
runIO $ k res
runIO (Free (Set key value next)) =
do setKey key value
runIO next
runIO (Free End) = close
runIO (Return _) = return ()
Ini akan dengan senang hati mengevaluasi setiap DSLfragmen, bahkan yang tidak berakhir dengan end. Untungnya, kita dapat membuat versi "aman" dari fungsi yang hanya menerima program yang ditutup dengan endmenyetel tanda tangan jenis input (forall a. Free DSL a) -> IO (). Sementara tanda tangan lama menerima a Free DSL auntuk apa pun a (seperti Free DSL String, Free DSL Intdan sebagainya), versi ini hanya menerima Free DSL ayang berfungsi untuk setiap kemungkinan a— yang hanya bisa kita buat dengan end. Ini menjamin kami tidak akan lupa untuk menutup koneksi ketika kami selesai.
safeRunIO :: (forall a. Free DSL a) -> IO ()
safeRunIO = runIO
(Kita tidak bisa hanya mulai dengan memberikan runIOtipe ini karena itu tidak akan berfungsi dengan baik untuk panggilan rekursif kami. Namun, kami dapat memindahkan definisi runIOmenjadi whereblok safeRunIOdan mendapatkan efek yang sama tanpa mengekspos kedua versi fungsi.)
Menjalankan kode kita IObukan satu-satunya hal yang bisa kita lakukan. Untuk pengujian, kami mungkin ingin menjalankannya terhadap yang murni State Map. Menulis kode itu adalah latihan yang bagus.
Jadi ini adalah pola penerjemah + monad gratis. Kami membuat DSL, mengambil keuntungan dari struktur monad gratis untuk melakukan semua plumbing. Kita dapat menggunakan notasi dan fungsi monad standar dengan DSL kita. Kemudian, untuk benar-benar menggunakannya, kita harus menafsirkannya; karena pohon pada akhirnya hanyalah sebuah struktur data, kita dapat menafsirkannya bagaimanapun kita suka untuk tujuan yang berbeda.
Ketika kami menggunakan ini untuk mengelola akses ke penyimpanan data eksternal, ini memang mirip dengan pola Repositori. Ini menengah antara penyimpanan data kami dan kode kami, memisahkan keduanya. Namun dalam beberapa hal, ini lebih spesifik: "repositori" selalu merupakan DSL dengan AST eksplisit yang kemudian dapat kita gunakan sesuai keinginan kita.
Namun, polanya sendiri lebih umum dari itu. Ini dapat digunakan untuk banyak hal yang tidak perlu melibatkan database atau penyimpanan eksternal. Masuk akal di mana pun Anda ingin mengontrol efek atau beberapa target untuk DSL.