Читатель Монада настолько сложный и кажется бесполезным. На императивном языке, таком как Java или С++, нет эквивалентного термина для монады читателя (если я прав).
Можете ли вы дать мне простой пример и немного разъяснить мне?
Читатель Монада настолько сложный и кажется бесполезным. На императивном языке, таком как Java или С++, нет эквивалентного термина для монады читателя (если я прав).
Можете ли вы дать мне простой пример и немного разъяснить мне?
Не бойтесь! Мода читателя на самом деле не так сложна и имеет настоящую простую в использовании утилиту.
Есть два способа приблизиться к монаде: мы можем спросить
Из первого подхода монада-читатель представляет собой абстрактный тип
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
является просто функцией композиции в другом порядке!
Я помню, как я был озадачен, пока не обнаружил, что варианты монады читателей повсюду. Как я узнал об этом? Потому что я продолжал писать код, который оказался небольшим изменением.
Например, однажды я написал код для обработки исторических значений; значения, которые меняются со временем. Очень простая модель этого - это функции от точек времени до значения в этот момент времени:
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
, когда вы применяете эту модель:
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
.
В Java или С++ вы можете получить доступ к любой переменной из любого места без каких-либо проблем. Проблемы возникают, когда ваш код становится многопоточным.
В Haskell у вас есть только два способа передать значение из одной функции в другую:
fn1 -> fn2 -> fn3
, функция fn2
может не нуждаться в параметре, который вы передаете от fn1
до fn3
.Монада Reader просто передает данные, которые вы хотите разделить между функциями. Функции могут читать эти данные, но не могут их изменить. Это все, что делает монада читателей. Ну, почти все. Существует также ряд функций, таких как local
, но вы можете в первый раз использовать только asks
.