A mônada do leitor é tão complexa e parece inútil. Em uma linguagem imperativa como Java ou C ++, não existe um conceito equivalente para a mônada do leitor, se não me engano.
Você pode me dar um exemplo simples e esclarecer um pouco isso?
A mônada do leitor é tão complexa e parece inútil. Em uma linguagem imperativa como Java ou C ++, não existe um conceito equivalente para a mônada do leitor, se não me engano.
Você pode me dar um exemplo simples e esclarecer um pouco isso?
Respostas:
Não tenha medo! A mônada do leitor não é tão complicada e tem uma utilidade muito fácil de usar.
Existem duas maneiras de abordar uma mônada: podemos perguntar
Desde a primeira abordagem, a mônada do leitor é algum tipo abstrato
data Reader env a
de tal modo que
-- 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
Então, como usamos isso? Bem, a mônada do leitor é boa para passar informações de configuração (implícitas) por meio de um cálculo.
Sempre que você tem uma "constante" em um cálculo que você precisa em vários pontos, mas realmente gostaria de ser capaz de realizar o mesmo cálculo com valores diferentes, você deve usar um leitor de mônada.
As mônadas de leitor também são usadas para fazer o que as pessoas OO chamam de injeção de dependência . Por exemplo, o algoritmo negamax é usado com frequência (em formas altamente otimizadas) para calcular o valor de uma posição em um jogo para dois jogadores. O algoritmo em si, entretanto, não se importa com qual jogo você está jogando, exceto que você precisa ser capaz de determinar quais são as "próximas" posições no jogo, e você precisa ser capaz de dizer se a posição atual é uma posição de vitória.
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
Isso funcionará com qualquer jogo finito e determinístico para dois jogadores.
Esse padrão é útil mesmo para coisas que não são realmente injeção de dependência. Suponha que você trabalhe com finanças, possa criar uma lógica complicada para precificar um ativo (um derivado, digamos), o que é muito bom e você pode fazer sem nenhuma mônada fedorenta. Mas então, você modifica seu programa para lidar com várias moedas. Você precisa ser capaz de converter moedas rapidamente. Sua primeira tentativa é definir uma função de nível superior
type CurrencyDict = Map CurrencyName Dollars
currencyDict :: CurrencyDict
para obter preços spot. Você pode então chamar este dicionário em seu código .... mas espere! Isso não vai funcionar! O dicionário de moeda é imutável e, portanto, deve ser o mesmo não apenas durante a vida do seu programa, mas desde o momento em que ele é compilado ! Então, o que você faz? Bem, uma opção seria usar a mônada do Reader:
computePrice :: Reader CurrencyDict Dollars
computePrice
= do currencyDict <- ask
--insert computation here
Talvez o caso de uso mais clássico seja na implementação de interpretadores. Mas, antes de olharmos para isso, precisamos apresentar outra função
local :: (env -> env) -> Reader env a -> Reader env a
Ok, então Haskell e outras linguagens funcionais são baseadas no cálculo lambda . O cálculo lambda tem uma sintaxe que parece
data Term = Apply Term Term | Lambda String Term | Var Term deriving (Show)
e queremos escrever um avaliador para este idioma. Para fazer isso, precisaremos manter o controle de um ambiente, que é uma lista de associações associadas a termos (na verdade, serão encerramentos porque queremos fazer escopo estático).
newtype Env = Env ([(String, Closure)])
type Closure = (Term, Env)
Quando terminarmos, devemos obter um valor (ou um erro):
data Value = Lam String Closure | Failure String
Então, vamos escrever o intérprete:
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!
Finalmente, podemos usá-lo passando um ambiente trivial:
interp :: Term -> Value
interp term = runReader (interp' term) (Env [])
E é isso. Um intérprete totalmente funcional para o cálculo lambda.
A outra maneira de pensar sobre isso é perguntando: como isso é implementado? A resposta é que a mônada do leitor é, na verdade, uma das mais simples e elegantes de todas as mônadas.
newtype Reader env a = Reader {runReader :: env -> a}
Leitor é apenas um nome sofisticado para funções! Já definimos, runReader
e quanto às outras partes da API? Bem, todo Monad
também é um Functor
:
instance Functor (Reader env) where
fmap f (Reader g) = Reader $ f . g
Agora, para obter uma mônada:
instance Monad (Reader env) where
return x = Reader (\_ -> x)
(Reader f) >>= g = Reader $ \x -> runReader (g (f x)) x
o que não é tão assustador. ask
é muito simples:
ask = Reader $ \x -> x
enquanto local
não é tão ruim:
local f (Reader g) = Reader $ \x -> runReader g (f x)
Ok, então a mônada do leitor é apenas uma função. Por que ter o Reader? Boa pergunta. Na verdade, você não precisa disso!
instance Functor ((->) env) where
fmap = (.)
instance Monad ((->) env) where
return = const
f >>= g = \x -> g (f x) x
Estes são ainda mais simples. Além disso, ask
é justo id
e local
é apenas composição de funções com a ordem das funções trocadas!
Reader
é uma função com alguma implementação particular da classe do tipo mônada? Dizer isso antes teria me ajudado a ficar um pouco menos confuso. Primeiro eu não estava entendendo. No meio do caminho, pensei "Oh, isso permite que você retorne algo que dará o resultado desejado, uma vez que você forneça o valor ausente." Achei isso útil, mas de repente percebi que uma função faz exatamente isso.
local
função precisa de mais explicações.
(Reader f) >>= g = (g (f x))
?
x
?
Lembro-me de ter ficado intrigado como você, até que descobri por conta própria que variantes da mônada do Reader estão por toda parte . Como eu descobri isso? Porque eu continuei escrevendo código que acabou sendo pequenas variações dele.
Por exemplo, em um ponto eu estava escrevendo algum código para lidar com valores históricos ; valores que mudam com o tempo. Um modelo muito simples disso é funções de pontos de tempo para o valor naquele ponto no tempo:
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
A Applicative
instância significa que se você tem employees :: History Day [Person]
e customers :: History Day [Person]
pode fazer isso:
-- | For any given day, the list of employees followed by the customers
employeesAndCustomers :: History Day [Person]
employeesAndCustomers = (++) <$> employees <*> customers
Ou seja, Functor
e Applicative
nos permitem adaptar funções regulares, não-históricas para trabalhar com histórias.
A instância da mônada é mais intuitivamente entendida considerando a função (>=>) :: Monad m => (a -> m b) -> (b -> m c) -> a -> m c
. Uma função de tipo a -> History t b
é uma função que mapeia um a
para um histórico de b
valores; por exemplo, você poderia ter getSupervisor :: Person -> History Day Supervisor
, e getVP :: Supervisor -> History Day VP
. Portanto, a instância de Monad para History
trata de funções de composição como essas; por exemplo, getSupervisor >=> getVP :: Person -> History Day VP
é a função que obtém, para qualquer Person
, o histórico de VP
s que eles tiveram.
Bem, esta History
mônada é exatamente igual a Reader
. History t a
é realmente o mesmo que Reader t a
(que é o mesmo que t -> a
).
Outro exemplo: tenho feito protótipos de designs OLAP em Haskell recentemente. Uma ideia aqui é a de um "hipercubo", que é um mapeamento de interseções de um conjunto de dimensões para valores. Aqui vamos nós novamente:
newtype Hypercube intersection value = Hypercube { get :: intersection -> value }
Uma operação comum em hipercubos é aplicar funções escalares de vários locais aos pontos correspondentes de um hipercubo. Podemos obter isso definindo uma Applicative
instância para 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)
Acabei de copiar o History
código acima e mudar os nomes. Como você pode perceber, Hypercube
também é justo Reader
.
Isso continua e continua. Por exemplo, os intérpretes de linguagem também se resumem a Reader
, quando você aplica este modelo:
Reader
ask
Reader
ambiente de execução.local
Uma boa analogia é que a Reader r a
representa um a
com "buracos" que o impedem de saber do que a
estamos falando. Você só pode obter um real a
depois de fornecer um r
para preencher os buracos. Existem toneladas de coisas assim. Nos exemplos acima, um "histórico" é um valor que não pode ser calculado até que você especifique um tempo, um hipercubo é um valor que não pode ser calculado até que você especifique uma interseção e uma expressão de linguagem é um valor que pode não deve ser calculado até que você forneça os valores das variáveis. Também lhe dá uma intuição sobre por que Reader r a
é o mesmo que r -> a
, porque essa função também é intuitivamente um an a
ausente r
.
Portanto Functor
, as instâncias Applicative
e Monad
de Reader
são uma generalização muito útil para casos em que você está modelando qualquer coisa do tipo "um a
que está faltando um r
" e permite que você trate esses objetos "incompletos" como se fossem completos.
Contudo uma outra maneira de dizer a mesma coisa: um Reader r a
é algo que consome r
e produz a
, eo Functor
, Applicative
e Monad
exemplos são padrões básicos para trabalhar com Reader
s. Functor
= fazer um Reader
que modifica a saída de outro Reader
; Applicative
= conecte dois Reader
s à mesma entrada e combine suas saídas; Monad
= inspecionar o resultado de a Reader
e usá-lo para construir outro Reader
. As funções local
e withReader
= fazem um Reader
que modifica a entrada para outra Reader
.
GeneralizedNewtypeDeriving
extensão para derivar Functor
, Applicative
, Monad
, etc. para Newtypes com base em seus tipos subjacentes.
Em Java ou C ++ você pode acessar qualquer variável de qualquer lugar sem nenhum problema. Os problemas aparecem quando seu código se torna multi-thread.
Em Haskell, você tem apenas duas maneiras de passar o valor de uma função para outra:
fn1 -> fn2 -> fn3
função fn2
pode não precisar do parâmetro que você passa de fn1
para fn3
.A mônada do Reader apenas passa os dados que você deseja compartilhar entre as funções. As funções podem ler esses dados, mas não podem alterá-los. Isso é tudo que faz a mônada do leitor. Bem, quase todos. Existem também várias funções como local
, mas pela primeira vez você pode ficar com asks
apenas.
do
anotação, o que seria melhor se fosse refatorado em uma função pura.
where
cláusula, ela será aceita como uma 3ª forma de passar variáveis?