Je voudrais comprendre la différence clé entre le polymorphisme paramétrique tel que le polymorphisme des classes/fonctions génériques dans les langages Java/Scala/C++ et le polymorphisme "ad hoc" dans le système de type Haskell. Je connais le premier type de langues, mais je n'ai jamais travaillé avec le Haskell.
Plus précisément:
Merci d'avance.
Selon le TAPL , §23.2:
Le polymorphisme paramétrique (...), permet à un seul morceau de code d'être typé "de manière générique", en utilisant des variables à la place des types réels, puis instancié avec des types particuliers si nécessaire. Les définitions paramétriques sont uniformes: toutes leurs instances se comportent de la même manière. (...)
Le polymorphisme ad hoc, en revanche, permet à une valeur polymorphe de présenter des comportements différents lorsqu'elle est "vue" à différents types. L'exemple le plus courant de polymorphisme ad hoc est la surcharge, qui associe un symbole de fonction unique à de nombreuses implémentations; le compilateur (ou le système d'exécution, selon que la résolution de surcharge est statique ou dynamique) choisit une implémentation appropriée pour chaque application de la fonction, en fonction des types d'arguments.
Donc, si vous considérez les étapes successives de l'histoire, le fonctionnaire non générique Java (alias pré - J2SE 5. , bef. Sept. 2004) avait un polymorphisme ad hoc - vous pouvez donc - surcharger une méthode - mais pas le polymorphisme paramétrique, donc vous ne pouvez pas écrire une méthode générique . Ensuite, vous pouvez bien sûr faire les deux.
En comparaison, depuis ses tout débuts en 199 , Haskell était paramétriquement polymorphe, ce qui signifie que vous pouviez écrire:
swap :: (A; B) -> (B; A)
swap (x; y) = (y; x)
où A et B sont des variables de type peuvent être instanciées en types tous, sans hypothèses.
Mais il n'y avait aucune construction préexistante donnant le polymorphisme ad-hoc, qui a l'intention de vous permettre d'écrire des fonctions qui s'appliquent à plusieurs, mais pas tout types. Des classes de types ont été implémentées pour atteindre cet objectif.
Ils vous permettent de décrire une classe (quelque chose qui ressemble à une interface Java), donnant la signature de type des fonctions que vous souhaitez implémenter pour votre générique type. Ensuite, vous pouvez enregistrer certains (et, espérons-le, plusieurs) instances correspondant à cette classe. En attendant, vous pouvez écrire une méthode générique telle que:
between :: (Ord a) a -> a -> a -> Bool
between x y z = x ≤ y ^ y ≤ z
où Ord
est la classe qui définit la fonction (_ ≤ _)
. Lorsqu'il est utilisé, (between "abc" "d" "ghi")
Est résolu statiquement pour sélectionner la bonne instance pour les chaînes (plutôt que par exemple des entiers) - exactement au moment où ( Java) la surcharge de méthode le ferait.
Vous pouvez faire quelque chose de similaire dans Java avec caractères génériques bornés . Mais la principale différence entre Haskell et Java sur ce front est que seul Haskell peut faire passer le dictionnaire automatiquement : dans les deux langues, à deux reprises de Ord T
, disons b0
et b1
, vous pouvez construire une fonction f
qui les prend comme arguments et produit l'instance pour le type de paire (b0, b1)
, En utilisant, disons, l'ordre lexicographique. Dites maintenant que vous recevez (("hello", 2), ((3, "hi"), 5))
. Dans Java, vous devez vous souvenir des instances de string
et int
, et passer l'instance correcte (composée de quatre applications de f
!) Afin d'appliquer between
à cet objet. Haskell peut appliquer compositionnalité , et comprendre comment construire l'instance correcte en fonction des instances au sol et du constructeur f
(cela s'étend bien sûr à d'autres constructeurs).
Maintenant, en ce qui concerne inférence de type va (et cela devrait probablement être une question distincte), pour les deux langues c'est incomplet, dans le sens où vous pouvez écrivez toujours un programme non annoté dont le compilateur ne pourra pas déterminer le type.
pour Haskell, c'est parce qu'il a un polymorphisme imprédicatif (a.k.a. de première classe), pour lequel l'inférence de type est indécidable. Notez que sur ce point, Java est limité au polymorphisme de premier ordre (quelque chose sur lequel Scala se développe).
pour Java, c'est parce qu'il prend en charge sous-typage contravariant .
Mais ces langages diffèrent principalement dans la gamme d'instructions de programme auxquelles l'inférence de type s'applique dans la pratique, et dans l'importance donnée à l'exactitude des résultats d'inférence de type.
Pour Haskell, l'inférence s'applique à tous les termes "non hautement polymorphes" et fait un effort sérieux pour renvoyer des résultats sonores basés sur les extensions publiées d'un algorithme bien connu:
A
et B
dans l'exemple ci-dessus) ne peuvent être instanciés qu'avec des types non polymorphes (je simplifie, mais c'est essentiellement le style ML polymorphisme que vous pouvez trouver par exemple dans Ocaml.).Pour Java, l'inférence de type s'applique de façon beaucoup plus limitée de toute façon:
Avant la sortie de Java 5, il n'y avait pas d'inférence de type en Java. Selon la culture du langage Java, le type de chaque variable, méthode et objet alloué dynamiquement doit être explicitement déclaré par le programmeur . Lorsque des génériques (classes et méthodes paramétrées par type) ont été introduits dans Java 5, , le langage a conservé cette exigence pour les variables, méthodes et allocations . Mais l'introduction de méthodes polymorphes (paramétrisées par type) a dicté que (i) le programmeur fournisse les arguments de type de méthode à chaque site d'appel de méthode polymorphe ou (ii) le langage supporte l'inférence d'arguments de type de méthode. Pour éviter de créer une charge de travail supplémentaire pour les programmeurs, les concepteurs de Java 5 ont choisi d'effectuer l'inférence de type pour déterminer les arguments de type pour les appels de méthode polymorphe . ( source , c'est moi qui souligne)
algorithme d'inférence est essentiellement celui de GJ , mais avec n pekludgy l'ajout de caractères génériques après coup (Remarque que je ne suis pas au courant des éventuelles corrections apportées dans J2SE 6.0). La grande différence conceptuelle d'approche est que l'inférence de Java est local, en ce sens que le type inféré d'une expression ne dépend que des contraintes générées par le système de types et des types de ses sous-expressions , mais pas sur le contexte.
Notez que la ligne de parti concernant l'inférence de type incomplète et parfois incorrecte est relativement décontractée. Selon la spécification :
Notez également que l'inférence de type n'affecte en rien la solidité. Si les types déduits sont absurdes, l'invocation produira une erreur de type. L'algorithme d'inférence de type doit être considéré comme une heuristique, conçu pour bien s'exécuter en pratique. S'il ne parvient pas à déduire le résultat souhaité, des paramètres de type explicites peuvent être utilisés à la place.
polymorphisme paramétrique signifie que nous ne nous soucions pas du type, nous implémenterons la fonction de la même manière pour tout type. Par exemple, à Haskell:
length :: [a] -> Int
length [] = 0
length (x:xs) = 1 + length xs
Peu nous importe le type des éléments de la liste, nous nous soucions juste du nombre.
polymorphisme ad hoc (aka surcharge de méthode), cependant, signifie que nous utiliserons une implémentation différente selon le type du paramètre.
Voici un exemple dans Haskell. Disons que nous voulons définir une fonction appelée makeBreakfast
.
Si le paramètre d'entrée est Eggs
, je veux que makeBreakfast
renvoie un message sur la façon de faire des œufs.
Si le paramètre d'entrée est Pancakes
, je veux que makeBreakfast
renvoie un message sur la façon de faire des crêpes.
Nous allons créer une classe de types appelée BreakfastFood
qui implémente la fonction makeBreakfast
. L'implémentation de makeBreakfast
sera différente selon le type d'entrée à makeBreakfast
.
class BreakfastFood food where
makeBreakfast :: food -> String
instance BreakfastFood Eggs where
makeBreakfast = "First crack 'em, then fry 'em"
instance BreakfastFood Toast where
makeBreakfast = "Put bread in the toaster until brown"
Selon les concepts de John Mitchell dans les langages de programmation ,
La principale différence entre le polymorphisme paramétrique et la surcharge (ou polymorphisme ad hoc) est que les fonctions polymorphes paramétriques utilisent un algorithme pour fonctionner sur des arguments de nombreux types différents, tandis que les fonctions surchargées peuvent utiliser un algorithme différent pour chaque type d'argument.
Une discussion complète sur ce que signifient le polymorphisme paramétrique et le polymorphisme ad hoc et dans quelle mesure ils sont disponibles dans Haskell et dans Java est long; cependant, vos questions concrètes peuvent être abordées beaucoup plus simplement:
Comment l'algorithme d'inférence de type, par exemple in Java différence par rapport à l'inférence de type dans Haskell?
Pour autant que je sache, Java ne fait pas d'inférence de type. Donc la différence est que Haskell le fait.
S'il vous plaît, donnez-moi un exemple de la situation où quelque chose peut être écrit en Java/Scala mais ne peut pas être écrit en Haskell (selon les caractéristiques modulaires de ces plateformes aussi), et vice-versa.
Un exemple très simple de quelque chose que Haskell peut faire Java ne peut pas est de définir maxBound :: Bounded a => a
. Je n'en sais pas assez Java pour signaler quelque chose que Haskell ne peut pas faire.