Apa tujuan pembaca monad?

122

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?

chipbk10
sumber
21
Anda menggunakan reader monad jika Anda ingin - pada kesempatan - membaca beberapa nilai dari lingkungan (tidak dapat dimodifikasi), tetapi tidak ingin menyebarkan lingkungan itu secara eksplisit. Di Java atau C ++, Anda akan menggunakan variabel global (meskipun tidak persis sama).
Daniel Fischer
5
@Daniel: Kedengarannya sangat mengerikan seperti sebuah jawaban
SingleNegationElimination
@TokenMacGuy Terlalu pendek untuk sebuah jawaban, dan sekarang sudah terlambat bagi saya untuk memikirkan sesuatu lebih lama. Jika tidak ada orang lain yang melakukannya, saya akan melakukannya setelah saya tidur.
Daniel Fischer
8
Di Java atau C ++, monad Reader akan dianalogikan dengan parameter konfigurasi yang diteruskan ke objek dalam konstruktornya yang tidak pernah berubah selama masa pakai objek. Dalam Clojure, ini akan menjadi seperti variabel yang dicakup secara dinamis yang digunakan untuk parameter perilaku fungsi tanpa perlu meneruskannya secara eksplisit sebagai parameter.
danidiaz

Jawaban:

169

Jangan takut! Reader monad sebenarnya tidak terlalu rumit, dan memiliki utilitas yang sangat mudah digunakan.

Ada dua cara untuk mendekati monad: kita bisa bertanya

  1. Apa yang monad lakukan ? Operasi apa yang dilengkapi dengannya? Untuk apa ini bagus?
  2. Bagaimana monad diterapkan? Dari manakah itu muncul?

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!

Philip JF
sumber
6
Jawaban yang sangat menarik. Jujur, saya membacanya berkali-kali, ketika saya ingin mereview monad. Ngomong-ngomong, tentang algoritma nagamax, "nilai <- mapM (negate. Negamax (negate color)) mungkin" sepertinya tidak benar. Saya tahu, kode yang Anda berikan hanya untuk menunjukkan cara kerja reader monad. Tetapi jika Anda punya waktu, dapatkah Anda memperbaiki kode algoritma negamax? Sebab, menariknya, ketika Anda menggunakan reader monad untuk menyelesaikan negamax.
chipbk10
4
Jadi, 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.
ziggystar
1
Setelah membaca ini, saya mengerti sebagian besar. The localfungsi tersebut membutuhkan penjelasan lebih meskipun ..
Christophe De Troyer
@Philip Saya memiliki pertanyaan tentang instance Monad. Tidak bisakah kita menulis fungsi bind sebagai (Reader f) >>= g = (g (f x))?
zeronone
@zeronone dimana x?
Ashish Negi
56

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:

  • Ekspresi = a Reader
  • Variabel bebas = penggunaan ask
  • Lingkungan evaluasi = Reader lingkungan pelaksanaan.
  • Konstruksi mengikat = local

Sebuah 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.

Luis Casillas
sumber
5
Jawaban yang bagus. Anda juga dapat menggunakan GeneralizedNewtypeDerivingekstensi untuk menurunkan Functor, Applicative, Monad, dll untuk newtypes berdasarkan jenis yang mendasari mereka.
Rein Henrichs
20

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:

  • Anda meneruskan nilai melalui salah satu parameter input dari fungsi yang dapat dipanggil. Kekurangannya adalah: 1) Anda tidak dapat melewatkan SEMUA variabel dengan cara itu - daftar parameter input hanya membuat Anda takjub. 2) dalam urutan pemanggilan fungsi:, fn1 -> fn2 -> fn3fungsi fn2mungkin tidak memerlukan parameter yang Anda teruskan fn1ke fn3.
  • Anda meneruskan nilai dalam lingkup beberapa monad. Kekurangannya adalah: Anda harus benar-benar memahami apa itu konsepsi Monad. Meneruskan nilai-nilai hanyalah salah satu dari banyak aplikasi di mana Anda dapat menggunakan Monad. Sebenarnya konsepsi Monad sangat dahsyat. Jangan kesal, jika Anda tidak mendapatkan wawasan sekaligus. Teruslah mencoba, dan baca tutorial yang berbeda. Pengetahuan yang akan Anda dapatkan akan terbayar.

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.

Dmitry Bespalov
sumber
3
Kelemahan lebih lanjut menggunakan monad untuk secara implisit meneruskan data adalah sangat mudah menemukan diri Anda menulis banyak kode 'gaya-imperatif' dalam do-notasi, yang akan lebih baik jika difaktor ulang menjadi fungsi murni.
Benjamin Hodgson
4
@BenjaminHodgson Menulis kode 'imperatif-tampak' dengan monad di do -notation tidak perlu berarti menulis kode yang efektif sisi (tidak murni). Sebenarnya, kode efektif samping di Haskell hanya dapat dilakukan di dalam IO monad.
Dmitry Bespalov
Jika fungsi lain dilampirkan ke satu per whereklausa, apakah itu akan diterima sebagai cara ke-3 untuk melewatkan variabel?
Elmex80s