Pergunta

Alguém poderia dar algumas dicas sobre por que os cálculos impuros em Haskell são modelados como mônadas?

Quero dizer, a Monad é apenas uma interface com 4 operações, então qual foi o raciocínio para modelar efeitos colaterais nela?

Foi útil?

Solução

Suponha que uma função tenha efeitos colaterais. Se tomarmos todos os efeitos que produz como os parâmetros de entrada e saída, a função é pura para o mundo exterior.

Então, para uma função impura

f' :: Int -> Int

Adicionamos o mundo real à consideração

f :: Int -> RealWorld -> (Int, RealWorld)
-- input some states of the whole world,
-- modify the whole world because of the side effects,
-- then return the new world.

então f é puro novamente. Definimos um tipo de dados parametrizado IO a = RealWorld -> (a, RealWorld), então não precisamos digitar o Realworld tantas vezes

f :: Int -> IO Int

Para o programador, lidar diretamente com um mundo real é muito perigoso - em particular, se um programador colocar as mãos em um valor de tipo real, eles podem tentar cópia de É basicamente impossível. (Pense em tentar copiar todo o sistema de arquivos, por exemplo. Onde você o colocaria?) Portanto, nossa definição de IO também encapsula os estados do mundo inteiro.

Essas funções impuras são inúteis se não pudermos acorrentá -las. Considerar

getLine :: IO String               = RealWorld -> (String, RealWorld)
getContents :: String -> IO String = String -> RealWorld -> (String, RealWorld)
putStrLn :: String -> IO ()        = String -> RealWorld -> ((), RealWorld)

Queremos obter um nome de arquivo do console, ler esse arquivo e imprimir o conteúdo. Como faríamos isso se pudéssemos acessar os estados do mundo real?

printFile :: RealWorld -> ((), RealWorld)
printFile world0 = let (filename, world1) = getLine world0
                       (contents, world2) = (getContents filename) world1 
                   in  (putStrLn contents) world2 -- results in ((), world3)

Vemos um padrão aqui: as funções são chamadas assim:

...
(<result-of-f>, worldY) = f worldX
(<result-of-g>, worldZ) = g <result-of-f> worldY
...

Para que pudéssemos definir um operador ~~~ para ligá -los:

(~~~) :: (IO b) -> (b -> IO c) -> IO c

(~~~) ::      (RealWorld -> (b, RealWorld))
      -> (b -> RealWorld -> (c, RealWorld))
      ->       RealWorld -> (c, RealWorld)
(f ~~~ g) worldX = let (resF, worldY) = f worldX in
                        g resF worldY

Então poderíamos simplesmente escrever

printFile = getLine ~~~ getContents ~~~ putStrLn

sem tocar no mundo real.


Agora, suponha que queremos tornar o conteúdo do arquivo manchado também. A mancha é uma função pura

upperCase :: String -> String

Mas para entrar no mundo real, ele tem que devolver um IO String. É fácil levantar essa função:

impureUpperCase :: String -> RealWorld -> (String, RealWorld)
impureUpperCase str world = (upperCase str, world)

Isso pode ser generalizado:

impurify :: a -> IO a

impurify :: a -> RealWorld -> (a, RealWorld)
impurify a world = (a, world)

de modo a impureUpperCase = impurify . upperCase, e podemos escrever

printUpperCaseFile = 
    getLine ~~~ getContents ~~~ (impurify . upperCase) ~~~ putStrLn

(Nota: normalmente escrevemos getLine ~~~ getContents ~~~ (putStrLn . upperCase))


Agora vamos ver o que fizemos:

  1. Definimos um operador (~~~) :: IO b -> (b -> IO c) -> IO c que correntes duas funções impuras juntas
  2. Nós definimos uma função impurify :: a -> IO a que converte um valor puro em impuro.

Agora fazemos a identificação (>>=) = (~~~) e return = impurify, e veja? Temos uma mônada.


(Para verificar se é realmente uma mônada, há poucos axiomas devem ser satisfeitos:

(1) return a >>= f = f a

  impurify a               = (\world -> (a, world))
 (impurify a ~~~ f) worldX = let (resF, worldY) = (\world -> (a, world)) worldX 
                             in f resF worldY
                           = let (resF, worldY) =            (a, worldX))       
                             in f resF worldY
                           = f a worldX

(2) f >>= return = f

  (f ~~~ impurify) a worldX = let (resF, worldY) = impuify a worldX 
                              in f resF worldY
                            = let (resF, worldY) = (a, worldX)     
                              in f resF worldY
                            = f a worldX

(3) f >>= (\x -> g x >>= h) = (f >>= g) >>= h

Exercício.)

Outras dicas

Alguém poderia dar algumas dicas sobre por que os cálculos descompactos em Haskell são modelados como mônadas?

