Diferentes maneiras de ver uma mônada


29

Enquanto aprendia Haskell, enfrentei muitos tutoriais tentando explicar o que são mônadas e por que as mônadas são importantes em Haskell. Cada um deles usou analogias para que fosse mais fácil entender o significado. No final do dia, acabei com três visões diferentes do que é uma mônada:

Vista 1: Mônada como um rótulo

Às vezes acho que uma mônada como um rótulo para um tipo específico. Por exemplo, uma função do tipo:

myfunction :: IO Int

myfunction é uma função que sempre que é executada gera um valor Int. O tipo do resultado não é Int, mas IO Int. Portanto, IO é um rótulo do valor Int alertando o usuário para saber que o valor Int é o resultado de um processo em que uma ação de IO foi feita.

Conseqüentemente, esse valor Int foi marcado como um valor proveniente de um processo com IO, portanto, esse valor é "sujo". Seu processo não é mais puro.

Vista 2: Mônada como um espaço privado onde coisas desagradáveis ​​podem acontecer.

Em um sistema em que todo o processo é puro e rigoroso, às vezes você precisa ter efeitos colaterais. Assim, uma mônada é apenas um pequeno espaço que lhe permite fazer efeitos colaterais desagradáveis. Nesse espaço, você pode escapar do mundo puro, ficar impuro, fazer seu processo e depois voltar com um valor.

Vista 3: Mônada como na teoria das categorias

Esta é a visão que eu não entendo completamente. Uma mônada é apenas um functor para a mesma categoria ou subcategoria. Por exemplo, você tem os valores Int e como uma subcategoria IO Int, que são os valores Int gerados após um processo de IO.

Essas visualizações estão corretas? Qual é mais preciso?


5
# 2 não é o que uma mônada é em geral. De fato, é praticamente restrito ao IO, e não é uma visão útil (cf. O que uma Mônada não é ). Além disso, geralmente "rigoroso" é usado para nomear uma propriedade que Haskell não possui (ou seja, avaliação estrita). A propósito, as Mônadas também não mudam isso (novamente, consulte O que uma Mônada não é).

3
Tecnicamente, apenas o terceiro está correto. A mônada é endofuncora, pois são definidas operações especiais - promoção e encadernação. As mônadas são numerosas - uma lista de mônadas é um exemplo perfeito para obter intuição por trás das mônadas. As instalações de leitura são ainda melhores. Surpreendentemente, as mônadas são utilizáveis ​​como ferramentas para encadear o estado na linguagem funcional pura implicitamente. Isso não é uma propriedade definidora das mônadas: é coincidência que a segmentação de estado possa ser implementada em seus termos. O mesmo se aplica ao IO.
Permeakra

O Lisp comum possui seu próprio compilador como parte do idioma. Haskell tem mônadas.
Will Ness

Respostas:


33

As visualizações 1 e 2 estão incorretas em geral.

  1. Qualquer tipo de dado * -> *pode funcionar como um rótulo; as mônadas são muito mais que isso.
  2. (Com exceção da IOmônada) os cálculos dentro de uma mônada não são impuros. Eles simplesmente representam cálculos que percebemos como tendo efeitos colaterais, mas são puros.

Esses dois mal-entendidos vêm do foco na IOmônada, que é realmente um pouco especial.

Vou tentar elaborar um pouco o número 3, sem entrar na teoria das categorias, se possível.


Cálculos padrão

Todos os cálculos em uma linguagem de programação funcional pode ser visto como funções com um tipo de fonte e de um tipo de destino: f :: a -> b. Se uma função tiver mais de um argumento, podemos convertê-la em uma função de um argumento fazendo um curry (consulte também o wiki do Haskell ). E se temos apenas um valor x :: a(uma função com 0 argumentos), podemos convertê-lo em uma função que leva um argumento do tipo de unidade : (\_ -> x) :: () -> a.

Podemos construir programas mais complexos, a partir de programas mais simples, compondo essas funções usando o .operador. Por exemplo, se temos f :: a -> be g :: b -> crecebemos g . f :: a -> c. Observe que isso também funciona para nossos valores convertidos: se o tivermos x :: ae o convertermos em nossa representação, obteremos f . ((\_ -> x) :: () -> a) :: () -> b.

Essa representação possui algumas propriedades muito importantes, a saber:

  • Temos uma função muito especial - a função de identidadeid :: a -> a para cada tipo a. É um elemento de identidade com relação a .: fé igual a f . ide a id . f.
  • O operador de composição da função .é associativo .

