Okur monadının amacı nedir?


122

Okur monad çok karmaşık ve işe yaramaz görünüyor. Java veya C ++ gibi zorunlu bir dilde, yanılmıyorsam okuyucu monad için eşdeğer bir kavram yoktur.

Bana basit bir örnek verip bunu biraz netleştirebilir misin?


21
Okuyucu monadını, ara sıra (değiştirilemez) bir ortamdan bazı değerleri okumak istiyorsanız, ancak bu ortamı açıkça aktarmak istemiyorsanız kullanırsınız. Java veya C ++ 'da, genel değişkenler kullanırsınız (tam olarak aynı olmasa da).
Daniel Fischer

5
@Daniel: Bu kulağa çok fazla bir cevap
SingleNegationElimination

@TokenMacGuy Bir cevap için çok kısa ve artık daha uzun bir şey düşünmem için artık çok geç. Kimse yapmazsa, uyuduktan sonra yaparım.
Daniel Fischer

8
Java veya C ++ 'da, Reader monad, yapıcısındaki bir nesneye geçirilen ve nesnenin ömrü boyunca asla değiştirilmeyen konfigürasyon parametrelerine benzer. Clojure'da, bir işlevin davranışını açıkça bir parametre olarak iletmeye gerek kalmadan parametrize etmek için kullanılan dinamik kapsamlı bir değişkene benzeyecektir.
danidiaz

Yanıtlar:


169

Korkmayın! Okuyucu monad aslında o kadar karmaşık değildir ve kullanımı çok kolay bir yardımcı programa sahiptir.

Bir monad'a yaklaşmanın iki yolu vardır: sorabiliriz

  1. Monad ne geliyor do ? Hangi operasyonlarla donatılmıştır? Bu ne için iyi?
  2. Monad nasıl uygulanır? Nereden doğuyor?

İlk yaklaşımdan, okuyucu monad bazı soyut tiptedir

data Reader env a

öyle ki

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

Peki bunu nasıl kullanacağız? Okuyucu monad, yapılandırma bilgilerini bir hesaplamadan (örtük) geçirmek için iyidir.

Bir hesaplamada çeşitli noktalarda ihtiyaç duyduğunuz bir "sabit" e sahip olduğunuzda, ancak gerçekten aynı hesaplamayı farklı değerlerle yapabilmek istiyorsanız, o zaman bir okuyucu monad kullanmalısınız.

Okuyucu monadları, OO insanlarının bağımlılık enjeksiyonu dediği şeyi yapmak için de kullanılır . Örneğin, negamax algoritması, iki oyunculu bir oyunda bir pozisyonun değerini hesaplamak için sıklıkla (yüksek oranda optimize edilmiş formlarda) kullanılır. Algoritmanın kendisi hangi oyunu oynadığınızı önemsemiyor, ancak oyunda "sonraki" pozisyonların hangileri olduğunu belirleyebilmeniz ve mevcut pozisyonun bir zafer pozisyonu olup olmadığını söyleyebilmeniz dışında.

 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

Bu daha sonra herhangi bir sonlu, deterministik, iki oyunculu oyunla çalışacaktır.

Bu kalıp, gerçekten bağımlılık enjeksiyonu olmayan şeyler için bile kullanışlıdır. Finans alanında çalıştığınızı varsayalım, bir varlığı fiyatlandırmak için karmaşık bir mantık tasarlayabilirsiniz (bir türev ürünü), bu tamamen iyi ve iyidir ve kokuşmuş monadlar olmadan yapabilirsiniz. Ancak daha sonra, programınızı birden çok para birimiyle başa çıkacak şekilde değiştirirsiniz. Anında para birimleri arasında dönüştürme yapabilmeniz gerekir. İlk girişiminiz bir üst düzey işlevi tanımlamaktır

type CurrencyDict = Map CurrencyName Dollars
currencyDict :: CurrencyDict

