Читательская монада настолько сложна и кажется бесполезной. В императивном языке, таком как Java или C ++, нет эквивалентной концепции для читающей монады, если я не ошибаюсь.
Вы можете привести мне простой пример и немного прояснить это?
Читательская монада настолько сложна и кажется бесполезной. В императивном языке, таком как Java или C ++, нет эквивалентной концепции для читающей монады, если я не ошибаюсь.
Вы можете привести мне простой пример и немного прояснить это?
Ответы:
Не бойтесь! Монада чтения на самом деле не так уж сложна и имеет очень простую в использовании утилиту.
Есть два способа приблизиться к монаде: мы можем спросить
С первого подхода монада читателя - это некий абстрактный тип
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
просто композиция функций с порядком функций переключились!
Reader
есть функция с какой-то конкретной реализацией класса типа монады? Сказать это раньше помогло бы мне немного меньше озадачиться. Сначала я этого не понимал. На полпути я подумал: «О, это позволяет вам вернуть что-то, что даст вам желаемый результат, если вы укажете недостающее значение». Я подумал, что это полезно, но внезапно понял, что функция делает именно это.
local
функция требует дополнительных пояснений ..
(Reader f) >>= g = (g (f x))
?
x
?
Помню, я был озадачен, как и вы, пока сам не обнаружил, что варианты монады Читателя встречаются повсюду . Как я это обнаружил? Потому что я продолжал писать код, который оказался небольшими вариациями.
Например, однажды я писал код для работы с историческими значениями; ценности, которые меняются со временем. Очень простая модель этого - функции от моментов времени до значения в этот момент времени:
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
историю VP
s, которая у них была.
Ну, это 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
Хорошая аналогия: a 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
с. Functor
= сделать, Reader
что изменяет вывод другого Reader
; Applicative
= подключить два Reader
s к одному входу и объединить их выходы; Monad
= проверить результат a Reader
и использовать его для построения другого Reader
. Функции local
и withReader
= создают, Reader
который изменяет ввод на другой Reader
.
GeneralizedNewtypeDeriving
расширение для вывода Functor
, Applicative
, Monad
и т.д. для ньютайпов на основе базовых их типов.
В Java или C ++ вы можете без проблем получить доступ к любой переменной из любого места. Проблемы возникают, когда ваш код становится многопоточным.
В Haskell у вас есть только два способа передать значение от одной функции к другой:
fn1 -> fn2 -> fn3
функция fn2
может не понадобиться параметр , который вы передаете от fn1
до fn3
.Монада Reader просто передает данные, которые вы хотите разделить между функциями. Функции могут читать эти данные, но не могут их изменить. Это все, чем занимается монада Читателя. Ну почти все. Также есть ряд функций вроде local
, но в первый раз можно придерживаться asks
только.
do
нотации, который лучше было бы преобразовать в чистую функцию.
where
предложения, будет ли это принято как третий способ передачи переменных?