Qu'est-ce qui rend le système de types Haskell si vénéré (par exemple, Java)?

204

Je commence à apprendre Haskell . Je suis très novice dans ce domaine et je suis en train de lire quelques livres en ligne pour mieux comprendre ses concepts de base.

L’un des "mèmes" dont les habitués ont souvent parlé, est l’ensemble "s’il compile, cela fonctionnera *" - ce qui, à mon avis, est lié à la force du système de caractères.

J'essaie de comprendre pourquoi exactement Haskell est meilleur que les autres langages statiquement typés à cet égard.

En d'autres termes, je suppose qu'en Java, vous pourriez faire quelque chose de odieux, comme enterrer ArrayList<String>()pour contenir quelque chose qui devrait vraiment l'être ArrayList<Animal>(). La chose odieuse ici est que votre stringcontient elephant, giraffe, etc., et si quelqu'un le met Mercedes- votre compilateur ne vous aidera pas.

Si je l'ai fait faire ArrayList<Animal>()ensuite, à un moment donné plus tard dans le temps, si je décide de mon programme n'est pas vraiment sur les animaux, il est sur les véhicules, alors je peux changer, par exemple, une fonction qui produit ArrayList<Animal>pour produire ArrayList<Vehicle>et mon IDE dois me dire partout là - bas est une pause de compilation.

Mon hypothèse est que c'est ce que les gens entendent par système de type fort , mais il ne m'est pas évident de savoir pourquoi Haskell est meilleur. En d'autres termes, vous pouvez écrire du bon ou du mauvais Java, je suppose que vous pouvez faire la même chose en Haskell (c'est-à-dire insérer des éléments dans des chaînes / entêtes qui devraient être des types de données de première classe).

Je pense que je manque quelque chose d'important / basique.
Je serais très heureux de voir l'erreur de mes manières!

phatmanace
la source
31
Je laisserai les gens plus au courant que je n'écrirai de vraies réponses, mais l'essentiel est le suivant: les langages statiques, comme C #, ont un système de types qui essaie de vous aider à écrire du code défendable ; Des systèmes de type comme la tentative de Haskell de vous aider à écrire du code correct (c'est-à-dire prouvable). Le principe de base à l’œuvre est de déplacer des éléments pouvant être contrôlés au stade de la compilation; Haskell vérifie plus de choses au moment de la compilation.
Robert Harvey
8
Je ne connais pas grand chose à Haskell, mais je peux parler de Java. Bien qu'il semble fortement typé, il vous permet néanmoins de faire des choses "odieuses" comme vous l'avez dit. Pour presque toutes les garanties données par Java concernant son système de types, il existe un moyen de les contourner.
12
Je ne sais pas pourquoi toutes les réponses Maybene mentionnent que vers la fin. Si je devais choisir juste une chose que les langues plus populaires devraient emprunter à Haskell, ce serait ça. C'est une idée très simple (donc pas très intéressante d'un point de vue théorique), mais cela seul rendrait notre travail beaucoup plus facile.
Paul
1
Il y aura d'excellentes réponses ici, mais dans le but d'aider, étudiez les signatures de type. Ils permettent aux humains et aux programmes de raisonner sur les programmes de manière à illustrer la façon dont Java se situe à mi-chemin entre: les types.
Michael Easter
6
Par souci d'équité, je dois souligner que "le tout, s'il compile, tout fonctionnera", est un slogan et non une déclaration de fait littérale. Oui, nous, programmeurs Haskell, savons que passer le vérificateur de type donne une bonne chance d’être correct, pour quelques notions limitées d’exactitude, mais ce n’est certainement pas une affirmation littéralement et universellement "vraie"!
Tom Ellis

Réponses:

230