Esta questão contém um mal -entendido generalizado. Impureza e mônada são noções independentes. Impureza é não modelado por Monad. Ao contrário, existem alguns tipos de dados, como IO, que representam computação imperativa. E para alguns desses tipos, uma pequena fração de sua interface corresponde ao padrão de interface chamado "Monad". Além disso, não existe uma explicação pura/funcional/denotativa conhecida de IO (e é improvável que haja um, considerando o "Bin Sin" o propósito de IO), embora exista a história comum sobre World -> (a, World) sendo o significado de IO a. Essa história não pode descrever com sinceridade IO, Porque IO suporta simultaneidade e não determinismo. A história nem funciona quando para cálculos determinísticos que permitem a interação no meio da computação com o mundo.

Para mais explicações, veja esta resposta.

Editar: Ao reler a pergunta, não acho que minha resposta esteja no caminho certo. Modelos de computação imperativa geralmente se tornam mônicos, assim como dizia a pergunta. O ASKER pode realmente não assumir que a Monadness de alguma forma permite a modelagem de computação imperativa.

Pelo que entendi, alguém chamou Eugenio Moggi Primeiro notei que um construto matemático anteriormente obscuro chamado "Mônada" poderia ser usado para modelar efeitos colaterais nas linguagens do computador e, portanto, especificar sua semântica usando o cálculo Lambda. Quando Haskell estava sendo desenvolvido, havia várias maneiras pelas quais cálculos impuros foram modelados (ver Simon Peyton Jones ' Papel "Cabeça de Cabelo" Para mais detalhes), mas quando Phil Wadler introduziu Mônadas, rapidamente se tornou óbvio que essa era a resposta. E o resto é história.

Alguém poderia dar algumas dicas sobre por que os cálculos descompactos em Haskell são modelados como mônadas?

Bem, porque Haskell é puro. Você precisa de um conceito matemático para distinguir entre Destruta cálculos e puro sobre Nível de tipo e para modelar fluxos de programação em respectivamente.

Isso significa que você terá que acabar com algum tipo IO a Isso modela um cálculo de descomposição. Então você precisa saber maneiras de combinando esses cálculos dos quais Aplique em sequência (>>=) e levantar um valor (return) são os mais óbvios e básicos.

Com esses dois, você já definiu uma mônada (sem sequer pensar nisso);)

Além disso, as mônadas fornecem Abstrações muito gerais e poderosas, tantos tipos de fluxo de controle podem ser convenientemente generalizados em funções monádicas como sequence, liftM ou sintaxe especial, tornando a descendência não um caso tão especial.

Ver Mônadas em programação funcional e digitação de singularidade (A única alternativa que conheço) para obter mais informações.

Como você diz, Monad é uma estrutura muito simples. Metade da resposta é: Monad é a estrutura mais simples que poderíamos dar às funções de efeito colateral e poder usá-las. Com Monad Podemos fazer duas coisas: podemos tratar um valor puro como um valor de efeito colateral (return), e podemos aplicar uma função de efeito colateral a um valor de efeito colateral para obter um novo valor de efeito colateral (>>=). Perder a capacidade de fazer qualquer uma dessas coisas seria incapacitante, então nosso tipo de efeito colateral precisa ser "pelo menos" Monad, e acontece Monad é suficiente para implementar tudo o que precisamos até agora.

A outra metade é: qual é a estrutura mais detalhada que poderíamos dar a "possíveis efeitos colaterais"? Certamente, podemos pensar no espaço de todos os efeitos colaterais possíveis como um conjunto (a única operação que requer é a associação). Podemos combinar dois efeitos colaterais fazendo um após o outro, e isso dará origem a um efeito colateral diferente (ou possivelmente o mesmo - se o primeiro foi "Computador de desligamento" e o segundo foi "Arquivo de gravação", então o resultado de compor isso é apenas "Computador de desligamento").

OK, então o que podemos dizer sobre esta operação? É associativo; Ou seja, se combinarmos três efeitos colaterais, não importa em qual ordem façamos a combinação. Se o fizermos (Escreva o arquivo, leia o soquete) Em seguida, desligar o computador, é o mesmo que fazer o arquivo de gravação (Leia o soquete e depois desligar computador). Mas não é comutativo: ("Write File", em seguida, "Excluir arquivo") é um efeito colateral diferente do ("Excluir arquivo", então "Write File"). E temos uma identidade: o efeito colateral especial "sem efeitos colaterais" funciona ("sem efeitos colaterais", então "excluir arquivo" é o mesmo efeito colateral que apenas "excluir arquivo") neste momento qualquer matemático está pensando "grupo!" Mas os grupos têm inversas, e não há como inverter um efeito colateral em geral; "Excluir arquivo" é irreversível. Portanto, a estrutura que resta é a de um monóide, o que significa que nossas funções de efeito colateral devem ser mônadas.

