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 string
contient 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!
la source
Maybe
ne 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.Réponses:
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)
Eq
un 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.data Bt a = Here a | There (Bt (a, a))
. Réfléchissez bien aux valeurs valides deBt a
et notez comment ce type fonctionne. C'est délicat!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.mtl
systè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.(+)
choses ensemble? OhInteger
, d'accord! Insérons le bon code maintenant!". Sur des systèmes plus complexes, vous pouvez établir des contraintes plus intéressantes.mtl
bibliothèque est basée sur cette idée.(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 laa
variable est liée uniquement dans l'argument. Cela signifie que le défineur de la fonctionmapFree
doit décider de ce quia
est instancié au moment où il l’utilise, et non de l’utilisateurmapFree
.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
State
s'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 telsHandle
que prendre des arguments de types spécifiques tels queState
. C'est déroutant de comprendre tous les détails, mais aussi de manière très juste.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 -> TypeRepr
vous 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é . LeTypeable
système de Haskell permet la création d'un coffre-fortcoerce :: (Typeable a, Typeable b) => a -> Maybe b
. Ce système repose surTypeable
sa 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.
IO a
pour représenter calculs ayant des effets secondaires qui donnent des valeurs de typea
. C'est la base d'un très bon système d'effets intégré dans un langage pur.null
. Tout le monde sait quenull
c'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 typeA
dans le typeMaybe A
, atténuent complètement le problème denull
.Bt a
genre d'avant et d' essayer d'écrire une fonction pour calculer sa taille:size :: Bt a -> Int
. Ça va ressembler un peusize (Here a) = 1
etsize (There bt) = 2 * size bt
. Sur le plan opérationnel, cela n’est pas trop complexe, mais notez que l’appel récursif desize
la 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.
la source
char *p = NULL;
*p=1234
char *q = p+5678;
*q = 1234;
null
est 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.p = undefined
aussi longtemps qu'ilp
n'est pas évalué. Plus utilement, vous pouvez mettreundefined
une 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.for
boucle pour implémenter la même fonctionnalité, mais vous n'aurez pas les mêmes garanties de type statique, car unefor
boucle n'a pas de concept de type de retour.la source
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?)
la source
Maybe
(par exemple, JavaOptional
et ScalaOption
), mais dans ces langages c'est une solution à moitié cuite, puisque vous pouvez toujours assignernull
une 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 quefromJust
lorsque vous en avez uneNothing
, mais ces fonctions sont probablement mal vues).IO Integer
plus proche de "sous-programme qui, lorsqu'il est exécuté, donne un entier"? Comme a) dansmain = c >> c
la valeur renvoyée par le premierc
peut être différent alors par le secondc
alors quea
aura 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).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:
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:
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é.)
Cette méthode peut faire plusieurs choses:
in2
comme résultat.En fait, Haskell propose également ces trois options. Mais C # vous offre également des options supplémentaires:
in2
avant de le retourner. (Haskell n'a pas de modification en place.)La réflexion est un marteau particulièrement gros. en utilisant la réflexion, je peux construire un nouvel
TY
objet à 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
foobar
fonction 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!)la source
fubar :: a -> b
, n'est-ce pas? (Oui, je suis au courantunsafeCoerce
. 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)foobar :: x
est assez inapplicable ...x -> y -> y
est parfaitement implémentable. Le type(x -> y) -> y
n'est pas. Le typex -> y -> y
prend deux entrées et retourne la seconde. Le type(x -> y) -> y
prend une fonction qui opèrex
, et doit en quelque sorte en faire uney
...Une question connexe SO .
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:
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.
null
fait partie du système de types (asNothing
), 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:
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é.
la source
interface
peuvent ê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 àinterface
s, où deuxList<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.Object
.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éinventernull
dans un contexte imbriqué. Aucune langue ne peut.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
Int32
et unWord32
et 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.
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.
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
null
sans 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!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