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 runReader
jadi bagaimana dengan bagian lain dari API? Nah, setiap Monad
juga 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. ask
sangat sederhana:
ask = Reader $ \x -> x
sementara local
tidak 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, ask
hanya id
dan local
hanyalah komposisi fungsi dengan urutan fungsi yang dialihkan!
Reader
apakah 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.
local
fungsi 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 Applicative
contoh 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, Functor
danApplicative
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 b
adalah fungsi yang memetakan filea
sejarah b
nilai; misalnya, Anda dapat memiliki getSupervisor :: Person -> History Day Supervisor
, dan getVP :: Supervisor -> History Day VP
. Jadi contoh Monad History
adalah tentang menyusun fungsi seperti ini; misalnya, getSupervisor >=> getVP :: Person -> History Day VP
adalah fungsi yang mendapatkan, untuk apa pun Person
, riwayatVP
yang mereka miliki.
Nah, ini History
sebenarnya monad ini sama persis dengan Reader
. History t a
benar-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 History
kode di atas dan mengubah nama. Seperti yang Anda tahu, Hypercube
itu juga adil Reader
.
Ini terus berlanjut. Misalnya, penerjemah bahasa juga memiliki intisari Reader
, ketika Anda menerapkan model ini:
Reader
ask
Reader
lingkungan pelaksanaan.local
Sebuah analogi yang baik adalah bahwa a Reader r a
mewakili dan a
dengan "lubang" di dalamnya, yang mencegah Anda untuk mengetahui yang a
sedang kita bicarakan. Anda hanya bisa mendapatkan aktual a
setelah Anda menyediakan dan r
untuk 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 a
sama dengan r -> a
, karena fungsi seperti itu juga secara intuitif merupakan suatu yang a
hilang r
.
Jadi Functor
, Applicative
dan Monad
contoh dari Reader
adalah generalisasi yang sangat berguna untuk kasus di mana Anda memodelkan apa pun dari semacam "an a
yang 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 a
adalah sesuatu yang mengkonsumsi r
dan menghasilkan a
, dan Functor
, Applicative
dan Monad
contoh adalah pola dasar untuk bekerja dengan Reader
s. Functor
= membuat a Reader
yang mengubah keluaran dari yang lain Reader
; Applicative
= hubungkan dua Reader
s ke input yang sama dan gabungkan outputnya; Monad
= memeriksa hasil dari a Reader
dan menggunakannya untuk membuat yang lain Reader
. The local
and withReader
functions = make a Reader
yang mengubah input ke input lainnya Reader
.
GeneralizedNewtypeDeriving
ekstensi 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 -> fn3
fungsi fn2
mungkin tidak memerlukan parameter yang Anda teruskan fn1
ke 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.
where
klausa, apakah itu akan diterima sebagai cara ke-3 untuk melewatkan variabel?