it-swarm.com.ru

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

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

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

108
chipbk10

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

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

  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

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

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

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

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

147
Philip JF

Я помню, что был озадачен, как и вы, пока я не обнаружил, что варианты монады 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. Таким образом, экземпляр 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 @[email protected] hypercube to its corresponding point 
    -- in @[email protected]
    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 и MonadReader являются очень полезным обобщением для случаев, когда вы моделируете что-либо типа "a", в котором отсутствует r ", и позволяют обрабатывать эти" неполные "объекты так, как если бы они были завершены.

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

50
Luis Casillas

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

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

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

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

19
Dmitry Bespalov