Ответ 1
Не бойтесь! Мода читателя на самом деле не так сложна и имеет настоящую простую в использовании утилиту.
Есть два способа приблизиться к монаде: мы можем спросить
- Что делает монада ? С какими операциями он оснащен? Для чего это полезно?
- Как осуществляется монада? Откуда он возникает?
Из первого подхода монада-читатель представляет собой абстрактный тип
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
является просто функцией композиции в другом порядке!