In realtà penso che il polimorfismo del tipo di ritorno sia una delle migliori caratteristiche delle classi di tipo. Dopo averlo usato per un po ', a volte è difficile per me tornare alla modellazione in stile OOP dove non ce l'ho.
Considera la codifica dell'algebra. In Haskell abbiamo una classe di tipo Monoid
(ignorando mconcat
)
class Monoid a where
mempty :: a
mappend :: a -> a -> a
Come potremmo codificarlo come interfaccia in un linguaggio OO? La risposta breve è che non possiamo. Questo perché il tipo di mempty
è (Monoid a) => a
aka, restituisce il polimorfismo del tipo. Avere la capacità di modellare l'algebra è incredibilmente utile IMO. *
Inizi il tuo post con il reclamo su "Trasparenza referenziale". Ciò solleva un punto importante: Haskell è un linguaggio orientato al valore. Quindi espressioni come read 3
non devono essere comprese come cose che calcolano valori, ma possono anche essere comprese come valori. Ciò significa che il vero problema non è il polimorfismo del tipo di ritorno: sono i valori con il tipo polimorfico ( []
e Nothing
). Se la lingua dovrebbe avere questi, allora deve davvero avere tipi di ritorno polimorfici per coerenza.
Dovremmo essere in grado di dire che []
è di tipo forall a. [a]
? Credo di si. Queste funzionalità sono molto utili e rendono la lingua molto più semplice.
Se Haskell avesse il polimorfismo del sottotipo []
potrebbe essere un sottotipo per tutti [a]
. Il problema è che non conosco un modo per codificare che senza che il tipo dell'elenco vuoto sia polimorfico. Considera come verrebbe fatto in Scala (è più breve di farlo nel linguaggio canonico OOP tipicamente statico, Java)
abstract class List[A]
case class Nil[A] extends List[A]
case class Cons[A](h: A. t: List[A]) extends List[A]
Anche qui, Nil()
è un oggetto di tipo Nil[A]
**
Un altro vantaggio del polimorfismo del tipo di ritorno è che rende l'incorporamento di Curry-Howard molto più semplice.
Considera i seguenti teoremi logici:
t1 = forall P. forall Q. P -> P or Q
t2 = forall P. forall Q. P -> Q or P
Possiamo banalmente catturarli come teoremi in Haskell:
data Either a b = Left a | Right b
t1 :: a -> Either a b
t1 = Left
t2 :: a -> Either b a
t2 = Right
Per riassumere: mi piace il polimorfismo del tipo di ritorno e penso che rompa la trasparenza referenziale se hai una nozione limitata di valori (anche se questo è meno convincente nel caso della classe di tipo ad hoc). D'altra parte, trovo i tuoi punti su MR e digito il default in modo convincente.
*. Nei commenti che ysdx sottolinea questo non è strettamente vero: potremmo implementare nuovamente le classi di tipi modellando l'algebra come un altro tipo. Come il java:
abstract class Monoid<M>{
abstract M empty();
abstract M append(M m1, M m2);
}
Devi quindi passare oggetti di questo tipo con te. Scala ha una nozione di parametri impliciti che evita alcuni, ma nella mia esperienza non tutti, il sovraccarico di gestire esplicitamente queste cose. Mettere i tuoi metodi di utilità (metodi di fabbrica, metodi binari, ecc.) Su un tipo separato associato a F risulta essere un modo incredibilmente bello di gestire le cose in un linguaggio OO che supporta i generici. Detto questo, non sono sicuro che avrei criticato questo modello se non avessi avuto esperienza nel modellare le cose con le macchine da scrivere e non sono sicuro che lo faranno le altre persone.
Ha anche dei limiti, non c'è modo di ottenere un oggetto che implementi la classe per un tipo arbitrario. Devi passare esplicitamente i valori, usare qualcosa come gli impliciti di Scala o usare una qualche forma di tecnologia di iniezione di dipendenza. La vita diventa brutta. D'altra parte, è bello poter avere più implementazioni per lo stesso tipo. Qualcosa può essere un monoide in più modi. Inoltre, portare in giro queste strutture separatamente ha un aspetto IMO più matematicamente moderno, costruttivo. Quindi, anche se in genere preferisco ancora il modo di Haskell di farlo, probabilmente ho esagerato con il mio caso.
Le macchine da scrivere con polimorfismo del tipo di ritorno rendono questo tipo di cose facile da gestire. Questo non significa che sia il modo migliore per farlo.
**. Jörg W Mittag sottolinea che non è proprio il modo canonico di farlo a Scala. Invece, seguiremmo la libreria standard con qualcosa di più simile a:
abstract class List[+A] ...
case class Cons[A](head: A, tail: List[A]) extends List[A] ...
case object Nil extends List[Nothing] ...
Questo sfrutta il supporto di Scala per i tipi inferiori, nonché i parametri di tipo covariante. Quindi, Nil
è di tipo Nil
no Nil[A]
. A questo punto siamo abbastanza lontani da Haskell, ma è interessante notare come Haskell rappresenti il tipo di fondo
undefined :: forall a. a
Cioè, non è il sottotipo di tutti i tipi, è polimorficamente (sp) un membro di tutti i tipi.
Ancora più polimorfismo del tipo di ritorno.