Vou usar uma descrição independente de idioma de mônadas como esta, primeiro descrevendo monóides:
Um monóide é (aproximadamente) um conjunto de funções que usam algum tipo como parâmetro e retornam o mesmo tipo.
Uma mônada é (aproximadamente) um conjunto de funções que tomam um tipo de invólucro como parâmetro e retorna o mesmo tipo de invólucro.
Observe que são descrições, não definições. Sinta-se livre para atacar essa descrição!
Portanto, em uma linguagem OO, uma mônada permite composições de operações como:
Flier<Duck> m = new Flier<Duck>(duck).takeOff().flyAround().land()
Observe que a mônada define e controla a semântica dessas operações, em vez da classe contida.
Tradicionalmente, em uma linguagem OO, usamos uma hierarquia e herança de classes para fornecer essas semânticas. Assim teríamos uma Bird
classe com métodos takeOff()
, flyAround()
e land()
, e Duck herdaria aqueles.
Mas então temos problemas com pássaros que penguin.takeOff()
não voam, porque falham. Temos que recorrer ao lançamento e manuseio de exceção.
Além disso, quando dizemos que o Penguin é a Bird
, encontramos problemas com herança múltipla, por exemplo, se também temos uma hierarquia de Swimmer
.
Essencialmente, estamos tentando colocar classes em categorias (com desculpas aos sujeitos da teoria da categoria) e definir semântica por categoria, em vez de em classes individuais. Mas as mônadas parecem um mecanismo muito mais claro para fazer isso do que as hierarquias.
Portanto, nesse caso, teríamos uma Flier<T>
mônada como o exemplo acima:
Flier<Duck> m = new Flier<Duck>(duck).takeOff().flyAround().land()
... e nunca instanciamos a Flier<Penguin>
. Poderíamos até usar a digitação estática para impedir que isso aconteça, talvez com uma interface de marcador. Ou verificação de capacidade de execução para salvar. Mas, na verdade, um programador nunca deve colocar um pinguim no Flier, no mesmo sentido em que nunca deve dividir por zero.
Além disso, é mais aplicável em geral. Um panfleto não precisa ser um pássaro. Por exemplo Flier<Pterodactyl>
, ou Flier<Squirrel>
, sem alterar a semântica desses tipos individuais.
Uma vez que classificamos a semântica por funções composíveis em um contêiner - em vez de com hierarquias de tipos - ele resolve os problemas antigos com classes que "meio que fazem, meio que não" se encaixam em uma hierarquia específica. Também permite fácil e claramente várias semânticas para uma classe, como Flier<Duck>
também Swimmer<Duck>
. Parece que estamos lutando com uma incompatibilidade de impedâncias ao classificar o comportamento com hierarquias de classes. Mônadas lidam com isso com elegância.
Portanto, minha pergunta é: da mesma maneira que passamos a favor da composição sobre a herança, também faz sentido favorecer as mônadas sobre a herança?
(BTW, eu não tinha certeza se isso deveria estar aqui ou no Comp Sci, mas isso parece mais um problema prático de modelagem. Mas talvez seja melhor por lá.)
fonte
Respostas:
A resposta curta é não , as mônadas não são uma alternativa às hierarquias de herança (também conhecidas como polimorfismo de subtipo). Você parece estar descrevendo o polimorfismo paramétrico , que as mônadas fazem uso, mas não são a única coisa a fazer.
Tanto quanto eu as entendo, as mônadas não têm essencialmente nada a ver com herança. Eu diria que as duas coisas são mais ou menos ortogonais: elas destinam-se a resolver problemas diferentes, e assim:
Por fim, embora isso seja tangencial à sua pergunta, você pode estar interessado em saber que as mônadas têm maneiras incrivelmente poderosas de compor; leia sobre os transformadores de mônada para saber mais. No entanto, essa ainda é uma área ativa de pesquisa porque nós (e por nós, quero dizer pessoas 100000x mais inteligentes que eu) não descobrimos ótimas maneiras de compor mônadas, e parece que algumas mônadas não compõem arbitrariamente.
Agora, para escolher sua pergunta (desculpe, pretendo que isso seja útil e não fazer você se sentir mal): Sinto que há muitas premissas questionáveis nas quais tentarei lançar alguma luz.
Não, isso é
Monad
em Haskell: um tipo parametrizadom a
com uma implementaçãoreturn :: a -> m a
e(>>=) :: m a -> (a -> m b) -> m b
cumprindo as seguintes leis:Existem algumas instâncias do Monad que não são contêineres (
(->) b
) e há alguns contêineres que não são (e não podem ser criadas) instâncias do Monad (Set
devido à restrição de classe de tipo). Portanto, a intuição do "contêiner" é ruim. Veja isso para mais exemplos.Não, não mesmo. Esse exemplo não requer uma mônada. Tudo o que requer é funções com tipos de entrada e saída correspondentes. Aqui está outra maneira de escrevê-lo, enfatizando que é apenas um aplicativo de funções:
Acredito que esse seja um padrão conhecido como "interface fluente" ou "encadeamento de métodos" (mas não tenho certeza).
Os tipos de dados que também são mônadas podem (e quase sempre possuem!) Ter operações não relacionadas a mônadas. Aqui está um exemplo de Haskell composto por três funções nas
[]
quais nada tem a ver com mônadas:[]
"define e controla a semântica da operação" e a "classe contida" não, mas isso não é suficiente para criar uma mônada:Você observou corretamente que há problemas com o uso de hierarquias de classes para modelar as coisas. No entanto, seus exemplos não apresentam nenhuma evidência de que as mônadas possam:
fonte
land(flyAround(takeOff(new Flier<Duck>(duck))))
não funciona (pelo menos em OO) porque essa construção requer quebra de encapsulamento para obter os detalhes do Flier. Ao encadear operações na classe, os detalhes do Flier permanecem ocultos e podem preservar sua semântica. É semelhante à razão pela qual, em Haskell, uma mônada se liga(a, M b)
e não(M a, M b)
para que a mônada não precise expor seu estado à função "ação".unit
torna-se (principalmente) construtor no tipo contido ebind
torna - se (principalmente) uma operação de tempo de compilação implícita (ou seja, ligação antecipada) que vincula as funções de "ação" à classe. Se você possui funções de primeira classe ou uma classe Function <A, Monad <B>>, então umbind
método pode fazer ligação tardia, mas vou abordar esse abuso a seguir. ;)Flier<Thing>
controla a semântica do vôo, pode expor muitos dados e operações que mantêm a semântica do vôo, enquanto a semântica específica da "mônada" é realmente apenas torná-la encadeada e encadeada. Essas preocupações podem não (e as que eu tenho usado não são) da classe dentro da mônada: por exemplo,Resource<String>
possui uma propriedade httpStatus, mas String não.Em idiomas não OO, sim. Nas línguas OO mais tradicionais, eu diria que não.
O problema é que a maioria dos idiomas não possui especialização de tipo, o que significa que você não pode criar
Flier<Squirrel>
eFlier<Bird>
ter implementações diferentes. Você precisa fazer algo comostatic Flier Flier::Create(Squirrel)
(e depois sobrecarregar para cada tipo). O que, por sua vez, significa que você precisa modificar esse tipo sempre que adicionar um novo animal e provavelmente duplicar bastante código para fazê-lo funcionar.Ah, e em poucos idiomas (C # por exemplo)
public class Flier<T> : T {}
é ilegal. Nem vai construir. A maioria, se não todos os programadores de OO esperariamFlier<Bird>
ainda ser aBird
.fonte
Flier<Bird>
seja um contêiner parametrizado, ninguém consideraria umBird
(!?)List<String>
Uma lista e não uma string.Flier
não é apenas um contêiner. Se você o considera apenas um contêiner, por que você pensaria que ele poderia substituir o uso da herança?Animal / Bird / Penguin
geralmente é um mau exemplo, porque traz todos os tipos de semântica. Um exemplo prático é uma mônada REST-ish que estamos usando:Resource<String>.from(uri).get()
Resource
adiciona semântica em cima deString
(ou algum outro tipo), então obviamente não é aString
.