Monad pembaca begitu kompleks dan sepertinya tidak berguna. Dalam bahasa imperatif seperti Java atau C ++, tidak ada padanan konsep untuk reader monad, kalau saya tidak salah.
Dapatkah Anda memberi saya contoh sederhana dan menjelaskannya sedikit?
Monad pembaca begitu kompleks dan sepertinya tidak berguna. Dalam bahasa imperatif seperti Java atau C ++, tidak ada padanan konsep untuk reader monad, kalau saya tidak salah.
Dapatkah Anda memberi saya contoh sederhana dan menjelaskannya sedikit?
Jawaban:
Jangan takut! Reader monad sebenarnya tidak terlalu rumit, dan memiliki utilitas yang sangat mudah digunakan.
Ada dua cara untuk mendekati monad: kita bisa bertanya
Dari pendekatan pertama, monad pembaca adalah beberapa tipe abstrak
data Reader env a
seperti yang
-- Reader is a monad
instance Monad (Reader env)
-- and we have a function to get its environment
ask :: Reader env env
-- finally, we can run a Reader
runReader :: Reader env a -> env -> a
Jadi bagaimana kita menggunakan ini? Nah, reader monad bagus untuk meneruskan informasi konfigurasi (implisit) melalui komputasi.
Setiap kali Anda memiliki "konstanta" dalam komputasi yang Anda perlukan pada berbagai titik, tetapi sebenarnya Anda ingin dapat melakukan komputasi yang sama dengan nilai yang berbeda, Anda harus menggunakan pembaca monad.
Reader monads juga digunakan untuk melakukan apa yang oleh orang-orang OO disebut injeksi ketergantungan . Misalnya, algoritme negamax sering digunakan (dalam bentuk yang sangat dioptimalkan) untuk menghitung nilai posisi dalam permainan dua pemain. Algoritme itu sendiri tidak peduli game apa yang Anda mainkan, kecuali Anda harus dapat menentukan apa posisi "selanjutnya" dalam game, dan Anda harus dapat mengetahui apakah posisi saat ini adalah posisi kemenangan.
import Control.Monad.Reader
data GameState = NotOver | FirstPlayerWin | SecondPlayerWin | Tie
data Game position
= Game {
getNext :: position -> [position],
getState :: position -> GameState
}
getNext' :: position -> Reader (Game position) [position]
getNext' position
= do game <- ask
return $ getNext game position
getState' :: position -> Reader (Game position) GameState
getState' position
= do game <- ask
return $ getState game position
negamax :: Double -> position -> Reader (Game position) Double
negamax color position
= do state <- getState' position
case state of
FirstPlayerWin -> return color
SecondPlayerWin -> return $ negate color
Tie -> return 0
NotOver -> do possible <- getNext' position
values <- mapM ((liftM negate) . negamax (negate color)) possible
return $ maximum values
Ini kemudian akan bekerja dengan permainan dua pemain yang terbatas dan deterministik.
Pola ini berguna bahkan untuk hal-hal yang sebenarnya bukan injeksi ketergantungan. Misalkan Anda bekerja di bidang keuangan, Anda mungkin merancang beberapa logika rumit untuk menentukan harga suatu aset (kata turunan), yang semuanya baik dan bagus dan Anda dapat melakukannya tanpa monad yang bau. Tetapi kemudian, Anda memodifikasi program Anda untuk menangani banyak mata uang. Anda harus bisa menukar mata uang dengan cepat. Upaya pertama Anda adalah menentukan fungsi tingkat atas
type CurrencyDict = Map CurrencyName Dollars
currencyDict :: CurrencyDict
untuk mendapatkan harga spot. Anda kemudian dapat memanggil kamus ini dalam kode Anda .... tapi tunggu! Itu tidak akan berhasil! Kamus mata uang tidak dapat diubah dan karenanya harus sama tidak hanya untuk masa pakai program Anda, tetapi sejak saat itu dikompilasi ! Jadi apa yang kamu lakukan? Nah, salah satu opsinya adalah menggunakan Reader monad:
computePrice :: Reader CurrencyDict Dollars
computePrice
= do currencyDict <- ask
--insert computation here
Mungkin kasus penggunaan paling klasik adalah dalam mengimplementasikan penerjemah. Tapi, sebelum kita melihatnya, kita perlu memperkenalkan fungsi lain
local :: (env -> env) -> Reader env a -> Reader env a
Oke, jadi Haskell dan bahasa fungsional lainnya didasarkan pada kalkulus lambda . Kalkulus Lambda memiliki sintaks yang mirip
data Term = Apply Term Term | Lambda String Term | Var Term deriving (Show)
dan kami ingin menulis evaluator untuk bahasa ini. Untuk melakukannya, kita perlu melacak lingkungan, yang merupakan daftar binding yang terkait dengan istilah (sebenarnya ini akan menjadi closure karena kita ingin melakukan pelingkupan statis).
newtype Env = Env ([(String, Closure)])
type Closure = (Term, Env)
Ketika kita selesai, kita harus mendapatkan nilai (atau kesalahan):
data Value = Lam String Closure | Failure String
Jadi, mari kita tulis juru bahasa:
interp' :: Term -> Reader Env Value
--when we have a lambda term, we can just return it
interp' (Lambda nv t)
= do env <- ask
return $ Lam nv (t, env)
--when we run into a value, we look it up in the environment
interp' (Var v)
= do (Env env) <- ask
case lookup (show v) env of
-- if it is not in the environment we have a problem
Nothing -> return . Failure $ "unbound variable: " ++ (show v)
-- if it is in the environment, then we should interpret it
Just (term, env) -> local (const env) $ interp' term
--the complicated case is an application
interp' (Apply t1 t2)
= do v1 <- interp' t1
case v1 of
Failure s -> return (Failure s)
Lam nv clos -> local (\(Env ls) -> Env ((nv, clos) : ls)) $ interp' t2
--I guess not that complicated!
Akhirnya, kita bisa menggunakannya dengan melewatkan lingkungan sepele:
interp :: Term -> Value
interp term = runReader (interp' term) (Env [])
Dan itu dia. Penerjemah yang berfungsi penuh untuk kalkulus lambda.
Cara lain untuk memikirkan hal ini adalah dengan bertanya: Bagaimana penerapannya? Jawabannya adalah reader monad sebenarnya adalah salah satu monad yang paling sederhana dan paling elegan.
newtype Reader env a = Reader {runReader :: env -> a}
Pembaca hanyalah nama keren untuk fungsi! Kami telah mendefinisikan runReaderjadi bagaimana dengan bagian lain dari API? Nah, setiap Monadjuga merupakan Functor:
instance Functor (Reader env) where
fmap f (Reader g) = Reader $ f . g
Sekarang, untuk mendapatkan monad:
instance Monad (Reader env) where
return x = Reader (\_ -> x)
(Reader f) >>= g = Reader $ \x -> runReader (g (f x)) x
yang tidak terlalu menakutkan. asksangat sederhana:
ask = Reader $ \x -> x
sementara localtidak terlalu buruk:
local f (Reader g) = Reader $ \x -> runReader g (f x)
Oke, jadi pembaca monad hanyalah sebuah fungsi. Mengapa memiliki Pustaka? Pertanyaan bagus. Sebenarnya, Anda tidak membutuhkannya!
instance Functor ((->) env) where
fmap = (.)
instance Monad ((->) env) where
return = const
f >>= g = \x -> g (f x) x
Ini bahkan lebih sederhana. Terlebih lagi, askhanya iddan localhanyalah komposisi fungsi dengan urutan fungsi yang dialihkan!
Readerapakah fungsi dengan implementasi tertentu dari kelas tipe monad? Mengatakannya lebih awal akan membantu saya untuk tidak terlalu bingung. Pertama saya tidak mengerti. Di tengah jalan, saya berpikir "Oh, ini memungkinkan Anda mengembalikan sesuatu yang akan memberi Anda hasil yang diinginkan begitu Anda memberikan nilai yang hilang." Saya pikir itu berguna, tetapi tiba-tiba menyadari bahwa suatu fungsi melakukan hal ini.
localfungsi tersebut membutuhkan penjelasan lebih meskipun ..
(Reader f) >>= g = (g (f x))?
x?
Saya ingat pernah bingung seperti Anda, sampai saya menemukan sendiri bahwa varian Monad Reader ada di mana - mana . Bagaimana saya menemukannya? Karena saya terus menulis kode yang ternyata variasi kecil di atasnya.
Misalnya, pada satu titik saya sedang menulis beberapa kode untuk menangani nilai - nilai sejarah ; nilai-nilai yang berubah seiring waktu. Model yang sangat sederhana ini adalah fungsi dari titik waktu ke nilai pada titik waktu tersebut:
import Control.Applicative
-- | A History with timeline type t and value type a.
newtype History t a = History { observe :: t -> a }
instance Functor (History t) where
-- Apply a function to the contents of a historical value
fmap f hist = History (f . observe hist)
instance Applicative (History t) where
-- A "pure" History is one that has the same value at all points in time
pure = History . const
-- This applies a function that changes over time to a value that also
-- changes, by observing both at the same point in time.
ff <*> fx = History $ \t -> (observe ff t) (observe fx t)
instance Monad (History t) where
return = pure
ma >>= f = History $ \t -> observe (f (observe ma t)) t
The Applicativecontoh berarti bahwa jika Anda memiliki employees :: History Day [Person]dan customers :: History Day [Person]Anda dapat melakukan ini:
-- | For any given day, the list of employees followed by the customers
employeesAndCustomers :: History Day [Person]
employeesAndCustomers = (++) <$> employees <*> customers
Yaitu, FunctordanApplicative memungkinkan kita untuk menyesuaikan fungsi reguler, non-historis untuk bekerja dengan sejarah.
Contoh monad paling intuitif dipahami dengan mempertimbangkan fungsinya (>=>) :: Monad m => (a -> m b) -> (b -> m c) -> a -> m c. Fungsi tipe a -> History t badalah fungsi yang memetakan filea sejarah bnilai; misalnya, Anda dapat memiliki getSupervisor :: Person -> History Day Supervisor, dan getVP :: Supervisor -> History Day VP. Jadi contoh Monad Historyadalah tentang menyusun fungsi seperti ini; misalnya, getSupervisor >=> getVP :: Person -> History Day VPadalah fungsi yang mendapatkan, untuk apa pun Person, riwayatVP yang mereka miliki.
Nah, ini History sebenarnya monad ini sama persis dengan Reader. History t abenar-benar sama dengan Reader t a(yang sama dengan t -> a).
Contoh lain: Saya telah membuat prototipe desain OLAP di Haskell baru-baru ini. Salah satu ide di sini adalah dari "hypercube", yang merupakan pemetaan dari persimpangan sekumpulan dimensi ke nilai. Baiklah, kita lanjut lagi:
newtype Hypercube intersection value = Hypercube { get :: intersection -> value }
Salah satu operasi yang umum pada hypercube adalah menerapkan fungsi skalar multi-tempat ke titik-titik hypercube yang sesuai. Ini bisa kita dapatkan dengan mendefinisikanApplicative contoh untuk Hypercube:
instance Functor (Hypercube intersection) where
fmap f cube = Hypercube (f . get cube)
instance Applicative (Hypercube intersection) where
-- A "pure" Hypercube is one that has the same value at all intersections
pure = Hypercube . const
-- Apply each function in the @ff@ hypercube to its corresponding point
-- in @fx@.
ff <*> fx = Hypercube $ \x -> (get ff x) (get fx x)
Saya baru saja menyalin Historykode di atas dan mengubah nama. Seperti yang Anda tahu, Hypercubeitu juga adil Reader.
Ini terus berlanjut. Misalnya, penerjemah bahasa juga memiliki intisari Reader, ketika Anda menerapkan model ini:
ReaderaskReader lingkungan pelaksanaan.localSebuah analogi yang baik adalah bahwa a Reader r amewakili dan adengan "lubang" di dalamnya, yang mencegah Anda untuk mengetahui yang asedang kita bicarakan. Anda hanya bisa mendapatkan aktual asetelah Anda menyediakan dan runtuk mengisi lubang. Ada banyak sekali hal seperti itu. Dalam contoh di atas, "histori" adalah nilai yang tidak dapat dihitung hingga Anda menentukan waktu, hypercube adalah nilai yang tidak dapat dihitung hingga Anda menentukan perpotongan, dan ekspresi bahasa adalah nilai yang dapat tidak akan dihitung sampai Anda memberikan nilai variabel. Ini juga memberi Anda intuisi tentang mengapa Reader r asama dengan r -> a, karena fungsi seperti itu juga secara intuitif merupakan suatu yang ahilang r.
Jadi Functor, Applicativedan Monadcontoh dari Readeradalah generalisasi yang sangat berguna untuk kasus di mana Anda memodelkan apa pun dari semacam "an ayang kehilangan r," dan memungkinkan Anda untuk memperlakukan objek "tidak lengkap" ini seolah-olah mereka lengkap.
Cara lain untuk mengatakan hal yang sama: a Reader r aadalah sesuatu yang mengkonsumsi rdan menghasilkan a, dan Functor, Applicativedan Monadcontoh adalah pola dasar untuk bekerja dengan Readers. Functor= membuat a Readeryang mengubah keluaran dari yang lain Reader; Applicative= hubungkan dua Readers ke input yang sama dan gabungkan outputnya; Monad= memeriksa hasil dari a Readerdan menggunakannya untuk membuat yang lain Reader. The localand withReaderfunctions = make a Readeryang mengubah input ke input lainnya Reader.
GeneralizedNewtypeDerivingekstensi untuk menurunkan Functor, Applicative, Monad, dll untuk newtypes berdasarkan jenis yang mendasari mereka.
Di Java atau C ++ Anda dapat mengakses variabel apa pun dari mana saja tanpa masalah. Masalah muncul ketika kode Anda menjadi multi-threaded.
Di Haskell Anda hanya memiliki dua cara untuk meneruskan nilai dari satu fungsi ke fungsi lainnya:
fn1 -> fn2 -> fn3fungsi fn2mungkin tidak memerlukan parameter yang Anda teruskan fn1ke fn3.Monad Pembaca hanya meneruskan data yang ingin Anda bagi antar fungsi. Fungsi dapat membaca data itu, tetapi tidak dapat mengubahnya. Itu saja yang dilakukan Reader monad. Hampir semuanya. Ada juga sejumlah fungsi seperti local, tetapi untuk pertama kalinya Anda hanya dapat menggunakan asks.
do-notasi, yang akan lebih baik jika difaktor ulang menjadi fungsi murni.
whereklausa, apakah itu akan diterima sebagai cara ke-3 untuk melewatkan variabel?