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 next
parameter 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 next
untuk komposisi, jenisnya p1
sama dengan program kami (yaitu 3 perintah):
p1 :: DSL (DSL (DSL next))
Dalam contoh khusus ini, menggunakan next
seperti ini tampaknya sedikit aneh, tetapi penting jika kita ingin tindakan kita memiliki variabel tipe yang berbeda. Kami mungkin ingin mengetik get
dan set
, misalnya.
Perhatikan bagaimana next
bidang berbeda untuk setiap tindakan. Ini mengisyaratkan bahwa kita dapat menggunakannya untuk membuat DSL
functor:
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 deriving
untuk membuat instance secara otomatis dengan mengaktifkan DeriveFunctor
ekstensi.
Langkah selanjutnya adalah Free
tipe itu sendiri. Itulah yang kami gunakan untuk mewakili struktur AST kami , membangun di atas DSL
tipe 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 next
untuk 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, Free
adalah 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 do
notasi untuk membuat ekspresi DSL kita lebih baik. Satu-satunya pertanyaan adalah untuk apa memasukkan next
? Nah, idenya adalah menggunakan Free
struktur untuk komposisi, jadi kami hanya akan menempatkan Return
untuk 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 Free
dan di Return
semua tempat. Untungnya, ada pola kita dapat memanfaatkan: cara kita "angkat" tindakan DSL ke Free
selalu sama-kita bungkus dalam Free
dan menerapkan Return
untuk 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 p4
terlihat 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 follow
yang 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 follow
dapat digunakan dalam program kami seperti get
atau 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 DSL
fragmen, bahkan yang tidak berakhir dengan end
. Untungnya, kita dapat membuat versi "aman" dari fungsi yang hanya menerima program yang ditutup dengan end
menyetel tanda tangan jenis input (forall a. Free DSL a) -> IO ()
. Sementara tanda tangan lama menerima a Free DSL a
untuk apa pun a
(seperti Free DSL String
, Free DSL Int
dan sebagainya), versi ini hanya menerima Free DSL a
yang 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 runIO
tipe ini karena itu tidak akan berfungsi dengan baik untuk panggilan rekursif kami. Namun, kami dapat memindahkan definisi runIO
menjadi where
blok safeRunIO
dan mendapatkan efek yang sama tanpa mengekspos kedua versi fungsi.)
Menjalankan kode kita IO
bukan 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.