Voici une liste non ordonnée de fonctionnalités de système de types disponibles dans Haskell et indisponibles ou moins agréables en Java (à ma connaissance, qui est certes faible par rapport à Java)

  • La sécurité . Les types de Haskell ont de très bonnes propriétés de "sécurité de type". C'est assez spécifique, mais cela signifie essentiellement que les valeurs d'un type ne peuvent pas se transformer librement en un autre type. Ceci est parfois en contradiction avec la mutabilité (voir la restriction de valeur d'OCaml )
  • Types de données algébriques . Les types en Haskell ont essentiellement la même structure que les mathématiques au lycée. C’est scandaleusement simple et cohérent et pourtant, aussi puissant que vous le voudriez. C'est simplement une excellente base pour un système de types.
    • Programmation générique de type de données . Ce n'est pas la même chose que les types génériques (voir la généralisation ). En raison de la simplicité de la structure de type décrite précédemment, il est relativement facile d'écrire du code fonctionnant de manière générique sur cette structure. Je parlerai plus tard de la manière dont Equn compilateur Haskell pourrait dériver automatiquement quelque chose comme la fonctionnalité pour un type défini par l'utilisateur. Pour ce faire, il s’agit essentiellement de la structure simple et commune sous-jacente à tout type défini par l’utilisateur et de la faire correspondre à des valeurs - une forme très naturelle d’égalité structurelle.
  • Types mutuellement récursifs . Ceci est juste un élément essentiel de l’écriture de types non triviaux.
    • Types imbriqués . Cela vous permet de définir des types récursifs sur des variables qui récurse à différents types. Par exemple, un type d'arbres équilibrés est data Bt a = Here a | There (Bt (a, a)). Réfléchissez bien aux valeurs valides de Bt aet notez comment ce type fonctionne. C'est délicat!
  • Généralisation . C’est presque trop bête de ne pas avoir dans un système de type (ahem, te regarder, vas-y). Il est important d'avoir des notions de type variables et la possibilité de parler de code indépendamment du choix de cette variable. Hindley Milner est un système de types dérivé du système F. Le système de types de Haskell est un développement du typage HM et le système F est essentiellement le foyer de la généralisation. Ce que je veux dire, c'est que Haskell a une très bonne histoire de généralisation.
  • Types abstraits . L'histoire de Haskell n'est pas grande mais pas non plus inexistante. Il est possible d'écrire des types qui ont une interface publique mais une implémentation privée. Cela nous permet à la fois d’admettre ultérieurement les modifications apportées au code d’implémentation et, surtout puisque c’est la base de toutes les opérations en Haskell, d’écrire des types "magiques" qui ont des interfaces bien définies, telles que IO. Pour être honnête, Java a probablement une histoire de type abstrait plus sympathique, mais je ne pense pas que, jusqu'à ce que Interfaces devienne plus populaire, cela soit réellement vrai.
  • Parametricity . Les valeurs Haskell n'ont pas d' opérations universelles. Java viole cela avec des choses comme l’égalité de référence et le hachage et encore plus flagrante avec la coercition. Cela signifie que vous obtenez des théorèmes libres sur les types qui vous permettent de connaître la signification d'une opération ou d'une valeur à un degré remarquable entièrement de son type - certains types sont tels qu'il ne peut y avoir qu'un très petit nombre d'habitants.
  • Les types plus gentils affichent tout le type lors de l’encodage de choses plus délicates. Functor / Applicatif / Monad, Foldable / Traversable, tout le mtlsystème de typage d'effets, points de fixation de foncteur généralisés. La liste se rallonge de plus en plus. Il y a beaucoup de choses qui sont mieux exprimées dans les types les plus élevés et relativement peu de systèmes de types permettent même à l'utilisateur de parler de ces choses.
  • Classes de type . Si vous considérez les systèmes de types comme des logiques - ce qui est utile -, on vous demande souvent de prouver des choses. Dans de nombreux cas, il s’agit essentiellement d’un bruit de ligne: il ne peut y avoir qu’une seule bonne réponse et c’est une perte de temps et d’efforts pour le programmeur de le dire. Les classes de types permettent à Haskell de générer les preuves pour vous. Plus concrètement, cela vous permet de résoudre des "systèmes d'équation de type" simples tels que "À quel type voulons-nous allier les (+)choses ensemble? Oh Integer, d'accord! Insérons le bon code maintenant!". Sur des systèmes plus complexes, vous pouvez établir des contraintes plus intéressantes.
    • Calcul de contraintes . Les contraintes en Haskell - qui sont le mécanisme permettant d’atteindre le système de prologue de type class - sont structurellement typées. Cela donne une forme très simple de relation de sous-typage qui vous permet d’assembler des contraintes complexes à partir de contraintes plus simples. Toute la mtlbibliothèque est basée sur cette idée.
    • Dériver . Afin de gérer la canonicité du système de classe de types, il est nécessaire d'écrire beaucoup de code souvent trivial pour décrire les contraintes que les types définis par l'utilisateur doivent instancier. Faites à la structure très normale des types Haskell, il est souvent possible de demander au compilateur de faire cette passe-partout pour vous.
    • Type de classe prolog . Le résolveur de classes de types Haskell - le système qui génère ces "preuves" dont j'ai parlé plus tôt - est essentiellement une forme estropiée de Prolog avec des propriétés sémantiques plus agréables. Cela signifie que vous pouvez encoder des choses très poilues dans le type prolog et vous attendre à ce qu’elles soient gérées au moment de la compilation. Un bon exemple pourrait être la recherche de la preuve que deux listes hétérogènes sont équivalentes si vous oubliez l'ordre, ce sont des "ensembles" hétérogènes équivalents.
    • Classes de types multi-paramètres et dépendances fonctionnelles . Ce ne sont que des améliorations massivement utiles pour baser la classe prolog de la classe. Si vous connaissez Prolog, vous pouvez imaginer combien le pouvoir d’expression augmente lorsque vous pouvez écrire des prédicats de plusieurs variables.
  • Très bonne conclusion . Les langues basées sur les systèmes de type Hindley Milner ont une assez bonne inférence. HM lui-même a une inférence complète, ce qui signifie que vous n'avez jamais besoin d'écrire une variable de type. Haskell 98, la forme la plus simple de Haskell, l'utilise déjà dans de très rares circonstances. De manière générale, Haskell moderne a été une expérience visant à réduire lentement l'espace d'inférence complète tout en ajoutant plus de puissance à HM et en détectant les plaintes des utilisateurs. Les gens se plaignent très rarement - la conclusion de Haskell est plutôt bonne.
  • Très, très, très très peu de sous-typage . J'ai mentionné précédemment que le système de contraintes de prolog typeclass a une notion de sous-typage structurel. C’est la seule forme de sous-typage en Haskell . Le sous-typage est terrible pour le raisonnement et l'inférence. Cela complique considérablement chacun de ces problèmes (un système d'inégalités au lieu d'un système d'égalités). Il est également très facile de mal comprendre (sous-classer est-il identique au sous-typage? Bien sûr que non! Mais très souvent, les gens confondent cela et de nombreuses langues contribuent à cette confusion! Comment en sommes-nous arrivés là? Je suppose que personne n’examine jamais le LSP.)
    • Note récemment (début 2017), Steven Dolan a publié sa thèse sur MLsub , une variante de l'inférence de type ML et Hindley-Milner qui présente une très belle histoire de sous-typage ( voir aussi ). Cela n'empêche pas ce que j'ai écrit ci-dessus - la plupart des systèmes de sous-typage sont cassés et ont une mauvaise inférence - mais laisse à penser que nous venons peut-être de découvrir des moyens prometteurs de combiner parfaitement l'inférence et le sous-typage. Pour être tout à fait clair, les notions de sous-typage de Java ne peuvent en aucun cas tirer parti des algorithmes et des systèmes de Dolan. Cela nécessite de repenser ce que signifie le sous-typage.
  • Types de rang supérieur . J'ai parlé de généralisation plus tôt, mais plus que la simple généralisation , il est utile de pouvoir parler de types qui ont des variables généralisées en leur sein . Par exemple, un mappage entre des structures d'ordre supérieur qui est inconscient (voir la paramétrie ) avec ce que ces structures "contiennent" a un type similaire à (forall a. f a -> g a). Dans HM droit , vous pouvez écrire une fonction à ce type, mais avec des types de rang supérieur vous demande une telle fonction comme un argument de façon: mapFree :: (forall a . f a -> g a) -> Free f -> Free g. Notez que la avariable est liée uniquement dans l'argument. Cela signifie que le défineur de la fonction mapFreedoit décider de ce qui aest instancié au moment où il l’utilise, et non de l’utilisateur mapFree.
  • Types existentiels . Alors que les types de rang supérieur nous permettent de parler de la quantification universelle, les types existentiels nous permettent de parler de la quantification existentielle: l'idée qu'il existe simplement existe un certain type inconnu satisfaisant certaines équations. Cela finit par être utile et continuer plus longtemps, cela prendrait longtemps.
  • Tapez les familles . Parfois, les mécanismes de classes de types ne sont pas pratiques car nous ne pensons pas toujours à Prolog. Les familles de types permettent d’écrire directement des relations fonctionnelles entre types.
    • Familles de type fermées . Les familles de types sont par défaut ouvertes, ce qui est agaçant car cela signifie que même si vous pouvez les étendre à tout moment, vous ne pouvez les "inverser" avec aucun espoir de succès. C'est parce que vous ne pouvez pas prouver l' injectivité , mais vous pouvez le faire avec des familles de type fermé.
  • Types indexés et promotion de type . Je commence à être vraiment exotique à ce stade, mais ils ont une utilisation pratique de temps en temps. Si vous souhaitez écrire un type de poignées ouvertes ou fermées, vous pouvez le faire très bien. Remarquez dans l'extrait suivant qu'il States'agit d'un type algébrique très simple dont les valeurs ont également été promues au niveau du type. Ensuite, par la suite, nous pouvons parler de constructeurs de types tels Handleque prendre des arguments de types spécifiques tels que State. C'est déroutant de comprendre tous les détails, mais aussi de manière très juste.

    data State = Open | Closed
    
    data Handle :: State -> * -> * where
      OpenHandle :: {- something -} -> Handle Open a
      ClosedHandle :: {- something -} -> Handle Closed a
  • Des représentations de type d'exécution qui fonctionnent . Java est réputé pour avoir effacé le texte et avoir cette fonctionnalité sous la pluie lors des défilés. La suppression de type est la bonne façon de procéder, cependant, comme si vous aviez une fonction, getRepr :: a -> TypeReprvous violeriez au moins la paramétrie. Ce qui est pire, c’est que s’il s’agit d’une fonction générée par l’utilisateur qui est utilisée pour déclencher des coercitions dangereuses au moment de l’exécution ... alors vous avez un grave problème de sécurité . Le Typeablesystème de Haskell permet la création d'un coffre-fort coerce :: (Typeable a, Typeable b) => a -> Maybe b. Ce système repose sur Typeablesa mise en oeuvre dans le compilateur (et non dans le pays utilisateur) et ne pourrait pas non plus recevoir une sémantique aussi agréable sans le mécanisme de type class de Haskell et les lois qu'il est garanti de respecter.

Cependant, la valeur du système de types de Haskell est également liée à la manière dont les types décrivent le langage. Voici quelques fonctionnalités de Haskell qui génèrent de la valeur via le système de types.

  • Pureté . Haskell ne permet aucun effet secondaire pour une définition très large, très large, de "effet secondaire". Cela vous oblige à entrer plus d'informations dans les types car ceux-ci régissent les entrées et les sorties et sans effets secondaires, tout doit être pris en compte dans les entrées et les sorties.
    • IO . Par la suite, Haskell avait besoin d’un moyen de parler des effets secondaires - puisqu'un programme réel devait en inclure -, une combinaison de classes de types, de types à type plus élevé et de types abstraits donnait naissance à la notion d’utilisation d’un type super spécial appelé IO apour représenter calculs ayant des effets secondaires qui donnent des valeurs de type a. C'est la base d'un très bon système d'effets intégré dans un langage pur.
  • Manque denull . Tout le monde sait que nullc'est l'erreur d'un milliard de dollars des langages de programmation modernes. Les types algébriques, en particulier la possibilité d'ajouter simplement un état "n'existe pas" à vos types en transformant un type Adans le type Maybe A, atténuent complètement le problème de null.
  • Récursion polymorphe . Cela vous permet de définir des fonctions récursives qui généralisent des variables de type malgré leur utilisation à différents types dans chaque appel récursif dans leur propre généralisation. Il est difficile de parler de cela, mais c'est particulièrement utile pour parler de types imbriqués. Revoyez le Bt agenre d'avant et d' essayer d'écrire une fonction pour calculer sa taille: size :: Bt a -> Int. Ça va ressembler un peu size (Here a) = 1et size (There bt) = 2 * size bt. Sur le plan opérationnel, cela n’est pas trop complexe, mais notez que l’appel récursif de sizela dernière équation se produit sous un type différent , alors que la définition globale a un beau type généralisé size :: Bt a -> Int. Notez qu'il s'agit d'une fonctionnalité qui rompt l'inférence totale, mais si vous fournissez une signature de type, Haskell l'autorise.

Je pourrais continuer, mais cette liste devrait vous permettre de commencer et ensuite.

J. Abrahamson
la source
7
Null n'était pas une "erreur" d'un milliard de dollars. Il existe des cas où il n'est pas possible de vérifier statiquement que le pointeur ne sera pas déréférencé avant qu'il ne puisse exister de signification ; dans ce cas, il est souvent préférable de tenter une interruption de déréférence que d'exiger que le pointeur identifie initialement un objet sans signification. Je pense que la plus grande erreur liée à null était d'avoir des implémentations qui, étant donné , vont piéger , mais ne char *p = NULL;*p=1234char *q = p+5678;*q = 1234;
piégeront
37
Tony Hoare vient juste de citer: fr.wikipedia.org/wiki/Tony_Hoare#Apologies_and_retractions . Bien que je sois sûr qu'il y a des moments où il nullest nécessaire en arithmétique de pointeur, j'interprète plutôt cela pour dire que l'arithmétique de pointeur est un mauvais endroit pour héberger la sémantique de votre langue, même si null n'est pas une erreur.
J. Abrahamson
18
@supercat, vous pouvez en effet écrire une langue sans null. Permettre ou non, c'est un choix.
Paul Draper
6
@supercat - Ce problème existe également en Haskell, mais sous une forme différente. Haskell est normalement paresseux et immuable, et vous permet donc d'écrire p = undefinedaussi longtemps qu'il pn'est pas évalué. Plus utilement, vous pouvez mettre undefinedune sorte de référence mutable, encore une fois tant que vous ne l'évaluez pas. Le défi le plus sérieux concerne les calculs paresseux qui ne peuvent pas se terminer, ce qui est bien sûr indécidable. La principale différence est que ce sont tous des erreurs de programmation sans ambiguïté et qu'ils ne sont jamais utilisés pour exprimer une logique ordinaire.
Christian Conkle
6
@supercat Haskell manque totalement de sémantique de référence (c'est la notion de transparence référentielle qui implique que tout soit préservé en remplaçant les références par leurs référents). Ainsi, je pense que votre question est mal posée.
J. Abrahamson
78
  • Inférence de type complète. Vous pouvez réellement utiliser des types complexes de manière omniprésente sans avoir l’impression de «merde, tout ce que je fais est d’écrire des signatures de type».
  • Les types sont entièrement algébriques , ce qui permet d’exprimer facilement des idées complexes.
  • Haskell a des classes de types, qui sont en quelque sorte des interfaces, sauf que vous n'avez pas à mettre toutes les implémentations d'un même type au même endroit. Vous pouvez créer des implémentations de vos propres classes de types pour les types tiers existants, sans avoir besoin d'accéder à leur source.
  • Les fonctions d'ordre supérieur et récursives ont tendance à affecter davantage de fonctionnalités au vérificateur de types. Prenez le filtre , par exemple. Dans un langage impératif, vous pouvez écrire une forboucle pour implémenter la même fonctionnalité, mais vous n'aurez pas les mêmes garanties de type statique, car une forboucle n'a pas de concept de type de retour.
  • L'absence de sous-types simplifie grandement le polymorphisme paramétrique.
  • Les types (types de types) de type supérieur sont relativement faciles à spécifier et à utiliser dans Haskell, ce qui vous permet de créer des abstractions autour de types complètement insondables en Java.
Karl Bielefeldt
la source
7
Bonne réponse - pourriez-vous me donner un exemple simple d'un type plus gentil, pensez que cela m'aiderait à comprendre pourquoi il est impossible de le faire en Java.
Phatmanace
3
Il y a quelques bons exemples ici .
Karl Bielefeldt
3
La correspondance des motifs est également très importante, cela signifie que vous pouvez utiliser le type d'objet pour prendre des décisions très facilement.
Benjamin Gruenbaum
2
@ BenjaminGruenbaum Je ne pense pas que j'appellerais cela une fonctionnalité de système de types.
Doval
3
Bien que les TDA et les HKT fassent définitivement partie de la réponse, je doute que quiconque posera cette question saura pourquoi ils sont utiles, je suggère que les deux sections doivent être développées pour expliquer ceci
jk.
62
a :: Integer
b :: Maybe Integer
c :: IO Integer
d :: Either String Integer

En Haskell: un entier, un entier qui peut être nul, un entier dont la valeur vient du monde extérieur et un entier qui pourrait être une chaîne, sont tous des types distincts - et le compilateur l'imposera . Vous ne pouvez pas compiler un programme Haskell qui ne respecte pas ces distinctions.

(Vous pouvez cependant omettre les déclarations de type. Dans la plupart des cas, le compilateur peut déterminer le type le plus général pour vos variables, ce qui aboutira à une compilation réussie. N'est-ce pas intéressant?)

WolfeFan
la source
11
+1 alors que cette réponse n’est pas complète, je pense qu’il est bien mieux présenté au niveau de la question
jk.
1
+1 Bien qu'il serait utile d'expliquer que d'autres langages ont également Maybe(par exemple, Java Optionalet Scala Option), mais dans ces langages c'est une solution à moitié cuite, puisque vous pouvez toujours assigner nullune variable de ce type et faire exploser votre programme à l'exécution. temps. Cela ne peut pas arriver avec Haskell [1], car il n'y a pas de valeur null , vous ne pouvez donc pas tricher. ([1]: en fait, vous pouvez générer une erreur similaire à une exception NullPointerException en utilisant des fonctions partielles telles que fromJustlorsque vous en avez une Nothing, mais ces fonctions sont probablement mal vues).
Andres F.
2
"un entier dont la valeur vient du monde extérieur" - ne serait-il pas IO Integerplus proche de "sous-programme qui, lorsqu'il est exécuté, donne un entier"? Comme a) dans main = c >> cla valeur renvoyée par le premier cpeut être différent alors par le second calors que aaura la même valeur quelle que soit sa position (tant que nous sommes dans la même portée) b) il existe des types qui désignent des valeurs du monde extérieur pour imposer sa désinfection (c.-à-d. ne pas les mettre directement mais vérifier d'abord si l'entrée de l'utilisateur est correcte / non malveillante).
Maciej Piechotka
4
Maciej, ce serait plus précis. Je cherchais la simplicité.
WolfeFan
30

Beaucoup de gens ont mentionné les bonnes choses à propos de Haskell. Mais en réponse à votre question spécifique "Pourquoi le système de types rend-il les programmes plus corrects?", Je suppose que la réponse est "polymorphisme paramétrique".

Considérez la fonction Haskell suivante:

foobar :: x -> y -> y

Il n'y a littéralement qu'un seul moyen possible d'implémenter cette fonction. Juste par la signature de type, je peux dire précisément ce que cette fonction fait, car il n’ya qu’une chose possible à faire. [OK, pas tout à fait, mais presque!]

Arrêtez-vous et réfléchissez-y un instant. C'est vraiment une grosse affaire! Cela signifie que si j'écris une fonction avec cette signature, il est en fait impossible pour la fonction de faire autre chose que ce que j'avais l'intention de faire. (La signature de type elle-même peut toujours être fausse. Aucun langage de programmation n'empêchera jamais tous les bogues.)

Considérons cette fonction:

fubar :: Int -> (x -> y) -> y

Cette fonction est impossible . Vous ne pouvez littéralement pas implémenter cette fonction. Je peux dire que juste de la signature de type.

Comme vous pouvez le constater, une signature de type Haskell vous en dit long!


Comparez à C #. (Désolé, mon Java est un peu rouillé.)

public static TY foobar<TX, TY>(TX in1, TY in2)

Cette méthode peut faire plusieurs choses:

  • Retour in2comme résultat.
  • Boucle pour toujours et ne retourne jamais rien.
  • Lance une exception et ne retourne jamais rien.

En fait, Haskell propose également ces trois options. Mais C # vous offre également des options supplémentaires:

  • Retourne null. (Haskell n'a pas null.)
  • Modifier in2avant de le retourner. (Haskell n'a pas de modification en place.)
  • Utilisez la réflexion. (Haskell n'a pas de réflexion.)
  • Effectuez plusieurs actions d'E / S avant de renvoyer un résultat. (Haskell ne vous laissera pas effectuer des E / S sauf si vous déclarez que vous effectuez des E / S ici.)

La réflexion est un marteau particulièrement gros. en utilisant la réflexion, je peux construire un nouvel TYobjet à partir de rien, et le rendre! Je peux inspecter les deux objets et effectuer différentes actions en fonction de ce que je trouve. Je peux apporter des modifications arbitraires aux deux objets transmis.

I / O est un marteau similaire. Le code peut afficher des messages à l’utilisateur, ouvrir des connexions à une base de données, reformater votre disque dur, ou n’importe quoi, vraiment.


La foobarfonction Haskell , en revanche, ne peut prendre que certaines données et les restituer telles quelles. Il ne peut pas "regarder" les données, car leur type est inconnu au moment de la compilation. Il ne peut pas créer de nouvelles données, car ... eh bien, comment construisez-vous des données de tout type possible? Vous auriez besoin de réflexion pour cela. Il ne peut effectuer aucune E / S, car la signature de type ne déclare pas qu'une E / S est en cours d'exécution. Donc, il ne peut pas interagir avec le système de fichiers ou le réseau, ni même exécuter des threads dans le même programme! (C'est-à-dire qu'il est garanti à 100% avec un thread-safe.)

Comme vous pouvez le voir, en ne vous laissant pas faire beaucoup de choses, Haskell vous permet de donner de très fortes garanties sur ce que votre code fait réellement. Si étroit, en fait, que (pour un code vraiment polymorphe), il n'y a généralement qu'un seul moyen possible de lier les morceaux.

(Pour être clair: il est toujours possible d'écrire des fonctions Haskell où la signature de type ne vous en dit pas beaucoup Int -> Int. Cela pourrait être à peu près tout. Mais même dans ce cas, nous savons que la même entrée produira toujours la même sortie avec une certitude à 100%. Java ne le garantit même pas!)

MathematicalOrchid
la source
4
+1 excellente réponse! C'est très puissant et souvent sous-estimé par les nouveaux arrivants à Haskell. À propos, une fonction "impossible" plus simple serait fubar :: a -> b, n'est-ce pas? (Oui, je suis au courant unsafeCoerce. Je suppose que nous ne parlons de rien avec "dangereux" dans son nom, et les nouveaux arrivants ne devraient pas s'en inquiéter!: D)
Andres F.
Il y a beaucoup de signatures de type plus simples que vous ne pouvez pas écrire, oui. Par exemple, foobar :: xest assez inapplicable ...
MathematicalOrchid
En fait, vous ne pouvez pas rendre le code pur thread-safe, mais vous pouvez quand même le rendre multi-thread. Vos options sont "avant d’évaluer ceci, évaluer ceci", "lorsque vous évaluez ceci, vous pouvez également évaluer ceci dans un fil séparé", et "lorsque vous évaluez cela, vous pouvez également évaluer ceci, éventuellement dans un fil séparé ". La valeur par défaut est "fais ce que tu veux", ce qui signifie essentiellement "évalue le plus tard possible".
John Dvorak le
De manière plus prosaïque, vous pouvez appeler des méthodes d'instance sur in1 ou in2 qui ont des effets secondaires. Vous pouvez également modifier l’état global (qui, accordé, est modélisé comme une action IO dans Haskell, mais pourrait ne pas correspondre à ce que la plupart des gens considèrent comme des IO).
Doug McClean
2
@isomorphismes Le type x -> y -> yest parfaitement implémentable. Le type (x -> y) -> yn'est pas. Le type x -> y -> yprend deux entrées et retourne la seconde. Le type (x -> y) -> yprend une fonction qui opère x, et doit en quelque sorte en faire une y...
MathematicalOrchid
17

Une question connexe SO .

Je suppose que vous pouvez faire la même chose avec haskell (c.-à-d. Insérer des éléments dans des chaînes / entres qui devraient vraiment être des types de données de première classe)

Non, vous ne pouvez vraiment pas - du moins pas de la même manière que Java. En Java, ce genre de chose se produit:

String x = (String)someNonString;

et Java se fera un plaisir d'essayer de convertir votre non-chaîne en chaîne. Haskell n'autorise pas ce genre de chose, éliminant toute une classe d'erreurs d'exécution.

nullfait partie du système de types (as Nothing), il doit donc être explicitement demandé et traité, éliminant ainsi toute une autre classe d’erreurs d’exécution.

Il y a aussi une foule d'autres avantages subtils - en particulier en ce qui concerne la réutilisation et les classes de types - que je n'ai pas l'expertise nécessaire pour connaître suffisamment pour communiquer.

La plupart du temps, c’est parce que le système de types de Haskell permet une grande expressivité. Vous pouvez faire beaucoup de choses avec seulement quelques règles. Considérez l’arbre Haskell toujours présent:

data Tree a = Leaf a | Branch (Tree a) (Tree a) 

Vous avez défini une arborescence binaire générique complète (et deux constructeurs de données) dans une ligne de code relativement lisible. Tout en utilisant quelques règles (avoir des types de somme et des types de produits ). C'est 3-4 fichiers de code et classes en Java.

Surtout parmi ceux qui sont enclins à vénérer les systèmes de type, ce type de concision / élégance est très apprécié.

Telastyn
la source
Je n'ai compris que NullPointerExceptions de votre réponse. Pourriez-vous inclure plus d'exemples?
Jesvin Jose
2
Pas nécessairement vrai, JLS §5.5.1 : Si T est un type de classe, alors soit | S | <: | T |, ou | T | <: | S |. Sinon, une erreur de compilation survient. Donc, le compilateur ne vous permettra pas de lancer des types non convertibles - il y a évidemment des façons de le contourner.
Boris l'Araignée
A mon avis, le moyen le plus simple de présenter les avantages des classes de types est qu’elles interfacepeuvent être ajoutées après coup, sans oublier le type qui les implémente. En d'autres termes, vous pouvez vous assurer que deux arguments d'une fonction ont le même type, contrairement à interfaces, où deux List<String>s peuvent avoir des implémentations différentes. Vous pouvez techniquement faire quelque chose de très similaire en Java en ajoutant un paramètre de type à chaque interface, mais 99% des interfaces existantes ne le font pas et vous allez déconcerter vos pairs.
Doval
2
@BoristheSpider C'est vrai, mais les exceptions de casting impliquent presque toujours le passage au noir d'une super-classe à une sous-classe ou d'une interface à une classe, et il n'est pas rare que la super-classe soit Object.
Doval
2
Je pense que la question sur les chaînes ne concerne pas les erreurs de type cast et runtime, mais le fait que si vous ne voulez pas utiliser de types, Java ne vous obligera pas à stocker vos données dans des fichiers sérialisés. forme, en abusant des chaînes comme un type ad-hoc any. Haskell ne vous empêchera pas de le faire non plus, car ... eh bien, il a des chaînes. Haskell peut vous donner des outils, cela ne peut pas vous empêcher de faire des choses stupides si vous insistez pour que Greenspunning ait suffisamment d'interprète pour se réinventer nulldans un contexte imbriqué. Aucune langue ne peut.
Leushenko
0

L’un des "mèmes" dont les habitués ont souvent parlé, est l’ensemble "s’il compile, cela fonctionnera *" - ce qui, à mon avis, est lié à la force du système de caractères.

Ceci est surtout vrai avec les petits programmes. Haskell vous empêche de commettre des erreurs faciles dans d'autres langues (par exemple, comparer un Int32et un Word32et quelque chose explose), mais cela ne vous empêche pas de toutes les erreurs.

Haskell facilite beaucoup la refactorisation . Si votre programme était correct auparavant et qu'il vérifie la typographie, il y a de bonnes chances qu'il le soit toujours après des corrections mineures.

J'essaie de comprendre pourquoi exactement Haskell est meilleur que les autres langages statiquement typés à cet égard.

Les types en Haskell sont assez légers, car il est facile de déclarer de nouveaux types. Cela contraste avec un langage comme Rust, où tout est un peu plus lourd.

Mon hypothèse est que c'est ce que les gens entendent par système de type fort, mais il ne m'est pas évident de savoir pourquoi Haskell est meilleur.

Haskell présente de nombreuses fonctionnalités au-delà de la simple somme et des types de produits; il a aussi des types universellement quantifiés (par exemple id :: a -> a). Vous pouvez également créer des types d'enregistrement contenant des fonctions, ce qui est assez différent d'un langage tel que Java ou Rust.

GHC peut également dériver des instances basées sur des types uniquement, et depuis l'avènement des génériques, vous pouvez écrire des fonctions génériques entre types. C'est très pratique et plus fluide que le même en Java.

Une autre différence est que Haskell a tendance à avoir des erreurs de type relativement bonnes (au moins en écriture). L'inférence de type de Haskell est sophistiquée, et il est assez rare que vous deviez fournir des annotations de type afin d'obtenir quelque chose à compiler. Cela contraste avec Rust, où l'inférence de type peut parfois nécessiter des annotations même lorsque le compilateur peut en principe en déduire le type.

Enfin, Haskell a des classes de classe, parmi lesquelles la célèbre monade. Les monades sont un moyen particulièrement efficace de gérer les erreurs; ils vous offrent pratiquement tout le confort nullsans le débogage horrible et sans renoncer à votre sécurité. La capacité à écrire des fonctions sur ces types est donc un facteur important pour nous encourager à les utiliser!

En d'autres termes, vous pouvez écrire du bon ou du mauvais Java, je suppose que vous pouvez faire la même chose en Haskell.

C'est peut-être vrai, mais il manque un point crucial: le point où vous commencez à vous tirer une balle dans le pied à Haskell est plus loin que le moment où vous commencez à vous tirer une balle dans le pied à Java.


la source