Cálculos monádicos

Suponha que desejemos selecionar e trabalhar com alguma categoria especial de cálculos, cujo resultado contenha algo além do valor de retorno único. Não queremos especificar o que "algo a mais" significa, queremos manter as coisas o mais geral possível. A maneira mais geral de representar "algo a mais" é representá-la como uma função de tipo - um tipo mde tipo * -> *(ou seja, converte um tipo para outro). Portanto, para cada categoria de computação com a qual queremos trabalhar, teremos alguma função de tipo m :: * -> *. (Em Haskell, mé [], IO, Maybe, etc.) E a vontade categoria contém todas as funções de tipos a -> m b.

Agora gostaríamos de trabalhar com as funções nessa categoria da mesma maneira que no caso básico. Queremos poder compor essas funções, queremos que a composição seja associativa e queremos ter uma identidade. Nós precisamos:

  • Ter um operador (vamos chamá-lo <=<) que compõe funções f :: a -> m be g :: b -> m cem algo como g <=< f :: a -> m c. E, deve ser associativo.
  • Para ter alguma função de identidade para cada tipo, vamos chamá-lo return. Também queremos que f <=< returnseja igual fe igual a return <=< f.

Qualquer um m :: * -> *para o qual tenhamos tais funções returne <=<seja chamado de mônada . Ele nos permite criar cálculos complexos a partir de cálculos mais simples, como no caso básico, mas agora os tipos de valores de retorno são transformados m.

(Na verdade, abusei levemente do termo categoria aqui. No sentido da teoria da categoria, podemos chamar nossa construção de categoria somente depois que soubermos que ela obedece a essas leis.)

Mônadas em Haskell

Em Haskell (e outras linguagens funcionais), trabalhamos principalmente com valores, não com funções de tipos () -> a. Então, em vez de definir <=<para cada mônada, definimos uma função (>>=) :: m a -> (a -> m b) -> m b. Essa definição alternativa é equivalente, podemos expressar >>=usando <=<e vice-versa (tente como um exercício ou veja as fontes ). O princípio é menos óbvio agora, mas permanece o mesmo: nossos resultados são sempre de tipos m ae compomos funções de tipos a -> m b.

Para cada mônada que criamos, não devemos esquecer de verificar isso returne <=<ter as propriedades necessárias: associatividade e identidade esquerda / direita. Expressa usando returne >>=eles são chamados de leis de mônada .

Um exemplo - listas

Se optarmos mpor ser [], obteremos uma categoria de funções dos tipos a -> [b]. Tais funções representam cálculos não determinísticos, cujos resultados podem ser um ou mais valores, mas também nenhum valor. Isso dá origem à chamada mônada da lista . A composição f :: a -> [b]e g :: b -> [c]funciona da seguinte maneira: g <=< f :: a -> [c]significa calcular todos os resultados possíveis do tipo [b], aplicar ga cada um deles e coletar todos os resultados em uma única lista. Expressado em Haskell

return :: a -> [a]
return x = [x]
(<=<) :: (b -> [c]) -> (a -> [b]) -> (a -> [c])
g (<=<) f  = concat . map g . f

ou usando >>=

(>>=) :: [a] -> (a -> [b]) -> [b]
x >>= f  = concat (map f x)

Observe que neste exemplo os tipos de retorno eram, [a]portanto, era possível que eles não contivessem nenhum valor do tipo a. De fato, não existe um requisito para uma mônada de que o tipo de retorno tenha esses valores. Algumas mônadas sempre têm (gostam IOou State), mas outras não, gostam []ou Maybe.

Mônada IO

Como mencionei, a IOmônada é um tanto especial. Um valor do tipo IO asignifica um valor do tipo aconstruído pela interação com o ambiente do programa. Portanto (ao contrário de todas as outras mônadas), não podemos descrever um valor do tipo IO ausando alguma construção pura. Aqui IOestá simplesmente uma etiqueta ou rótulo que distingue os cálculos que interagem com o ambiente. Este é (o único caso) em que as visualizações 1 e 2 estão corretas.

Para a IOmônada:

  • Composição f :: a -> IO be g :: b -> IO cmeios: Calcule fque interage com o ambiente e, em seguida, calcule gque usa o valor e calcula o resultado interagindo com o ambiente.
  • returnapenas adiciona a IO"tag" ao valor (simplesmente "calculamos" o resultado mantendo o ambiente intacto).
  • As leis da mônada (associatividade, identidade) são garantidas pelo compilador.