spot fiyatlar almak için. Daha sonra bu sözlüğü kodunuzda çağırabilirsiniz .... ama bekleyin! Bu işe yaramaz! Para birimi sözlüğü değişmezdir ve bu nedenle yalnızca programınızın ömrü boyunca değil, derlendiği andan itibaren aynı olmalıdır ! Ee ne yapıyorsun? Okuyucu monadını kullanmak bir seçenek olabilir:

 computePrice :: Reader CurrencyDict Dollars
 computePrice
    = do currencyDict <- ask
      --insert computation here

Belki de en klasik kullanım durumu tercümanları uygulamaktır. Ancak buna bakmadan önce, başka bir işlevi tanıtmamız gerekiyor.

 local :: (env -> env) -> Reader env a -> Reader env a

Tamam, yani Haskell ve diğer işlevsel diller lambda hesabına dayanmaktadır . Lambda hesaplamasının şöyle görünen bir sözdizimi vardır:

 data Term = Apply Term Term | Lambda String Term | Var Term deriving (Show)

ve bu dil için bir değerlendirici yazmak istiyoruz. Bunu yapmak için, terimlerle ilişkili bağlamaların bir listesi olan bir ortamı takip etmemiz gerekecek (aslında bu, statik kapsam belirleme yapmak istediğimiz için kapanışlar olacaktır).

 newtype Env = Env ([(String, Closure)])
 type Closure = (Term, Env)

İşimiz bittiğinde, bir değer (veya bir hata) almalıyız:

 data Value = Lam String Closure | Failure String

Öyleyse, tercümanı yazalım:

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!

Son olarak, önemsiz bir ortamı geçerek kullanabiliriz:

