web-dev-qa-db-fra.com

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

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!

209
phatmanace

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))

  • 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 certain type ne peuvent pas se transformer sans motif en un autre type. C'est parfois en contradiction avec la mutabilité (voir OCaml's restriction de valeur)
  • Types de données algébriques . Les types à Haskell ont essentiellement la même structure que les mathématiques du secondaire. C'est scandaleusement simple et cohérent, pourtant, il s'avère que, aussi puissant que vous pourriez souhaiter. C'est simplement une excellente base pour un système de type.
    • Programmation générique de type de données . Ce n'est pas la même chose que les types génériques (voir généralisation). Au lieu de cela, en raison de la simplicité de la structure de type, comme indiqué précédemment, il est relativement facile d'écrire du code qui fonctionne de manière générique sur cette structure. Plus tard, je parlerai de la façon dont quelque chose comme Equality 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.
  • Types mutuellement récursifs . Ce n'est qu'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 récursives sur 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 a Et remarquez comment ce type fonctionne. C'est délicat!
  • Généralisation . C'est presque trop idiot pour ne pas avoir dans un système de type (ahem, en vous regardant, allez). Il est important d'avoir des notions de variables de type et la capacité de parler de code qui est indépendant 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 une élaboration du typage HM et le système F est essentiellement le foyer de généralisation. Ce que je veux dire, c'est que Haskell a une histoire de généralisation très bien.
  • Types abstraits . L'histoire de Haskell ici n'est pas géniale mais aussi 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 les modifications apportées au code d'implémentation à une date ultérieure et, ce qui est important car c'est la base de toutes les opérations dans Haskell, d'écrire des types "magiques" qui ont des interfaces bien définies telles que 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.
  • Paramétrie . Les valeurs Haskell n'ont pas n'importe quel opérations universelles. Java viole cela avec des choses comme l'égalité de référence et le hachage et encore plus de manière flagrante avec les coercitions. Cela signifie que vous obtenez théorèmes gratuits sur les types qui vous permettent de connaître la signification d'une opération ou d'une valeur à un degré remarquable entièrement à partir de son type --- certains types sont tels qu'il ne peut y avoir qu'un très petit nombre d'habitants.
  • Les types de type supérieur affichent tous les types lors de l'encodage de choses plus délicates. Functor/Applicative/Monad, Foldable/Traversable, tout le système de typage des effets 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.
  • Classes de types . Si vous considérez les systèmes de types comme des logiques, ce qui est utile, vous devez souvent prouver les choses. Dans de nombreux cas, il s'agit essentiellement de 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 sont un moyen pour Haskell de générer les preuves pour vous. En termes plus concrets, cela vous permet de résoudre de simples "systèmes d'équations de types" comme "A quel type avons-nous l'intention de (+) 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.
    • Calcul des contraintes . Les contraintes dans Haskell, qui sont le mécanisme permettant d'accéder au système de prologue de classe de types, sont structurellement typées. Cela donne une très simple forme de relation de sous-typage qui vous permet d'assembler des contraintes complexes à partir de contraintes plus simples. Toute la bibliothèque mtl est basée sur cette idée.
    • Dérivation . Afin de piloter le canonicity du système de classes, 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. Faire à la structure très normale des types Haskell, il est souvent possible de demander au compilateur de faire ce passe-partout pour vous.
    • Tapez la classe prolog . Le solveur de classe de type Haskell - le système qui génère les "preuves" dont j'ai parlé plus haut - est essentiellement une forme de Prolog paralysée avec de plus belles propriétés sémantiques. Cela signifie que vous pouvez encoder des choses vraiment velues dans le type prolog et vous attendre à ce qu'elles soient gérées tout au moment de la compilation. Un bon exemple pourrait être de trouver une 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 extrêmement utiles pour le prologue de la classe de base. Si vous connaissez Prolog, vous pouvez imaginer combien la puissance expressive augmente lorsque vous pouvez écrire des prédicats de plus d'une variable.
  • Très bonne inférence . Les langues basées sur des systèmes de type Hindley Milner ont une assez bonne inférence. HM lui-même a complet inférence ce qui signifie que vous n'avez jamais besoin d'écrire une variable de type. Haskell 98, la forme la plus simple de Haskell, jette déjà cela dans certaines circonstances très rares. En règle générale, Haskell moderne a été une expérience pour réduire lentement l'espace d'inférence complète tout en ajoutant plus de puissance à HM et en voyant les utilisateurs se plaindre. Les gens se plaignent très rarement - l'inférence de Haskell est assez bonne.
  • Sous-typage très, très, très faible uniquement . J'ai mentionné plus tôt que le système de contraintes du prologue de classe de types a une notion de sous-typage structurel. C'est la seule forme de sous-typage dans Haskell. Le sous-typage est terrible pour le raisonnement et l'inférence. Cela rend chacun de ces problèmes beaucoup plus difficile (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 (le sous-classement est-il le même que le sous-typage? Bien sûr que non! Mais les gens le confondent très souvent et de nombreuses langues contribuent à cette confusion! Comment en sommes-nous arrivés là? Je suppose que personne n'a jamais examiné le LSP.)
    • Remarque récemment (début 2017) Steven Dolan a publié son thèse sur MLsub , une variante de l'inférence de type ML et Hindley-Milner qui a un très bien 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 suggère que nous venons aujourd'hui de découvrir des moyens prometteurs pour que l'inférence complète et le sous-typage jouent bien ensemble. Maintenant, pour être totalement clair, les notions Java de sous-typage ne sont en aucun cas en mesure de 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 qu'une simple généralisation, il est utile de pouvoir parler de types qui ont des variables généralisées en eux. Par exemple, un mappage entre des structures d'ordre supérieur qui est inconscient (voir paramétricité) à ce que ces structures "contiennent" a un type comme (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 existentiels . Alors que les types de rang supérieur nous permettent de parler de quantification universelle, les types existentiels nous parlent de quantification existentielle: l'idée qu'il existe simplement existe un type inconnu satisfaisant certaines équations. Cela finit par être utile et continuer plus longtemps à ce sujet prendrait beaucoup de temps.
  • Familles de types . Parfois, les mécanismes de typeclass ne sont pas pratiques car nous ne pensons pas toujours dans Prolog. Les familles de types nous permettent d'écrire des relations fonctionnelles droites entre les types.
    • Familles de types fermées . Les familles de types sont par défaut ouvertes, ce qui est ennuyeux car cela signifie que même si vous pouvez les étendre à tout moment, vous ne pouvez pas les "inverser" avec un quelconque espoir de succès. C'est parce que vous ne pouvez pas prouver l'injectivité, mais avec des familles de type fermées vous pouvez.
  • 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.

  • Pureté . Haskell ne permet aucun effet secondaire pour une définition très, très, très large d '"effet secondaire". Cela vous oblige à mettre plus d'informations dans les types car les types 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 - puisque tout programme réel doit en inclure - donc une combinaison de classes de types, de types de type supérieur et de types abstraits a donné naissance à la notion d'utiliser un type particulier, super spécial appelé 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.
  • Manque de 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.
  • 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 d'en parler, mais particulièrement utile pour parler des types imbriqués. Revenez au type 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.

