Qual é o propósito da mônada do leitor?

122

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?

chipbk10
fonte
21
Você usa a mônada do leitor se quiser - na ocasião - ler alguns valores de um ambiente (não modificável), mas não quiser passar explicitamente esse ambiente adiante. Em Java ou C ++, você usaria variáveis ​​globais (embora não sejam exatamente iguais).
Daniel Fischer
5
@Daniel: Isso parece muito com uma resposta
SingleNegationElimination
@TokenMacGuy Muito curto para uma resposta, e agora é tarde demais para eu pensar em algo mais. Se ninguém mais o fizer, farei isso depois de dormir.
Daniel Fischer
8
Em Java ou C ++, a mônada do Reader seria análoga aos parâmetros de configuração passados ​​para um objeto em seu construtor que nunca são alterados durante o tempo de vida do objeto. Em Clojure, seria um pouco como uma variável com escopo dinâmico usada para parametrizar o comportamento de uma função sem precisar passá-la explicitamente como um parâmetro.
danidiaz

Respostas:

169

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

  1. O que a mônada faz ? Com que operações está equipado? Para que serve?
  2. Como a mônada é implementada? De onde isso surge?

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, runReadere quanto às outras partes da API? Bem, todo Monadtambé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 localnã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 ide localé apenas composição de funções com a ordem das funções trocadas!

Philip JF
fonte
6
Resposta muito interessante. Honestamente, eu leio de novo muitas vezes, quando quero fazer uma revisão da mônada. A propósito, sobre o algoritmo de nagamax, "valores <- mapM (negar. Negamax (negar cor)) possível" não parece correto. Eu sei disso, o código que você fornece é apenas para mostrar como funciona a mônada do leitor. Mas se você tiver tempo, você poderia corrigir o código do algoritmo negamax? Porque é interessante quando você usa a mônada do leitor para resolver o negamax.
chipbk10
4
Então, 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.
ziggystar de
1
Depois de ler isso, entendi quase tudo. A localfunção precisa de mais explicações.
Christophe De Troyer,
@Philip Tenho uma pergunta sobre a instância Monad. Não podemos escrever a função bind como (Reader f) >>= g = (g (f x))?
zeronone de
@zeronone onde está x?
Ashish Negi
56

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 Applicativeinstâ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, Functore Applicativenos 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 apara um histórico de bvalores; por exemplo, você poderia ter getSupervisor :: Person -> History Day Supervisor, e getVP :: Supervisor -> History Day VP. Portanto, a instância de Monad para Historytrata 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 VPs que eles tiveram.

Bem, esta Historymô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 Applicativeinstâ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 Historycódigo acima e mudar os nomes. Como você pode perceber, Hypercubetambé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:

  • Expression = a Reader
  • Variáveis ​​livres = usos de ask
  • Ambiente de avaliação = Readerambiente de execução.
  • Construções de ligação = local

Uma boa analogia é que a Reader r arepresenta um acom "buracos" que o impedem de saber do que aestamos falando. Você só pode obter um real adepois de fornecer um rpara 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 aausente r.

Portanto Functor, as instâncias Applicativee Monadde Readersão uma generalização muito útil para casos em que você está modelando qualquer coisa do tipo "um aque 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 re produz a, eo Functor, Applicativee Monadexemplos são padrões básicos para trabalhar com Readers. Functor= fazer um Readerque modifica a saída de outro Reader; Applicative= conecte dois Readers à mesma entrada e combine suas saídas; Monad= inspecionar o resultado de a Readere usá-lo para construir outro Reader. As funções locale withReader= fazem um Readerque modifica a entrada para outra Reader.

Luis casillas
fonte
5
Ótima resposta. Você também pode usar a GeneralizedNewtypeDerivingextensão para derivar Functor, Applicative, Monad, etc. para Newtypes com base em seus tipos subjacentes.
Rein Henrichs
20

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:

  • Você passa o valor por meio de um dos parâmetros de entrada da função que pode ser chamada. As desvantagens são: 1) você não pode passar TODAS as variáveis ​​dessa forma - a lista de parâmetros de entrada simplesmente o surpreende. 2) na sequência de chamadas de função:, a fn1 -> fn2 -> fn3função fn2pode não precisar do parâmetro que você passa de fn1para fn3.
  • Você passa o valor no escopo de alguma mônada. A desvantagem é: você tem que entender bem o que é a concepção da Mônada. Passar os valores é apenas uma das muitas aplicações em que você pode usar as Mônadas. Na verdade, a concepção da Mônada é incrivelmente poderosa. Não fique chateado, se você não teve uma visão imediata. Continue tentando e leia diversos tutoriais. O conhecimento que você obterá valerá a pena.

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 asksapenas.

Dmitry Bespalov
fonte
3
Uma outra desvantagem de usar mônadas para passar dados implicitamente é que é muito fácil encontrar-se escrevendo muitos códigos de 'estilo imperativo' em doanotação, o que seria melhor se fosse refatorado em uma função pura.
Benjamin Hodgson
4
@BenjaminHodgson Escrever código de 'aparência imperativa' com mônadas na notação do não significa necessariamente escrever código com efeito lateral (impuro). Na verdade, o código com efeito lateral em Haskell pode ser possível apenas dentro da mônada IO.
Dmitry Bespalov
Se a outra função for anexada a outra por uma wherecláusula, ela será aceita como uma 3ª forma de passar variáveis?
Elmex80s