interp :: Term -> Value
interp term = runReader (interp' term) (Env [])

Ve işte bu. Lambda hesabı için tamamen işlevsel bir yorumlayıcı.


Bunun hakkında düşünmenin diğer yolu sormaktır: Nasıl uygulanıyor? Cevap şudur: Okur monad aslında tüm monadlar arasında en basit ve en zarif olanıdır.

newtype Reader env a = Reader {runReader :: env -> a}

Okuyucu, işlevler için sadece süslü bir isimdir! Daha önce tanımlamıştık, runReaderpeki ya API'nin diğer bölümleri? Her Monadbiri aynı zamanda bir Functor:

instance Functor (Reader env) where
   fmap f (Reader g) = Reader $ f . g

Şimdi, bir monad almak için:

instance Monad (Reader env) where
   return x = Reader (\_ -> x)
   (Reader f) >>= g = Reader $ \x -> runReader (g (f x)) x

ki bu çok korkutucu değil. askgerçekten basit:

ask = Reader $ \x -> x

ise localo kadar kötü değil:

local f (Reader g) = Reader $ \x -> runReader g (f x)

Tamam, öyleyse okuyucu monad sadece bir işlevdir. Neden Reader'a sahipsin? İyi soru. Aslında buna ihtiyacın yok!

instance Functor ((->) env) where
  fmap = (.)

instance Monad ((->) env) where
  return = const
  f >>= g = \x -> g (f x) x

Bunlar daha da basit. Dahası, fonksiyonların sırasına göre asksadece idve localsadece fonksiyon kompozisyonu!


6
Çok ilginç cevap. Dürüst olmak gerekirse, monad'ı gözden geçirmek istediğimde defalarca tekrar okudum. Bu arada, nagamax algoritmasıyla ilgili olarak, "değerler <- mapM (negatif. Negamax (negatif rengi)) mümkün" doğru görünmüyor. Sağladığınız kodun yalnızca monad okuyucularının nasıl çalıştığını göstermek için olduğunu biliyorum. Ama zamanınız varsa, negamax algoritmasının kodunu düzeltebilir misiniz? Çünkü negamax'ı çözmek için okuyucu monad'ı kullandığınızda ilginç.
chipbk10

4
Öyleyse Reader, monad türü sınıfın belirli bir uygulaması olan bir işlev mi? Bunu daha önce söylemek kafamın biraz daha az karışmasına yardımcı olabilirdi. İlk önce anlamıyordum. Yarı yolda, "Oh, eksik değeri sağladığınızda size istenen sonucu verecek bir şeyi geri getirmenize izin veriyor" diye düşündüm. Bunun yararlı olduğunu düşündüm, ancak aniden bir işlevin tam olarak bunu yaptığını fark ettim.
ziggystar

1
Bunu okuduktan sonra çoğunu anlıyorum. localFonksiyonu olsa biraz daha açıklama ihtiyacı var ..
Christophe De Troyer

@Philip Monad örneği hakkında bir sorum var. Bind işlevini şu şekilde yazamaz mıyız (Reader f) >>= g = (g (f x))?
zeronone

@zeronone nerede x?
Ashish Negi

56

Reader monad'ın varyantlarının her yerde olduğunu kendi başıma keşfedene kadar senin gibi şaşkın olduğumu hatırlıyorum . Nasıl keşfettim? Çünkü üzerinde küçük varyasyonlar olduğu ortaya çıkan bir kod yazmaya devam ettim.

Örneğin, bir noktada tarihsel değerleri ele almak için bazı kodlar yazıyordum ; zamanla değişen değerler. Bunun çok basit bir modeli, zaman noktalarından zamanın o noktasındaki değere kadar olan işlevlerdir:

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

ApplicativeEğer varsa o örneği aracı employees :: History Day [Person]ve customers :: History Day [Person]bunu yapabilirsiniz:

-- | For any given day, the list of employees followed by the customers
employeesAndCustomers :: History Day [Person]
employeesAndCustomers = (++) <$> employees <*> customers

Yani, Functorve Applicativebize geçmişleri ile çalışmak için düzenli, tarihsel olmayan fonksiyonları uyum sağlamasına olanak.

Monad örneği, sezgisel olarak işlev dikkate alınarak anlaşılır (>=>) :: Monad m => (a -> m b) -> (b -> m c) -> a -> m c. Bir tür a -> History t bişlevi, bir değer ageçmişini eşleyen bir işlevdir b; örneğin, olabilir getSupervisor :: Person -> History Day Supervisorve getVP :: Supervisor -> History Day VP. Yani Monad örneği Historybunun gibi işlevler oluşturmakla ilgilidir; örneğin, sahip oldukları e-posta geçmişini getSupervisor >=> getVP :: Person -> History Day VPalan işlevdir .PersonVP

Bu Historymonad aslında tamamen aynı Reader. History t agerçekten aynıdır Reader t a(ile aynıdır t -> a).

Başka bir örnek: Son zamanlarda Haskell'de OLAP tasarımlarının prototipini oluşturuyorum . Buradaki fikirlerden biri, bir boyut kümesinin kesişimlerinden değerlere bir eşleme olan "hiperküp" fikridir. Yine başlıyoruz:

newtype Hypercube intersection value = Hypercube { get :: intersection -> value }

Hiperküpler üzerinde yaygın bir işlem, bir hiperküpün karşılık gelen noktalarına çok-yerli skaler fonksiyonlar uygulamaktır. Bunu, aşağıdakiler için bir Applicativeörnek tanımlayarak elde edebiliriz 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)

Az önce Historyyukarıdaki kodu kopyaladım ve isimleri değiştirdim. Anlayabileceğiniz gibi, Hypercubeaynı zamanda adil Reader.

Devam ediyor. Örneğin, Readerbu modeli uyguladığınızda dil çevirmenleri de özetle :

  • İfade = a Reader
  • Ücretsiz değişkenler = kullanımları ask
  • Değerlendirme ortamı = Readeryürütme ortamı.
  • Bağlama yapıları = local