Existe uma estrutura mais complexa? Claro! Poderíamos dividir possíveis efeitos colaterais em efeitos baseados em sistemas de arquivos, efeitos baseados em rede e muito mais, e poderíamos criar regras de composição mais elaboradas que preservassem esses detalhes. Mas, novamente, tudo se resume a: Monad é muito simples e ainda poderoso o suficiente para expressar a maioria das propriedades com as quais nos preocupamos. (Em particular, a associatividade e os outros axiomas nos vamos testar nossa aplicação em pedaços pequenos, com confiança de que os efeitos colaterais da aplicação combinada serão os mesmos que a combinação dos efeitos colaterais das peças).

Na verdade, é uma maneira bastante limpa de pensar em E/S de maneira funcional.

Na maioria das linguagens de programação, você faz operações de entrada/saída. Em Haskell, imagine escrever código para não Faz as operações, mas para gerar uma lista das operações que você gostaria de fazer.

Mônadas são apenas uma sintaxe exatamente para isso.

Se você quiser saber por que as mônadas, em oposição a outra coisa, acho que a resposta é que elas são a melhor maneira funcional de representar a E/S que as pessoas poderiam pensar quando estavam fazendo Haskell.

Afaik, o motivo é poder incluir verificações de efeitos colaterais no sistema de tipos. Se você quiser saber mais, ouça aqueles Se-radio Episódios: Episódio 108: Simon Peyton Jones em programação funcional e Haskell Episódio 72: Erik Meijer no Linq

Acima, existem respostas detalhadas muito boas com antecedentes teóricos. Mas eu quero dar minha opinião sobre a Monad IO. Não sou experiente em programador Haskell, por isso pode ser bastante ingênuo ou mesmo errado. Mas me ajudei a lidar com a Mônada de IO até certo ponto (observe que não se relaciona com outras mônadas).

Primeiro, quero dizer que esse exemplo com "mundo real" não é muito claro para mim, pois não podemos acessar seus estados anteriores (do mundo real). Pode ser que não se reeve a cálculos de Monad, mas é desejado no sentido de transparência referencial, que geralmente se apresenta no código Haskell.

Então, queremos que nossa linguagem (Haskell) seja pura. Mas precisamos de operações de entrada/saída, pois sem elas nosso programa não pode ser útil. E essas operações não podem ser puras por sua natureza. Portanto, a única maneira de lidar com isso, precisamos separar operações impuras do restante do código.

Aqui vem a Monad. Na verdade, não tenho certeza de que não existe outra construção com propriedades necessárias semelhantes, mas o ponto é que a Monad possui essas propriedades, para que possa ser usada (e é usada com sucesso). A propriedade principal é que não podemos escapar dela. A interface Monad não tem operações para se livrar da mônada em torno de nosso valor. Outras mônadas (não io) fornecem essas operações e permitem a correspondência de padrões (por exemplo, talvez), mas essas operações não estão na interface Monad. Outra propriedade necessária é a capacidade de encadear operações.

Se pensarmos no que precisamos em termos de sistema de tipo, chegamos ao fato de precisarmos digitar com o construtor, que pode ser envolvido em qualquer Vale. O construtor deve ser privado, à medida que proibimos escapar dele (ou seja, correspondência de padrões). Mas precisamos funcionar para colocar valor neste construtor (aqui o retorno vem à mente). E precisamos da maneira de encorrer as operações. Se pensarmos sobre isso por algum tempo, chegaremos ao fato de que a operação de encadeamento deve ter tipo como >> = tem. Então, chegamos a algo muito parecido com a Monad. Eu acho que, se agora analisarmos possíveis situações contraditórias com esse construto, chegaremos aos axiomas da Monad.

Observe que o construto desenvolvido não tem nada em comum com impureza. Ele tem apenas propriedades, que desejávamos ter que ser capaz de lidar com operações impuras, a saber, sem escapação, encadeamento e uma maneira de entrar.

Agora, algum conjunto de operações impuras é predefinido pelo idioma dentro desta Monad IO selecionada. Podemos combinar essas operações para criar novas operações de descomposição. E todas essas operações terão que ter IO no tipo deles. Observe, no entanto, que a presença de IO no tipo de alguma função não torna essa função impura. Mas, pelo que entendi, é uma má idéia escrever funções puras com IO no tipo deles, pois era inicialmente nossa idéia separar funções puras e impuras.

Finalmente, quero dizer que a Mônada não transforma operações impuras em puras. Ele apenas permite separá -los de maneira eficaz. (Repito, que é apenas o meu entendimento)

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top