Apa pola "Free Monad + Interpreter"?

95

Saya telah melihat orang-orang berbicara tentang Monad Gratis dengan Juru Bahasa , khususnya dalam konteks akses data. Apa pola ini? Kapan saya ingin menggunakannya? Bagaimana cara kerjanya, dan bagaimana saya akan menerapkannya?

Saya mengerti (dari tulisan seperti ini ) bahwa ini tentang memisahkan model dari akses data. Apa bedanya dengan pola Repositori yang terkenal? Mereka tampaknya memiliki motivasi yang sama.

Benjamin Hodgson
sumber

Jawaban:

138

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.

Tikhon Jelvis
sumber
6
Kenapa disebut monad 'gratis'?
Benjamin Hodgson
14
Nama "bebas" berasal dari teori kategori: ncatlab.org/nlab/show/free+object tetapi agak berarti monad "minimal" - bahwa hanya operasi yang valid yang merupakan operasi monad, seperti halnya " lupa "semua itu struktur lain.
Boyd Stephen Smith Jr
3
@BenjaminHodgson: Boyd sepenuhnya benar. Saya tidak akan terlalu khawatir tentang hal itu kecuali jika Anda hanya ingin tahu. Dan Piponi memberikan ceramah besar tentang apa artinya "gratis" di BayHac, yang patut dilihat. Coba ikuti dengan slide- nya karena visual dalam video sama sekali tidak berguna.
Tikhon Jelvis
3
Sebuah nitpick: "Bagian monad gratis hanya [penekanan saya] cara praktis untuk mendapatkan AST yang dapat Anda rakit menggunakan fasilitas monad standar Haskell (seperti notasi) tanpa harus menulis banyak kode khusus." Lebih dari "hanya" itu (seperti yang saya yakin Anda tahu). Monad gratis juga merupakan representasi program yang dinormalisasi yang membuat juru bahasa tidak mungkin membedakan antara program yang- donotasinya berbeda tetapi sebenarnya "memiliki arti yang sama".
sacundim
5
@sacundim: Bisakah Anda menguraikan komentar Anda? Terutama kalimat 'Free monads juga merupakan representasi program yang dinormalisasi yang membuat penerjemah tidak mungkin membedakan antara program yang not-notasinya berbeda tetapi sebenarnya "memiliki arti yang sama".
Giorgio
15

Monad gratis pada dasarnya adalah monad yang membangun struktur data dalam "bentuk" yang sama dengan perhitungan daripada melakukan sesuatu yang lebih rumit. ( Ada beberapa contoh yang dapat ditemukan secara online. ) Struktur data ini kemudian diteruskan ke sepotong kode yang mengkonsumsinya dan melakukan operasi. * Saya tidak sepenuhnya akrab dengan pola repositori, tetapi dari apa yang saya baca tampaknya menjadi arsitektur tingkat yang lebih tinggi, dan penerjemah + monad gratis dapat digunakan untuk mengimplementasikannya. Di sisi lain, penerjemah monad + gratis juga dapat digunakan untuk mengimplementasikan hal-hal yang sama sekali berbeda, seperti parser.

* Perlu dicatat bahwa pola ini tidak eksklusif untuk monad, dan pada kenyataannya dapat menghasilkan kode yang lebih efisien dengan aplikasi gratis, atau panah gratis . ( Parser adalah contoh lain dari ini. )

Dan
sumber
Maaf, saya seharusnya lebih jelas tentang Repositori. (Saya lupa bahwa tidak semua orang memiliki latar belakang sistem bisnis / OO / DDD!) Repositori pada dasarnya merangkum akses data dan merehidrasi objek domain untuk Anda. Ini sering digunakan bersamaan dengan Ketergantungan Inversi - Anda dapat 'menyambungkan' implementasi Repo yang berbeda (berguna untuk pengujian, atau jika Anda perlu mengganti basis data atau ORM). Kode domain hanya memanggil repository.Get()tanpa tahu dari mana ia mendapatkan objek domain.
Benjamin Hodgson