Какова цель читательской монады?

Читатель Монада настолько сложный и кажется бесполезным. На императивном языке, таком как Java или С++, нет эквивалентного термина для монады читателя (если я прав).

Можете ли вы дать мне простой пример и немного разъяснить мне?

Ответ 1

Не бойтесь! Мода читателя на самом деле не так сложна и имеет настоящую простую в использовании утилиту.

Есть два способа приблизиться к монаде: мы можем спросить

  • Что делает монада ? С какими операциями он оснащен? Для чего это полезно?
  • Как осуществляется монада? Откуда он возникает?

Из первого подхода монада-читатель представляет собой абстрактный тип

data Reader env a

такое, что

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

Итак, как мы это используем? Ну, читательская монада хороша для передачи (неявной) информации о конфигурации через вычисление.

Каждый раз, когда у вас есть "постоянная" в вычислении, которая вам нужна в разных точках, но на самом деле вы хотели бы иметь возможность выполнять одни и те же вычисления с разными значениями, тогда вы должны использовать монаду-читателю.

Монады-читатели также используются, чтобы делать то, что люди OO называют инъекцией зависимостей. Например, алгоритм negamax часто используется (в сильно оптимизированных формах) для вычисления значения позиции в двухпользовательской игре. Сам алгоритм не заботится о том, какую игру вы играете, за исключением того, что вам нужно определить, какие "следующие" позиции находятся в игре, и вы должны быть в состоянии определить, является ли текущая позиция победой.

 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

Затем это будет работать с любой конечной, детерминированной, двухпользовательской игрой.

Этот шаблон полезен даже для вещей, которые на самом деле не являются инъекциями зависимостей. Предположим, что вы работаете в сфере финансов, вы можете разработать сложную логику для определения цен на активы (например, производные), которые все хорошо и хорошо, и вы можете обойтись без каких-либо вонючих монадов. Но тогда вы изменяете свою программу, чтобы иметь дело с несколькими валютами. Вы должны иметь возможность конвертировать между валютами на лету. Ваша первая попытка - определить функцию верхнего уровня

type CurrencyDict = Map CurrencyName Dollars
currencyDict :: CurrencyDict

чтобы получить спотовые цены. Затем вы можете вызвать этот словарь в своем коде.... но подождите! Это не сработает! Валютный словарь неизменен и поэтому должен быть таким же не только для жизни вашей программы, но и с момента его получения скомпилированного! Ну так что ты делаешь? Ну, один вариант - использовать монаду читателя:

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

Возможно, наиболее распространенным вариантом использования является использование интерпретаторов. Но, прежде чем мы посмотрим на это, нам нужно ввести другую функцию

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

Хорошо, поэтому Haskell и другие функциональные языки основаны на лямбда-исчислении. Исчисление лямбда имеет синтаксис, который выглядит как

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

и мы хотим написать оценщика для этого языка. Для этого нам нужно будет отслеживать среду, которая представляет собой список привязок, связанных с терминами (на самом деле это будет закрытие, потому что мы хотим сделать статическую область).

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

Когда мы закончим, мы должны получить значение (или ошибку):

 data Value = Lam String Closure | Failure String

Итак, давайте напишем интерпретатор:

interp' :: Term -> Reader Env Value
--when we have 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, than 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!

Наконец, мы можем использовать его, передав тривиальную среду:

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

И это все. Полностью функциональный интерпретатор для лямбда-исчисления.


Итак, другой способ подумать об этом - спросить: как это реализовано? Ну, ответ заключается в том, что монада-читатель на самом деле одна из самых простых и элегантных из всех монадов.

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

Читатель - просто причудливое имя для функций! Мы уже определили runReader, а как насчет других частей API? Ну, каждый Monad также является Functor:

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

Теперь, чтобы получить монаду:

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

что не так страшно. ask действительно прост:

ask = Reader $ \x -> x

в то время как local не так уж плохо.

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

Хорошо, так что монада-читатель - это просто функция. Почему у читателя вообще? Хороший вопрос. На самом деле, вам это не нужно!

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

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

Это еще проще. Более того, ask является просто id и local является просто функцией композиции в другом порядке!

Ответ 2

Я помню, как я был озадачен, пока не обнаружил, что варианты монады читателей повсюду. Как я узнал об этом? Потому что я продолжал писать код, который оказался небольшим изменением.

Например, однажды я написал код для обработки исторических значений; значения, которые меняются со временем. Очень простая модель этого - это функции от точек времени до значения в этот момент времени:

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

