Na verdade, acho que o polimorfismo de tipo de retorno é um dos melhores recursos das classes de tipo. Depois de usá-lo por um tempo, às vezes é difícil voltar à modelagem de estilo OOP, onde não a tenho.
Considere a codificação da álgebra. Em Haskell, temos uma classe de tipo Monoid
(ignorando mconcat
)
class Monoid a where
mempty :: a
mappend :: a -> a -> a
Como podemos codificar isso como uma interface em uma linguagem OO? A resposta curta é que não podemos. Isso mempty
ocorre porque o tipo de é (Monoid a) => a
conhecido como polimorfismo do tipo de retorno. Ter a capacidade de modelar álgebra é IMO incrivelmente útil. *
Você inicia sua postagem com a reclamação sobre "Transparência referencial". Isso levanta um ponto importante: Haskell é uma linguagem orientada a valores. Portanto, expressões como read 3
não precisam ser entendidas como coisas que computam valores, elas também podem ser entendidas como valores. O que isso significa é que o problema real não é o polimorfismo do tipo de retorno: são valores com o tipo polimórfico ( []
e Nothing
). Se o idioma os possuir, é necessário que haja tipos de retorno polimórficos para garantir a consistência.
Deveríamos ser capazes de dizer que []
é do tipo forall a. [a]
? Acho que sim. Esses recursos são muito úteis e tornam o idioma muito mais simples.
Se Haskell tivesse um subtipo de polimorfismo, []
poderia ser um subtipo para todos [a]
. O problema é que não conheço uma maneira de codificar que, sem que o tipo da lista vazia seja polimórfico. Considere como isso seria feito no Scala (é mais curto do que na linguagem OOP canônica estaticamente tipada, Java)
abstract class List[A]
case class Nil[A] extends List[A]
case class Cons[A](h: A. t: List[A]) extends List[A]
Mesmo aqui, Nil()
é um objeto do tipo Nil[A]
**
Outra vantagem do polimorfismo do tipo retorno é que ele torna a incorporação de Curry-Howard muito mais simples.
Considere os seguintes teoremas lógicos:
t1 = forall P. forall Q. P -> P or Q
t2 = forall P. forall Q. P -> Q or P
Podemos capturá-los trivialmente como teoremas em Haskell:
data Either a b = Left a | Right b
t1 :: a -> Either a b
t1 = Left
t2 :: a -> Either b a
t2 = Right
Resumindo: eu gosto do polimorfismo de tipo de retorno e só acho que quebra a transparência referencial se você tiver uma noção limitada de valores (embora isso seja menos atraente no caso da classe de tipo ad hoc). Por outro lado, acho seus pontos sobre o MR e digito convincente por padrão.
* Nos comentários, o ysdx ressalta que isso não é totalmente verdade: poderíamos reimplementar as classes de tipos modelando a álgebra como outro tipo. Como o java:
abstract class Monoid<M>{
abstract M empty();
abstract M append(M m1, M m2);
}
Você precisa passar objetos desse tipo com você. Scala tem uma noção de parâmetros implícitos que evita alguns, mas na minha experiência não todos, a sobrecarga de gerenciar explicitamente essas coisas. Colocar seus métodos utilitários (métodos de fábrica, métodos binários, etc.) em um tipo separado de limite F é uma maneira incrivelmente agradável de gerenciar as coisas em uma linguagem OO que oferece suporte a genéricos. Dito isto, não tenho certeza de que teria adotado esse padrão se não tivesse experiência em modelar coisas com classes tipográficas e não tenho certeza de que outras pessoas o farão.
Ele também possui limitações, já que não há como obter um objeto que implemente a classe de tipo para um tipo arbitrário. Você deve passar os valores explicitamente, usar algo como os implícitos de Scala ou usar alguma forma de tecnologia de injeção de dependência. A vida fica feia. Por outro lado, é bom que você possa ter várias implementações para o mesmo tipo. Algo pode ser um monóide de várias maneiras. Além disso, transportar essas estruturas separadamente oferece à IMO uma sensação mais matematicamente moderna e construtiva. Portanto, embora eu ainda prefira a maneira de Haskell de fazer isso, provavelmente exagerei meu caso.
As classes com polimorfismo do tipo retorno tornam esse tipo de coisa fácil de manusear. Isso não significa que é a melhor maneira de fazê-lo.
** Jörg W Mittag ressalta que essa não é realmente a maneira canônica de fazer isso no Scala. Em vez disso, seguiríamos a biblioteca padrão com algo mais como:
abstract class List[+A] ...
case class Cons[A](head: A, tail: List[A]) extends List[A] ...
case object Nil extends List[Nothing] ...
Isso tira proveito do suporte da Scala para tipos de fundo, bem como paramaters de tipo covariante. Então, Nil
é do tipo Nil
não Nil[A]
. Neste ponto, estamos bem longe de Haskell, mas é interessante observar como Haskell representa o tipo inferior
undefined :: forall a. a
Ou seja, não é o subtipo de todos os tipos, é polimorficamente (sp) um membro de todos os tipos.
Ainda mais polimorfismo de tipo de retorno.