234
J. Abrahamson
  • Inférence de type complet. Vous pouvez réellement utiliser des types complexes de manière omniprésente sans vous sentir comme, "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 très facilement des idées complexes.
  • Haskell a des classes de types, qui sont en quelque sorte des interfaces similaires, 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 à mettre davantage de fonctionnalités à la disposition du vérificateur de type. Prenez filtre , par exemple. Dans un langage impératif, vous pouvez écrire une boucle 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 .
  • Le manque de sous-types simplifie considérablement le polymorphisme paramétrique.
  • Les types de type supérieur (types de types) sont relativement faciles à spécifier et à utiliser dans Haskell, ce qui vous permet de créer des abstractions autour de types qui sont complètement insondables en Java.
80
Karl Bielefeldt
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?)

63
WolfeFan

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:

  • Revenir in2 comme résultat.
  • Boucle pour toujours et ne retourne jamais rien.
  • Jetez une exception et ne retournez jamais rien.

En fait, Haskell a également ces trois options. Mais C # vous donne également les options supplémentaires:

  • Renvoie null. (Haskell n'a pas null.)
  • Modifier in2 avant de le retourner. (Haskell n'a pas de modification sur 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 d'E/S à moins que vous ne déclariez 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 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!)

31
MathematicalOrchid

ne question connexe SO .

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é.

17
Telastyn

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.

0
user275647