Je commence à apprendre Haskell . Je suis très nouveau dans ce domaine et je lis simplement quelques livres en ligne pour me familiariser avec ses concepts de base.
L'un des "mèmes" dont les gens qui le connaissent ont souvent parlé, est la chose "si ça compile, ça marchera *" - ce qui, je pense, est lié à la force du système de type.
J'essaie de comprendre pourquoi exactement Haskell est meilleur que les autres langages typés à cet égard.
Autrement dit, je suppose qu'en Java, vous pourriez faire quelque chose de haineux comme enterrer ArrayList<String>()
pour contenir quelque chose qui devrait vraiment être ArrayList<Animal>()
. La chose odieuse ici est que votre string
contient elephant, giraffe
, Etc, et si quelqu'un met Mercedes
- votre compilateur ne vous aidera pas.
Si je did do ArrayList<Animal>()
alors, à un moment ultérieur, si je décide mon programme n'est pas vraiment sur les animaux, c'est sur les véhicules, alors je peux changer, disons, une fonction qui produit ArrayList<Animal>
pour produire ArrayList<Vehicle>
et mon IDE devrait me dire partout où il y a une pause de compilation.
Mon hypothèse est que c'est ce que les gens entendent par un système de type strong, mais il n'est pas évident pour moi pourquoi Haskell est meilleur. Autrement dit, vous pouvez écrire du bon ou du mauvais Java, je suppose que vous pouvez faire la même chose dans Haskell (c'est-à-dire remplir des choses en chaînes/entrées qui devraient vraiment être des types de données de première classe).
Je soupçonne que je manque quelque chose d'important/basique.
Je serais très heureux de voir l'erreur de mes voies!
Voici une liste non ordonnée des fonctionnalités du système de type disponibles dans Haskell et non disponibles ou moins Nice dans Java (à ma connaissance, qui est certes faible par rapport à Java))
Eq
uality pourrait être dérivé automatiquement pour un type défini par l'utilisateur par un compilateur Haskell. Essentiellement, la façon de procéder consiste à parcourir la structure simple et commune sous-jacente à tout type défini par l'utilisateur et à la faire correspondre entre les 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 de Bt a
Et remarquez comment ce type fonctionne. C'est délicat!IO
. Java a probablement en fait une histoire de type abstrait plus agréable, pour être honnête, mais je ne pense pas que tant que les interfaces soient devenues plus populaires, c'était vraiment vrai.mtl
, points de repère généralisés du fonctor. La liste se rallonge de plus en plus. Il y a beaucoup de choses qui s'expriment mieux dans les types supérieurs et relativement peu de systèmes de types permettent même à l'utilisateur de parler de ces choses.(+)
Choses ensemble? Oh, Integer
, ok! Insérons le bon code maintenant ! ". Dans des systèmes plus complexes, vous pourriez établir des contraintes plus intéressantes. mtl
est basée sur cette idée.(forall a. f a -> g a)
. Dans HM droit, vous pouvez écrire une fonction de ce type, mais avec des types de rang supérieur, vous exigez une fonction telle qu'un argument comme ceci: mapFree :: (forall a . f a -> g a) -> Free f -> Free g
. Notez que la variable a
n'est liée que dans l'argument. Cela signifie que le definer de la fonction mapFree
décide de ce que a
est instancié au moment où ils l'utilisent, pas l'utilisateur de mapFree
. Types indexés et promotion des types . Je deviens vraiment exotique à ce stade, mais ceux-ci 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. Notez dans l'extrait suivant que State
est 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 type comme Handle
comme prenant des arguments à des sortes comme State
. C'est déroutant de comprendre tous les détails, mais c'est aussi très juste.
data State = Open | Closed
data Handle :: State -> * -> * where
OpenHandle :: {- something -} -> Handle Open a
ClosedHandle :: {- something -} -> Handle Closed a
Représentations de type Runtime qui fonctionnent . Java est connu pour avoir effacé les caractères et avoir cette caractéristique pluie sur les défilés de certaines personnes. Effacer les caractères c'est la bonne façon de faire, cependant, comme si vous aviez une fonction getRepr :: a -> TypeRepr
alors vous violez à tout le moins la paramétricité. Ce qui est pire, c'est que si c'est une fonction générée par l'utilisateur qui est utilisée pour déclencher des coercitions dangereuses lors de l'exécution ... alors vous avez un problème de sécurité énorme. Le système Typeable
de Haskell permet la création d'une coerce :: (Typeable a, Typeable b) => a -> Maybe b
sûre. Ce système repose sur l'implémentation de Typeable
dans le compilateur (et non sur l'espace utilisateur) et ne pourrait pas non plus recevoir une telle sémantique niçoise sans le mécanisme de typeclass de Haskell et les lois qu'il est garanti de suivre.
Plus que cela, la valeur du système de types de Haskell se rapporte également à la façon 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 type.
IO a
Pour représenter les calculs à effets secondaires qui donnent des valeurs de type a
. Ceci est le fondement d'un très Joli système d'effets intégré dans un langage pur.null
. Tout le monde sait que null
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" aux types que vous avez en transformant un type A
en type Maybe A
, Atténuent complètement le problème de null
.Bt a
D'avant et essayez d'écrire une fonction pour calculer sa taille: size :: Bt a -> Int
. Cela ressemblera un peu à size (Here a) = 1
et size (There bt) = 2 * size bt
. Sur le plan opérationnel, ce n'est pas trop complexe, mais notez que l'appel récursif à size
dans la dernière équation se produit à un type différent, mais la définition globale a un type généralisé Nice 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'autorisera.Je pourrais continuer, mais cette liste devrait vous aider à démarrer, puis à en savoir plus.
for
pour implémenter la même fonctionnalité, mais vous n'aurez pas les mêmes garanties de type statique, car une boucle for
n'a pas de concept de type de retour .a :: Integer
b :: Maybe Integer
c :: IO Integer
d :: Either String Integer
Dans Haskell: un entier, un entier qui pourrait être nul, un entier dont la valeur est venue du monde extérieur et un entier qui pourrait être une chaîne à la place, sont tous des types distincts - et le compilateur appliquera cela =. 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 bien?)
Beaucoup de gens ont énuméré de bonnes choses sur Haskell. Mais en réponse à votre question spécifique "pourquoi le système de type rend les programmes plus corrects?", Je soupçonne que la réponse est "polymorphisme paramétrique".
Considérez la fonction Haskell suivante:
foobar :: x -> y -> y
Il y a littéralement seulement ne façon possible pour implémenter cette fonction. Juste par la signature de type, je peux dire précisément ce que fait cette fonction, car il n'y a qu'une seule chose possible qu'elle peut faire. [OK, pas tout à fait, mais presque!]
Arrêtez-vous et réfléchissez-y un instant. C'est vraiment un gros problème! Cela signifie que si j'écris une fonction avec cette signature, c'est en fait impossible pour que la fonction fasse autre chose que ce que je voulais. (La signature de type elle-même peut toujours être fausse, bien sûr. Aucun langage de programmation n'empêchera jamais tous les bogues.)
Considérez cette fonction:
fubar :: Int -> (x -> y) -> y
Cette fonction est impossible. Vous ne pouvez littéralement pas implémenter cette fonction. Je peux le dire uniquement à partir de la signature de type.
Comme vous pouvez le voir, une signature de type Haskell vous en dit beaucoup!
Comparez avec C #. (Désolé, mon Java est un peu rouillé.)
public static TY foobar<TX, TY>(TX in1, TY in2)
Il y a quelques choses que cette méthode pourrait faire:
in2
comme résultat.En fait, Haskell a également ces trois options. Mais C # vous donne également les options supplémentaires:
in2
avant de le retourner. (Haskell n'a pas de modification sur place.)La réflexion est un marteau particulièrement gros; en utilisant la réflexion, je peux construire un nouvel objet TY
à partir de rien, et le retourner! 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 passés.
I/O est un gros marteau similaire. Le code peut être afficher des messages à l'utilisateur, ou ouvrir des connexions à une base de données, ou reformater votre disque dur, ou quoi que ce soit, vraiment.
La fonction Haskell foobar
, en revanche, peut niquement prendre des données et les renvoyer 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 que des E/S sont effectuées. Il ne peut donc 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 100% garanti sans fil.)
Comme vous pouvez le voir, en pas en vous laissant faire tout un tas de choses, Haskell vous permet de faire de très fortes garanties sur ce que fait réellement votre code. Tellement serré, en fait, que (pour un code vraiment polymorphe), il n'y a généralement qu'une seule façon possible pour que les pièces s'emboîtent.
(Pour être clair: il est toujours possible d'écrire des fonctions Haskell où la signature de type ne vous dit pas grand-chose. Int -> Int
pourrait être à peu près n'importe quoi. Mais même alors, nous savons que la même entrée produira toujours la même sortie avec une certitude à 100%. Java ne garantit même pas cela!)
Je suppose que vous pouvez faire la même chose dans haskell (c'est-à-dire que les choses dans des chaînes/entiers 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 peut. En Java, ce genre de chose se produit:
String x = (String)someNonString;
et Java essaiera avec plaisir de transtyper 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 type (comme Nothing
) doit donc être explicitement demandé et géré, éliminant toute une autre classe d'erreurs d'exécution.
Il y a aussi un tas d'autres avantages subtils - en particulier autour de la réutilisation et des classes de types - que je n'ai pas l'expertise pour connaître assez bien pour communiquer.
Mais surtout, c'est parce que le système de types de Haskell permet beaucoup d'expressivité. Vous pouvez faire beaucoup de choses avec seulement quelques règles. Considérez l'arbre Haskell omniprésent:
data Tree a = Leaf a | Branch (Tree a) (Tree a)
Vous avez défini un arbre binaire générique complet (et deux constructeurs de données) dans une seule ligne de code assez lisible. Tout cela en utilisant seulement quelques règles (ayant types de somme et 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é.
L'un des "mèmes" dont les gens qui le connaissent ont souvent parlé, est la chose "si ça compile, ça marchera *" - ce qui, je pense, est lié à la force du système de type.
Cela est principalement vrai pour les petits programmes. Haskell vous empêche de faire des erreurs qui sont faciles dans d'autres langues (par exemple en comparant un Int32
et un Word32
et quelque chose explose), mais cela ne vous empêche pas de toutes les erreurs.
Haskell facilite en fait la refactorisation d'un lot. Si votre programme était précédemment correct et qu'il vérifie la typographie, il y a de fortes chances qu'il soit toujours correct après des modifications mineures.
J'essaie de comprendre pourquoi exactement Haskell est meilleur que d'autres langages typés statiquement à cet égard.
Les types dans Haskell sont assez légers, en ce sens qu'il est facile de déclarer de nouveaux types. Cela contraste avec une langue comme Rust, où tout est un peu plus lourd.
Mon hypothèse est que c'est ce que les gens entendent par un système de type fort, mais il n'est pas évident pour moi pourquoi Haskell est meilleur.
Haskell a de nombreuses fonctionnalités au-delà de la simple somme et des types de produits; il a des types universellement quantifiés (par exemple id :: a -> a
) ainsi que. 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 certaines instances basées uniquement sur les types, et depuis l'avènement des génériques, vous pouvez écrire des fonctions génériques entre les types. C'est assez pratique, et c'est plus fluide que la même chose en Java.
Une autre différence est que Haskell a tendance à avoir des erreurs de type relativement bonnes (au moment de l'écriture, au moins). 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 déduire le type.
Enfin, Haskell possède des classes de caractères, parmi lesquelles la célèbre monade. Les monades s'avèrent être un moyen particulièrement agréable de gérer les erreurs; ils vous offrent essentiellement presque toutes les commodités de null
sans le débogage horrible et sans renoncer à la sécurité de votre type. Donc, la possibilité d'écrire fonctions sur ces types importe en fait beaucoup quand il s'agit de nous encourager à les utiliser!
Autrement dit, vous pouvez écrire du bon ou du mauvais Java, je suppose que vous pouvez faire de même dans Haskell
C'est peut-être vrai, mais il manque un point crucial: le point où vous commencez à vous tirer dans le pied à Haskell est plus éloigné que le point où vous commencez à vous tirer dans le pied à Java.