As mônadas são uma alternativa viável (talvez preferível) às hierarquias de herança?

20

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 Birdclasse 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á.)

Roubar
fonte
1
Não sei ao certo como funciona: um esquilo e um pato não voam da mesma maneira - portanto, a "ação da mosca" precisa ser implementada nessas classes ... E o aviador precisa de um método para fazer o esquilo e o pato voar ... Talvez em uma interface comum do Flier ... Opa, espere um minuto ... Perdi alguma coisa?
Assilias
As interfaces são diferentes da herança de classe, porque as interfaces definem recursos enquanto a herança funcional define o comportamento real. Mesmo na "composição sobre herança", a definição de interfaces ainda é um mecanismo importante (por exemplo, polimorfismo). As interfaces não têm os mesmos problemas de herança múltipla. Além disso, cada panfleto pode fornecer (através de uma interface e polimorfismo) propriedades de recursos como "getFlightSpeed ​​()" ou "getManuverability ()" para o contêiner usar.
Rob
3
Você está tentando perguntar se o uso de polimorfismo paramétrico é sempre uma alternativa viável ao subtipo de polimorfismo?
precisa saber é o seguinte
sim, com a dificuldade de adicionar funções composíveis que preservam a semântica. Os tipos de contêineres parametrizados existem há muito tempo, mas eles não me parecem uma resposta completa. É por isso que me pergunto se o padrão de mônada tem um papel mais fundamental a desempenhar.
Rob
6
Não entendo sua descrição de monóides e mônadas. A propriedade chave dos monóides é que ela envolve uma operação binária associativa (pense em adição de ponto flutuante, multiplicação de números inteiros ou concatenação de cadeias). Uma mônada é uma abstração que suporta o seqüenciamento de vários cálculos (possivelmente dependentes) em alguma ordem.
Rufflewind

Respostas:

15

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:

  1. Eles podem ser usados ​​sinergicamente em pelo menos dois sentidos:
    • confira a Typeclassopedia , que abrange muitas das classes de tipo de Haskell. Você notará que existem relacionamentos semelhantes a herança entre eles. Por exemplo, o Mônada é descendente do Aplicativo, que é descendente do Functor.
    • tipos de dados que são instâncias de mônadas podem participar de hierarquias de classes. Lembre-se, o Monad é mais como uma interface - implementá-lo para um determinado tipo informa algumas coisas sobre o tipo de dados, mas não tudo.
  2. Tentar usar um para fazer o outro será difícil e feio.

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.

  1. Uma mônada é um conjunto de funções que tomam um tipo de contêiner como parâmetro e retorna o mesmo tipo de contêiner.

    Não, isso é Monadem Haskell: um tipo parametrizado m acom uma implementação return :: a -> m ae (>>=) :: m a -> (a -> m b) -> m bcumprindo as seguintes leis:

    return a >>= k  ==  k a
    m >>= return  ==  m
    m >>= (\x -> k x >>= h)  ==  (m >>= k) >>= h
    

    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 ( Setdevido à restrição de classe de tipo). Portanto, a intuição do "contêiner" é ruim. Veja isso para mais exemplos.

  2. 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()
    

    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:

    Flier<Duck> m = land(flyAround(takeOff(new Flier<Duck>(duck))));
    

    Acredito que esse seja um padrão conhecido como "interface fluente" ou "encadeamento de métodos" (mas não tenho certeza).

  3. Observe que a mônada define e controla a semântica dessas operações, em vez da classe contida.

    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:

    \predicate -> length . filter predicate . reverse
    
  4. 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:

    • Faça um bom trabalho nessas coisas em que a herança é boa
    • Faça um bom trabalho nessas coisas em que a herança é ruim
Comunidade
fonte
3
Obrigado! Muito para eu processar. Eu não me sinto mal - eu aprecio muito o insight. Eu me sentiria pior carregando as más idéias. :) (Vai para todo o ponto de Stackexchange!)
Rob
1
@RobY De nada! A propósito, se você nunca ouviu falar disso antes, recomendo o LYAH , pois é uma ótima fonte para o aprendizado de mônadas (e Haskell!) Porque possui muitos exemplos (e eu acho que fazer muitos exemplos é a melhor maneira de mônadas).
Há muitas coisas aqui; Eu não quero inundar os comentários, mas alguns comentários: # 2 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".
Rob
# 1, infelizmente, estou tentando desfocar a definição estrita de Monad em Haskell, porque mapear qualquer coisa para Haskell tem um grande problema: composição de funções, incluindo composição em construtores , que você não pode fazer facilmente em uma linguagem de pedestres como Java. Assim, unittorna-se (principalmente) construtor no tipo contido e bindtorna - 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 um bindmétodo pode fazer ligação tardia, mas vou abordar esse abuso a seguir. ;)
Rob
# 3 concorda, e essa é a beleza disso. Se 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.
Rob
1

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?

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>e Flier<Bird>ter implementações diferentes. Você precisa fazer algo como static 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 esperariam Flier<Bird>ainda ser a Bird.

Telastyn
fonte
obrigado pelo comentário. Tenho mais algumas idéias, mas apenas trivialmente, mesmo que Flier<Bird>seja um contêiner parametrizado, ninguém consideraria um Bird(!?) List<String>Uma lista e não uma string.
Rob
@RobY - Fliernã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?
Telastyn
Eu te perdi lá ... meu ponto é que a mônada é um recipiente aprimorado. Animal / Bird / Penguingeralmente é 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() Resourceadiciona semântica em cima de String(ou algum outro tipo), então obviamente não é a String.
Rob
@RobY - mas também não está relacionado à herança.
Telastyn
Exceto que é um tipo diferente de contenção. Posso colocar String em Resource ou abstrair uma classe ResourceString e usar herança. Meu pensamento é que colocar uma classe em um contêiner de encadeamento é uma maneira melhor de abstrair o comportamento do que colocá-lo em uma hierarquia de classes com herança. Portanto, "de maneira alguma relacionada" no sentido de "substituir / evitar" - sim.
Rob