In realtà penso che il polimorfismo del tipo restituito sia una delle migliori caratteristiche delle classi di tipi. 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 caratteri Monoid
(ignorando mconcat
)
class Monoid a where
mempty :: a
mappend :: a -> a -> a
Come potremmo codificarlo come un'interfaccia in un linguaggio OO? La risposta breve è che non possiamo. Questo perché il tipo di mempty
è (Monoid a) => a
aka, restituisce il tipo di polimorfismo. Avere la capacità di modellare l'algebra è IMO incredibilmente utile. *
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 intese come cose che calcolano valori, possono anche essere intese come valori. Ciò significa che il vero problema non è il tipo di polimorfismo di ritorno: si tratta di valori con 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 aveva il polimorfismo del sottotipo []
potrebbe essere un sottotipo per tutto [a]
. Il problema è che non conosco un modo di codificare quello senza che il tipo della lista vuota sia polimorfico. Considera come dovrebbe essere fatto in Scala (è più breve di farlo nel linguaggio OOP, Java, staticamente tipizzato staticamente)
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 molto più semplice l'incorporamento di Curry-Howard.
Considera i seguenti teoremi logici:
t1 = forall P. forall Q. P -> P or Q
t2 = forall P. forall Q. P -> Q or P
Possiamo catturarli banalmente 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 restituito, e penso solo che infrange la trasparenza referenziale se si dispone di una nozione limitata di valori (sebbene ciò sia meno convincente nel caso della classe di tipo ad hoc). D'altra parte, trovo i tuoi punti di vista su MR e digita il valore predefinito.
*. Nei commenti ysdx indica questo non è strettamente vero: potremmo ri-implementare classi di tipi modellando l'algebra come un altro tipo. Come la java:
abstract class Monoid<M>{
abstract M empty();
abstract M append(M m1, M m2);
}
Quindi devi passare oggetti di questo tipo con te. Scala ha una nozione di parametri impliciti che evita alcuni, ma nella mia esperienza non tutti, del sovraccarico di gestione esplicita di queste cose. Mettendo i tuoi metodi di utilità (metodi di fabbrica, metodi binari, ecc.) Su un tipo F separato, si rivela un modo incredibilmente bello di gestire le cose in un linguaggio OO che supporta i generici. Detto questo, non sono sicuro che avrei annullato questo schema se non avessi avuto esperienza nel modellare le cose con le classifiche, e non sono sicuro che gli altri lo faranno.
Ha anche delle limitazioni, fuori dalla scatola non c'è modo di ottenere un oggetto che implementa la classe di caratteri per un tipo arbitrario. Bisogna o passare i valori in modo esplicito, usare qualcosa come impliciti di Scala, o usare una qualche forma di tecnologia di iniezione delle dipendenze. La vita diventa brutta D'altra parte, è bello poter avere più implementazioni per lo stesso tipo. Qualcosa può essere un Monoid in più modi. Inoltre, portare in giro queste strutture separatamente ha IMO un aspetto più matematicamente moderno, costruttivo. Quindi, anche se generalmente preferisco il modo Haskell di farlo, probabilmente ho sopravvalutato il mio caso.
I tipi di classe con polimorfismo di tipo restituito rendono questo tipo di cose facile da gestire. Ciò non significa che sia il modo migliore per farlo.
**. Jörg W Mittag sottolinea questo non è davvero il modo canonico di farlo in 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 di fondo, così come i parametri di tipo covariante. Quindi, Nil
è di tipo Nil
non 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.