Какова цель Монады Читателя?

Reader Monad настолько сложен и кажется бесполезным. В императивном языке, таком как Java или C++, не существует эквивалентного термина для монады читателя (если я прав).

Можете ли вы привести простой пример и немного прояснить ситуацию?

4 ответа

Решение

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

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

  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

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

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

Монады считывателя также используются для того, что ОО называют инжекцией зависимости. Например, алгоритм 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

чтобы получить спотовые цены. Затем вы можете вызвать этот словарь в своем коде.... но подождите! Это не сработает! Словарь валют является неизменным и поэтому должен быть одинаковым не только для жизни вашей программы, но и с момента ее компиляции! Ну так что ты делаешь? Ну, один вариант будет использовать монаду 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 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}

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 это просто функция композиции в другом порядке!

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

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

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, Так что экземпляр Монады для 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 @ff@ hypercube to its corresponding point 
    -- in @fx@.
    ff <*> fx = Hypercube $ \x -> (get ff x) (get fx x)

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

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

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

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

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

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

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

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

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

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

Я многому научился из приведенных выше ответов и благодарю их за ответы. Хотя я новичок в Haskell, я хочу сказать кое-что о монаде.

Во-первых, отношение к читателю как кComputationэто важно. Это не состояние, а вычисление. Например, функцияcalc_isCountCorrectупомянутый в первом официальном примере монады Reader возвращаетReader Bindings Bool, что означает, что он получает, когдаrunReaderи верните Bool. Это вычисление.

      calc_isCountCorrect :: Reader Bindings Bool
calc_isCountCorrect = do
    count <- asks (lookupVar "count")
    bindings <- ask
    return (count == (Map.size bindings))

Вы также можете пройтиBindingsчерез аргумент, нет существенных различий, когда ваш код довольно прост.

      calcIsCountCorrectWithoutReader :: Bindings -> Bool
calcIsCountCorrectWithoutReader bindings = do
  let num = lookupVar "count" bindings
  let count = Map.size bindings
  num == count 

Тем не менее, у него есть разница, этоwhere the value comes from. Это дает вам возможность получить его с помощью неявного источника вместо аргумента.

Когда дело доходит до вопроса аналогии, я думаю, чтоlambdaвC++это хорошее объяснение.

В императивных языках, таких как Java или C++, не существует эквивалентной концепции монады чтения.

Программа чтения дает вам возможность получить значение извне (НЕ глобальная переменная, а верхняя область видимости). Это очень похоже на оговорку о захвате вC++ lambda.

Например, у вас есть код Haskell, который утраиваетnumberи добавляет его с неявным значением. Следующий код выводит10.

      import           Control.Monad.Reader
trebleNumberAndAddImplicitly :: Int -> Reader Int Int
trebleNumberAndAddImplicitly number = do
  implicitNumber <- ask
  return $ 3*number + implicitNumber

main :: IO ()
main = do
  print $ runReader (trebleNumberAndAddImplicitly 3) 1

Неявное значение находится за пределами ВЫЧИСЛЕНИЯ, но доступно с помощьюReader.

Внутри C++ это называетсяcapture clause. Результат:output is: 10. Однако у него больше ограничений, чем у Haskell. Но на мой взгляд, это похоже.

      #include<iostream>

int main(){
    int implicitNumber = 1;
    auto trebleNumberAndAddImplicitly = [implicitNumber](int number) -> int {
        return (3*number + implicitNumber);
    };
    std::cout << "output is: " << trebleNumberAndAddImplicitly(3);
}
Другие вопросы по тегам