Algumas notas:

  1. Como os cálculos monádicos sempre têm o tipo de resultado m a, não há como "escapar" da IOmônada. O significado é: depois que um cálculo interage com o ambiente, você não pode construir um cálculo que não o faça.
  2. Quando um programador funcional não sabe como fazer algo de uma maneira pura, ele pode (como último recurso) programar a tarefa por algum cálculo de estado dentro da IOmônada. É por isso que IOcostuma ser chamado de bin do pecado de um programador .
  3. Observe que em um mundo impuro (no sentido de programação funcional) a leitura de um valor também pode mudar o ambiente (como consumir a entrada do usuário). É por isso que funções como getChardevem ter um tipo de resultado IO something.

3
Ótima resposta. Eu esclareceria que IOnão tem semântica especial do ponto de vista da linguagem. É não especial, ele se comporta como qualquer outro código. Somente a implementação da biblioteca de tempo de execução é especial. Além disso, existe uma maneira de finalidade especial de escape ( unsafePerformIO). Eu acho que isso é importante porque as pessoas geralmente pensam IOcomo um elemento de linguagem especial ou uma etiqueta declarativa. Não é.
usr

2
Bom ponto. Eu acrescentaria que o unsafePerformIO é realmente inseguro e deve ser usado apenas por especialistas. Ele permite que você quebre tudo, por exemplo, você pode criar uma função coerce :: a -> bque converte dois tipos (e trava seu programa na maioria dos casos). Veja este exemplo - você pode converter até uma função em Intetc.
Petr Pudlák

Outra mônada "mágica especial" seria ST, que permite declarar referências à memória das quais você pode ler e escrever conforme achar melhor (embora apenas dentro da mônada) e, em seguida, extrair um resultado chamandorunST :: (forall s. GHC.ST.ST s a) -> a
sara

5

Vista 1: Mônada como um rótulo

"Conseqüentemente, esse valor Int foi marcado como um valor proveniente de um processo com IO, portanto, esse valor é" sujo "."

"IO Int" geralmente não é um valor Int (embora possa ser, em alguns casos, como "return 3"). É um procedimento que gera algum valor Int. Execuções diferentes deste "procedimento" podem gerar valores Int diferentes.

Uma mônada m é uma "linguagem de programação" incorporada (imperativa): dentro dessa linguagem é possível definir alguns "procedimentos". Um valor monádico (do tipo ma) é um procedimento nesta "linguagem de programação" que gera um valor do tipo a.

Por exemplo:

foo :: IO Int

é um procedimento que gera um valor do tipo Int.

Então:

bar :: IO (Int, Int)
bar = do
  a <- foo
  b <- foo
  return (a,b)

é um procedimento que gera duas Ints (possivelmente diferentes).

Cada "linguagem" desse tipo suporta algumas operações:

  • dois procedimentos (ma e mb) podem ser "concatenados": você pode criar um procedimento maior (ma >> mb) feito do primeiro e depois do segundo;

  • além do mais, a saída (a) da primeira pode afetar a segunda (ma >> = \ a -> ...);

  • um procedimento (retorno x) pode render algum valor constante (x).

As diferentes linguagens de programação incorporadas diferem quanto ao tipo de suporte, como:

  • produzindo valores aleatórios;
  • "bifurcação" (a [] mônada);
  • exceções (lançamento / captura) (The Either monad);
  • continuação explícita / suporte a callcc;
  • enviar / receber mensagens para outros "agentes";
  • crie, defina e leia variáveis ​​(locais para esta linguagem de programação) (a mônada ST).

1

Não confunda um tipo monádico com a classe monad.

Um tipo monádico (isto é, um tipo sendo uma instância da classe monad) resolveria um problema específico (em princípio, cada tipo monádico resolve um problema diferente): Estado, Aleatório, Talvez, E / S. Todos eles são tipos com contexto (o que você chama de "rótulo", mas não é isso que os torna uma mônada).

Para todos eles, há a necessidade de "encadear operações com opção" (uma operação depende do resultado da anterior). Aqui entra em jogo a classe mônada: faça com que seu tipo (resolvendo um determinado problema) seja uma instância da classe mônada e o problema de encadeamento seja resolvido.

Consulte O que a classe da mônada resolve?

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.