İyi bir benzetme, a'nın içinde "delikler" bulunan ve sizin neden bahsettiğimizi bilmenizi engelleyen bir Reader r atemsilidir . Sadece delikleri doldurmak için bir an sağladığınızda gerçek bir tane alabilirsiniz . Bunun gibi tonlarca şey var. Yukarıdaki örneklerde, "geçmiş", bir zaman belirtene kadar hesaplanamayacak bir değerdir, bir hiperküp, bir kesişme belirtene kadar hesaplanamayan bir değerdir ve bir dil ifadesi, Değişkenlerin değerlerini sağlayana kadar hesaplanmayacaktır. Aynı zamanda size neden aynı şey olduğuna dair bir sezgisellik verir , çünkü böyle bir fonksiyon sezgisel olarak eksik bir an'dır .aaarReader r ar -> aar

Yani Functor, Applicativeve Monadörnekleri, Reader"an aeksik olan " türünde herhangi bir şeyi modellediğiniz durumlar için çok yararlı bir genellemedir rve bu "eksik" nesneleri tamamlanmış gibi ele almanıza izin verir.

Yine aynı şeyi söylemenin başka bir yolu: Bir Reader r atüketir o şeydir rve üretir ave Functor, Applicativeve Monadörnekleri ile çalışmak için temel kalıplardır Readers. Functor= bir Readerbaşkasının çıktısını değiştiren yapmak Reader; Applicative= iki Readers'yi aynı girişe bağlayın ve çıkışlarını birleştirin; Monad= a'nın sonucunu inceleyin ve Readerbaşka bir tane oluşturmak için kullanın Reader. localVe withReaderişlevleri = bir hale Readerdeğiştirir diğerine girdi o Reader.


5
Mükemmel cevap. Ayrıca kullanabilirsiniz GeneralizedNewtypeDerivingtüretmek uzantısı Functor, Applicative, Monadbunların altında yatan türlerine göre newtypes için vs..
Rein Henrichs

20

Java veya C ++ 'da herhangi bir değişkene her yerden sorunsuz olarak erişebilirsiniz. Kodunuz çok iş parçacıklı hale geldiğinde sorunlar görünür.

Haskell'de değeri bir işlevden diğerine aktarmanın yalnızca iki yolu vardır:

  • Değeri, çağrılabilir işlevin giriş parametrelerinden birinden geçirirsiniz. Dezavantajları şunlardır: 1) TÜM değişkenleri bu şekilde geçiremezsiniz - girdi parametrelerinin listesi aklınızı başınızdan alır. 2) işlev çağrılarının sırayla: fn1 -> fn2 -> fn3işlevi fn2aralarından geçmesi parametre gerekmeyebilir fn1için fn3.
  • Değeri bir monad kapsamında iletiyorsunuz. Dezavantajı ise: Monad kavramının ne olduğunu sağlam bir şekilde anlamalısınız. Değerleri etrafından geçirmek, Monad'leri kullanabileceğiniz çok sayıda uygulamadan sadece biridir. Aslında Monad anlayışı inanılmaz derecede güçlü. Hemen içgörü alamazsanız üzülmeyin. Sadece denemeye devam edin ve farklı eğitimleri okuyun. Alacağınız bilgi karşılığını alacaktır.

Reader monad, yalnızca işlevler arasında paylaşmak istediğiniz verileri aktarır. İşlevler bu verileri okuyabilir, ancak değiştiremez. Okuyucu monadını yapan tek şey bu. Neredeyse hepsi. Gibi işlevler de vardır local, ancak ilk kez asksyalnızca bağlı kalabilirsiniz .


3
Verileri örtük olarak iletmek için monadları kullanmanın bir başka dezavantajı, kendinizi çok sayıda 'zorunlu stil' kod donotasyonu yazarken bulmanın çok kolay olmasıdır; bu, saf bir işleve dönüştürülmekten daha iyi olacaktır.
Benjamin Hodgson

4
@BenjaminHodgson Do notasyonunda monadlarla 'zorunlu görünümlü' kod yazmak, yan etkili (saf olmayan) kod yazmak anlamına gelmez. Aslında, Haskell'deki yan etkili kod yalnızca IO monad içinde mümkün olabilir.
Dmitry Bespalov

Diğer fonksiyon birine bir wherecümle ile eklenmişse , değişkenleri geçirmenin 3. yolu olarak kabul edilecek mi?
Elmex80s
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.