Экземпляр Applicative означает, что если у вас есть employees :: History Day [Person] и customers :: History Day [Person], вы можете сделать это:

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

I.e., Functor и Applicative позволяют нам адаптировать регулярные, неисторические функции для работы с историями.

Экземпляр монады наиболее интуитивно понятен, рассматривая функцию (>=>) :: Monad m => (a -> m b) -> (b -> m c) -> a -> m c. Функция типа a -> History t b - это функция, которая отображает a в историю значений b; например, вы могли бы иметь getSupervisor :: Person -> History Day Supervisor и getVP :: Supervisor -> History Day VP. Таким образом, экземпляр Monad для History посвящен составлению таких функций; например, getSupervisor >=> getVP :: Person -> History Day VP - это функция, которая получает для любой Person историю VP, которую они имели.

Ну, эта монада History на самом деле точно такая же, как Reader. History t a действительно совпадает с Reader t a (что совпадает с t -> a).

Еще один пример: я недавно разрабатывал прототипы OLAP в Haskell. Здесь есть одна идея "гиперкуба", которая является отображением от пересечений множества измерений к значениям. Здесь мы снова и снова:

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

Одной из распространенных операций на гиперкубах является применение многолокальных скалярных функций к соответствующим точкам гиперкуба. Это можно получить, определив экземпляр Applicative для 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 @[email protected] hypercube to its corresponding point 
    -- in @[email protected]
    ff <*> fx = Hypercube $ \x -> (get ff x) (get fx x)

Я только что скопировал код History выше и изменил имена. Как вы можете сказать, Hypercube также просто Reader.

Это продолжается и продолжается. Например, языковые интерпретаторы также сводятся к Reader, когда вы применяете эту модель:

  • Выражение = a Reader
  • Свободные переменные = использование ask
  • среда оценки = Reader среда выполнения.
  • Связывающие конструкции = local

Хорошая аналогия заключается в том, что a Reader r a представляет a с "дырками" в нем, что мешает вам узнать, о каком a мы говорим. Вы можете получить фактический a после того, как вы предоставите a r, чтобы заполнить отверстия. Есть такие вещи. В приведенных выше примерах "история" - это значение, которое невозможно вычислить до тех пор, пока вы не укажете время, гиперкуб - это значение, которое невозможно вычислить до тех пор, пока вы не укажете пересечение, а выражение языка - это значение, которое может 't вычисляется до тех пор, пока вы не укажете значения переменных. Он также дает вам интуицию о том, почему Reader r a совпадает с r -> a, потому что такая функция также интуитивно a отсутствует r.

Таким образом, примеры Functor, Applicative и Monad Reader являются очень полезным обобщением для случаев, когда вы моделируете что-либо типа a a, в котором отсутствует r ", и позволяют обрабатывать эти" неполные" объекты, как если бы они были полными.

Еще один способ сказать одно и то же: a Reader r a - это то, что потребляет r и создает a, а экземпляры Functor, Applicative и Monad - это базовые шаблоны для работы с Reader s. Functor= make a Reader, который модифицирует вывод другого Reader; Applicative= подключить два Reader к одному и тому же входу и объединить их выходы; Monad= проверить результат Reader и использовать его для построения другого Reader. Функции local и withReader= make a Reader, который изменяет ввод на другой Reader.

Ответ 3

В Java или С++ вы можете получить доступ к любой переменной из любого места без каких-либо проблем. Проблемы возникают, когда ваш код становится многопоточным.

В Haskell у вас есть только два способа передать значение из одной функции в другую:

  • Вы передаете значение через один из входных параметров вызываемой функции. Недостатками являются: 1) вы не можете передать ВСЕ переменные таким образом - список входных параметров просто сбрасывает ваш разум. 2) в последовательности вызовов функций: fn1 -> fn2 -> fn3, функция fn2 может не нуждаться в параметре, который вы передаете от fn1 до fn3.
  • Вы передаете значение в пределах некоторой монады. Недостатком является то, что вам нужно твердо понять, что такое концепция Монады. Передача значений вокруг - это просто одно из многих приложений, в которых вы можете использовать Monads. На самом деле концепция Монады невероятна. Не расстраивайся, если сразу не поймешь. Просто продолжайте пытаться и читайте разные учебники. Знания, которые вы получите, окупится.

Монада Reader просто передает данные, которые вы хотите разделить между функциями. Функции могут читать эти данные, но не могут их изменить. Это все, что делает монада читателей. Ну, почти все. Существует также ряд функций, таких как local, но вы можете в первый раз использовать только asks.