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

122

Читательская монада настолько сложна и кажется бесполезной. В императивном языке, таком как Java или C ++, нет эквивалентной концепции для читающей монады, если я не ошибаюсь.

Вы можете привести мне простой пример и немного прояснить это?

chipbk10
источник
21
Вы используете монаду чтения, если хотите - иногда - читать некоторые значения из (неизменяемой) среды, но не хотите явно передавать эту среду. В Java или C ++ вы бы использовали глобальные переменные (хотя это не совсем то же самое).
Дэниел Фишер
5
@Daniel: Это ужасно похоже на ответ
SingleNegationElimination
@TokenMacGuy Слишком коротко для ответа, и мне уже поздно придумывать что-то более продолжительное. Если никто не сделает этого, я сделаю это после того, как высплюсь.
Daniel Fischer
8
В Java или C ++ монада Reader была бы аналогична параметрам конфигурации, передаваемым объекту в его конструкторе, которые никогда не меняются в течение времени существования объекта. В Clojure это немного похоже на переменную с динамической областью видимости, используемую для параметризации поведения функции без необходимости явно передавать ее в качестве параметра.
danidiaz 06

Ответы:

169

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

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

  1. Что монада делать ? Какими операциями он оборудован? Для чего это?
  2. Как реализована монада? Откуда это возникает?

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

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

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

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

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

 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

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

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

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

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

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


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

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

Reader - это просто причудливое название для функций! Мы уже определились, 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)

Итак, монада чтения - это просто функция. Зачем вообще нужен Reader? Хороший вопрос. Собственно, вам это и не нужно!

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

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

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

Филип Дж. Ф.
источник
6
Очень интересный ответ. Честно говоря, много раз перечитывал, когда хочу пересмотреть монаду. Кстати, об алгоритме нагамакса, «значения <- mapM (negate. Negamax (отрицать цвет)) возможно» кажется неправильным. Я знаю, что код, который вы предоставляете, предназначен только для демонстрации того, как работает монада чтения. Но если будет время, не могли бы вы поправить код алгоритма негамакс? Потому что это интересно, когда вы используете монаду читателя для решения негамакса.
chipbk10
4
Так Readerесть функция с какой-то конкретной реализацией класса типа монады? Сказать это раньше помогло бы мне немного меньше озадачиться. Сначала я этого не понимал. На полпути я подумал: «О, это позволяет вам вернуть что-то, что даст вам желаемый результат, если вы укажете недостающее значение». Я подумал, что это полезно, но внезапно понял, что функция делает именно это.
ziggystar
1
После прочтения я понял большую часть этого. Однако localфункция требует дополнительных пояснений ..
Кристоф Де Тройер
@Philip У меня вопрос по экземпляру Monad. Разве мы не можем написать функцию связывания как (Reader f) >>= g = (g (f x))?
zeronone
@zeronone где x?
Ашиш Неги,
56

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

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

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

Т.е. 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историю VPs, которая у них была.

Ну, это 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 @ff@ hypercube to its corresponding point 
    -- in @fx@.
    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только после того, как rзаполните дыры. Таких вещей масса. В приведенных выше примерах «история» - это значение, которое нельзя вычислить, пока вы не укажете время, гиперкуб - это значение, которое невозможно вычислить, пока вы не укажете пересечение, а выражение языка - это значение, которое может не будут вычисляться, пока вы не укажете значения переменных. Это также дает вам интуитивное представление о том, почему Reader r aэто то же самое r -> a, потому что такая функция также интуитивно является aотсутствующим r.

Таким образом Functor, экземпляры , Applicativeи являются очень полезным обобщением для случаев, когда вы моделируете что-либо типа « что-то отсутствует », и позволяют вам рассматривать эти «неполные» объекты, как если бы они были завершенными.MonadReaderar

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

Луис Касильяс
источник
5
Отличный ответ. Вы можете также использовать GeneralizedNewtypeDerivingрасширение для вывода Functor, Applicative, Monadи т.д. для ньютайпов на основе базовых их типов.
Rein Henrichs
20

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

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

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

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

Дмитрий Беспалов
источник
3
Еще одним недостатком использования монад для неявной передачи данных является то, что очень легко обнаружить, что вы пишете много кода «императивного стиля» в doнотации, который лучше было бы преобразовать в чистую функцию.
Бенджамин Ходжсон
4
@BenjaminHodgson Написание "императивного" кода с монадами в do -notation не обязательно означает написание побочного (нечистого) кода. Фактически, побочный код в Haskell может быть возможен только внутри монады ввода-вывода.
Дмитрий Беспалов
Если другая функция присоединена к одной с помощью whereпредложения, будет ли это принято как третий способ передачи переменных?
Elmex80s