Je pensais comme cela il y a quelques temps. J'ai fais 7 ans de python acharné sur des projets libres ou sur mes projets persos et j'étais convaincu qu'une bonne couverture de test était la solution à tous les problèmes. J'ai fais des présentation en entreprise où à l'université à ce sujet. J'ai appliqué cela pendant 6 ans en entreprise. Puis un jour j'ai découvert que la couverture de test à 100 % n'existait pas, que souvent du code n'avait pas été documenté parce que pas le temps et que de toute façon, même avec des tests et de la documentation, le refactoring en python me péterait toujours à la gueule en production dans le petit bout de code bien tordu qui appelait la fonction que j'avais raté. C'est l'example donné dans la présentation sur mypy de ce journal, quand tu as une method dosomething sur un objet et que un grep dans ta base de code te donne 150 instances de dosomething, lequelles tu dois modifier ?
Puis j'ai découverts ce qu'apporte le typage statique, en haskell principalement (mais j'ai fais un peu de rust et de ocaml). Quand tu refactor tu as une quantité d'erreur que tu ne peux plus faire du fait du typage statique. Un bon système de type évite aussi des erreurs immonde qui peuvent arriver avec un mauvais système de type (comme des casts implicites entre des matrices et des booléens, histoire vécue qui m'a fait perdre 10% de performance pendant 6 mois sur un logiciel "haute performance"). Un bon système de type évite aussi les null pointer exception. Un bon système de type te permet de modéliser avec les types et une fois que tu as trouvé les bonnes abstractions, tu peux écrire du code très rapidement. Et un bon système de type ne râle que quand tu fais des bêtises.
Après, un mauvais système de type, c'est juste lourd et contraignant, cela râle tout le temps pour rien, et malheureusement on se tape beaucoup de mauvais système de type comme example de ce qu'est le typage. Si votre idée d'un système de type est celle présentée dans Java ou C++, je vous invite vraiment à vous refaire une idée en allant voir ce qui se fait à coté. Rust est un bon point d'entrée, Haskell est énorme mais fait peur. Ocaml c'est sympa.
Le système d'inférence ne fait pas d'hypothèse, seulement des observations et une propagation de contraintes.
Dans le cas de Haskell, si tu écris une fonction pgcd, supposons comme ceci :
gcd'ab|b>0=gcd'b(modab)-- cas 1|otherwise=a-- cas 2
(Note, je l'appelle gcd' car gcd est déjà pris par une fonction qui fait la même chose… ;)
Le type se déduit de façon suivante :
il faut pouvoir appliquer (>) sur b et (>) est définie dans la typeclass Ord
il faut pouvoir appliquer mod a b, le type de mod est Integral t => t -> t -> t, donc on conclue que a et b sont dans la type classe Integral.
Integral étant plus restrictif que Ord, alors la seule contrainte c'est que a et b soient de type Integral
le type de retour de gcd' est soit le même que gcd' (cas 1), soit le même que a (cas 2).
On déduit donc le type suivant : gcd' :: Integral t => t -> t -> t.
À partir du moment où tu passes n'importe quel type qui implement Integral, alors tu n'auras pas de soucis. Si tu passes un autre type, cela ne marchera pas, mais bon, c'est normal, ton type ne sait pas faire…
Le seul problème ici c'est que, au sens de pas mal de monde, Integral est trop restrictif, puisque que pour implementer Integral il faut implement Integral et toutes ses sous typeclass, dont Real, Enum, Num, Ord et Eq avec, au minimum :
Integral : quotRem (div et mod en gros), toInteger
Real : toRational
Enum : toEnum, fromEnum
Num : (+), (*), abs, signum, fromInteger, negate ou (-)
Ord : (<=)
Eq : (==) ou (/=)
Ce qui fait beaucoup de bordel à implementer pour calculer un pgcd sur un type custom. La hiérarchie de type pour les nombres en Haskell est trop restrictive à mon gout, mais bon, personne n'est parfait ;) (En pratique cela ne pose rarement de problème sauf quand tu es un fou d'architecture par les types, mais à ce moment là Haskell n'est plus assez puissant pour tes besoins…)
De plus, il n'est pas obligatoire mais fortement conseillé de suivre des règles lors de l'implementation de ces classes (genre s'assurer que a + b == a - (negate b)).
Cependant, contrairement à de nombreux langage, ce mécanisme impose une certaine cohérence. Quand tu vois a / b, tu sais que c'est l’opérateur / de la classe Fractional et tu peux t'attendre a un comportement. Ce n'est pas le cas en python ou C++ ou le / peut être surchargé en n'importe quoi. Par example, certains s'en servent pour concaténer des chemins de fichiers. C'est une restriction mais j'ai appris à l'aimer car cela rend le code très explicite.
Je suis un peu déçu, je pensais que c'était inféré.
Il y a méprise. Ce qui est inferé c'est le type ou la classe (i.e. "interface" nécessaire d'une fonction). Mais par contre il faut bien dire ce que fait ton interface pour un type particulié. Par example si j'ai une interface Surface avec une fonction surface et que j'ai un type Rectangle, il faut bien que j'écrive quelque part la fonction surface du Rectangle… Par contre quand je vais écrire une fonction qui utilise surface, son type sera inféré comme prenant un type d'interface Surface en paramètre.
Oui je me suis mal exprimé, ce que je voulais dire c'est qu'une méthode prend un et un seul type par paramètre et que si on cherche à la rendre polymorphique, il faut définir un type somme.
Oui et non.
Les types sommes sont une manière d'encapsuler plusieurs trucs dans un seul type, c'est grosso modo une union typé.
Tu peux aussi définir ta fonction dans une typeclass, c'est à dire qu'elle prendra un type qui match la typeclass (i.e. l'interface). Elle est donc plus ou moins polymorphique.
Elle peut aussi être polymorphique car on se fout des types que du passes. Example d'une fonction swap (a, b) = (b, a), celle-ci prend un tuple de type (t, t') et retourne un tuple de type (t', t), en ce sens elle est polymorphique.
-- Un exemple de fonction qui est polymorphique
afficherListe :: (Affichable a) => [a] -> String
afficherListe = intercalate "," . map affiche
Petite remarque pour ceux qui ne lisent pas l'haskell couramment. La fonction afficherListe prend une liste de a et renvoie une String. a marche pour n'importe quel Affichable, mais il faut bien noter que la liste ne peut contenir qu'un seul type et le même.
En gros, si on a foo et bar des valeurs de type Foo et Bar, eux même instances de Affichable, on peut faire afficheListe [foo, foo, foo] ou afficherListe [bar, bar, bar] mais pas afficherListe [foo, bar].
En gros ce n'est pas l'équivalent des appels virtuels en C++ ou de l'héritage en python, tout est résolu à la compilation et une instance spécifique de afficherListe est générée pour chaque a possible. (Ce n'est pas complètement vrai ;)
La notion de polymorphisme à l'execution (i.e. héritage virtuel) est plus complexe en haskell…
C'est vraiment des interfaces, en haskell cela se nomme des typeclass. Sauf qu'elles sont définie de façon découplée du type associé, mais l'association est explicite.
Par example, si je définis :
classShapetwherearea::t->Float
Et que j'ai le type :
dataSquare=Square{side::Float}
Alors je peux associer la typeclass au type avec :
D'autre part, et c'est là que je me suis fourvoyé, je pensais que tu n'avais pas besoin de définir d'interface Num (que ta méthode pourrait prendre Num ou Bar), mais pour ça je présume qu'il faut explicitement dire qu'elle prend l'un ou l'autre.
Tu n'as pas besoin de définir que la fonction prend un Num ou un Bar, c'est l'inférence de type qui le fait tout seul (mais tu peux lui forcer la main en faisant quelque chose de plus restrictif). Donc en effet c'est en quelque sort du duck-typing statiquement typé. Example :
foos=areas*2
aura comme type foo :: Shape t => t -> Float
Ici n'importe quelle type qui implémente l'interface Shape fonctionne avec foo`.
En pratique il y a l'inférence de type qui fait cela très bien. Dans un langage comme Haskell, tu peux ne jamais écrire un bout de type, mais avoir une inférence qui te calcul le type tout seul et le vérifie à la compilation.
Par example une fonction qui calcul la somme des éléments d'une liste :
Note, dans la vraie vie je ne l'écrirais pas comme cela, mais plutôt :
sum[]=0sum(x:xs)=x+sumxs
Enfin bon, le compilateur infère tout seul le type de sum comme étant Num a => [a] -> a où dit autrement, une fonction qui prend une liste de a ([a]) et renvoie un a avec la contrainte que le a est un truc qui suit une "interface" Num.
De mon point de vue limité, cela donne la facilité d'écriture du code, tu ne te tracasses pas avec les types, mais au final tu as les outils du typage statique qui sont présents.
Pour le débat sur la traduction ponctuelle versus accompagnement longue durée, je suis tout à fait d'accord avec toi. Le monde de l'informatique est rempli de projet prometteur non maintenus.
Concernant ta vision de la différence entre langage de script et langage compilé je ne suis absolument pas d'accord avec toi pour plusieurs raisons.
En premier lieu, il n'y a pas que ELF dans la liste de dépendances d'un programme compilé, il y a une tonne de librairies qui peuvent changer d'ABI ou d'API et il y a aussi le compilateur et les librairies standards qui évoluent. Dernièrement, le passage à c++11 a cassé beaucoup de code. La librairie standard C++ a évoluée de façon non compatible avec l'existant. Pas plus tard que ce matin, j'ai fais évoluer la toolchain de notre entreprise de gcc 5 à gcc 6 et de clang 3.7 à clang 3.8 et j'ai réalisé une trentaine de commit lors de ce changement. Trois étaient nécessaires pour que cela compile !
Tu fais la différence entre langages compilés et langages de script avec un interpréteur, mais il existent de nombreux langages compilés qui ont un interpréteur (ou une VM, un runtime), comme Java, ou haskell, … La pluspart des langages "de script" que je connais sont compilées d'une manière où d'une autre. Python est compilé en byte-code intermédiaire pour sa VM mais il existe des projets de génération de code natif. Il existe des interpréteurs pour le C… Bref, la définition de langage de script est plus que floue.
Il en ressort que dans le cas d'un langage compilé, une seule branche sur le repository suffit à sa seule maintenance contrairement au langage de script qui doit être maintenu dans l'ancienne version de l'interpréteur, celle en cours et celle à venir. Donc 3 branches au lieu d'une.
Actuellement il est "simple" de garantir une unique base de code pour des technologies comme python, ruby, haskell, … qui fournissent des gestionnaire de dépendances dans lequels tu peux spécifier la version de l'intepreteur à utiliser ainsi que la version spécifique de toutes les dépendances. Tu es presque garantie que cela compilera / s'executera à l'identique. Et souvent tu n'as qu'une seul projet d'intepreteur à supporter.
D'un autre coté, en C++ par exemple, le roi des langages "compilé", il n'y a aucun consensus sur un outil de dependence / packaging / déploiement qui te donne les même garanties, il existe au moins 4 compilateurs différents que tu dois supporter (gcc / clang / visual studio / Intel cc) et des spécificité de programmation sur chaque système et des versions de compilateur différentes. Et crois moi (Je maintient une base de code C++ de plusieurs centaines de milliers de ligne qui tourne sur les 3 OS majeurs et sur plusieurs architecture matériel et sur les 4 compilateurs), on gère cela avec des directives de précompilation et c'est un enfer. Une des meilleure solution à l'heure actuelle c'est de distribuer ton logiciel dans une machine virtuelle, ou un conteneur léger (comme docker), mais ce n'est pas applicable à tous les projets.
Donc ma conclusion c'est que j'aimerais que on arrête de catégoriser certains outils dans ce groupe magique des "langages de script" qui est très péjoratif et souvent sur des arguments qui ne tiennent pas.
Petite histoire vraie vécue dans ma vraie vie de développeur. Etant à la R&D d'une boite, j'ai écris en python un prototype d'un outil. La solution a rapidement été validée et s'est posé la question de la mise en production du produit. Il fallait donc passer du prototype (100 lignes de python dans un seul fichier, pas de dépendance autre que l’interpréteur, portable sous linux / windows / mac OS) à la "vraie vie". Un ingénieur talentueux a donc réalisé le portage du langage de "script" en C++, parce que ce serait clairement plus rapide et plus robuste (compilé versus langage de script, y a pas photo non ?!). Quelques jour / homme plus tard, il revenait avec une version (quelques milliers de ligne de C++, qui ne marchait que dans visual studio, pas portable et avec 5 librairies en dépendance) et surtout un GROS problème. Quand ma version en python tournait en 2s, la sienne prenait 10 minutes. Impossible ! Bon, alors en fait, il avait utilisé un algo en O(N2) au lieu du mien qui était en O(N) pour la raison simple qu'il pensait que le C++ irait de toute manière plus vite. Une nouvelle dépendence plus tard (Pour avoir une hash map en C++ car il n'y en avait pas dans la standard libraire à cette époque, on aurait pu utiliser un std::set), son programme tournait en 3s… Toujours plus lent. Damned… Après profiling, c'était un problème d'allocation mémoire parce que l’algorithme créait énormément d'objet. Après avoir changé d'allocateur (à l'époque le gars a écrit un allocateur maison, mais bon, on aurait pu prendre un projet existant, je ne sais juste pas ce qui existait de bien à l'époque), le temps est passé à 2s. Pas plus rapide que la version python… Drame… En fait le temps était passé en entrée sortie avec le disque, donc il serait pas aller plus vite quoi qu'il arrive… Plus tard beaucoup de temps on été investis pour réaliser le portage de ce programme sous linux et sous mac os X et pour corriger les bugs dans l'allocateur custom. Encore plus tard, il a fallu corriger de nombreux problèmes liés à la portabilité sur différents compilateurs. Et pendant ce temps là la version python marchait toujours car elle servait de test de non régression ;)
Alors oui, ce cas est extrême, mais c'est l’exemple même de la peur des langages de "scripts". On peut reprocher beaucoup de choses à tous ces langages qui ne sont pas tout roses (python 3 ;), mais de là à dire que du code "compilé" à une meilleure durée de vie… Cela n'a rien à voir ;)
Aujourd'hui, grosso modo, il y a deux grandes familles de langage : le langage C avec ses multiples dérivés et les langages de script comme Python. Ces langages ont, au fil des ans, développés des procédures de traduction. L'une des plus connue est gettext.
Je supplie tout le monde de ne pas faire cette classification "langages de script". Après on se retrouve avec des gens qui pensent que le "script" (python, ruby, …) ne sert qu'à initialiser des vrais programmes (En Java / C / C++)… Je l'ai déjà vécu en école d’ingénieur et cela fait mal de se prendre une sale note parce "qu'un script n'est pas un programme professionnel".
D'autant que dans le contexte que tu donnes, la différence entre python et C d'un point de vu gettext est minimum…
Du coup, je rapproche ça à un pointeur (oui, je suis développeur c principalement…) avec le contenu de s' mis à jour dans g
Oui mais avec quelques subtilités. Principalement l'ordonnancement des opérations est garantie. En gros tu est certain que une utilisation de s' te retourne la somme complète, et pas une somme partielle en milieu de route.
Oui, l’implémentation doit être faite avec attention pour ne pas boucler.
letgsl=((map(/s)l),suml)
Je n'arrive pas à reproduire ta boucle en copiant ton implémentation de g. Es-tu certain que c'est bien celle que tu utilises ?
Voici mon implémentation de g :
gs(x:[])=([x/s],x)-- cas 0gs(x:xs)=let(res,s')=gsxs-- cas 1in((x/s):res,s'+x)
Ici, dans le cas 0, je traite la liste d'un seul élément. Le résultat c'est [x / s] et la somme pour un unique élément c'est x.
Dans le cas 1, je traite la liste de plus d'un élément. Je réalise d'abord un appel récursif g s xs pour récupérer le résultat res sur la sous liste et la somme s' sur la sous liste. Puis je renvois le résultat courant (x / s) : res) et la somme mise à jours s' + x.
J'ai crée une liste infinie de 1 avec une expression qui s'auto-référence. L'évaluation paresseuse fait que la liste ne sera jamais évaluée, seul les éléments nécessaires le seront. Si je demande avec take 2 les deux premiers éléments, il peux s’arrêter à 1:1:l sans évaluer la suite.
La liste d'origine est donc parcourue pour créer la 2nde liste, contenant les fonctions non évaluées. Mais pour évaluer les résultats, la 2nde liste devra aussi être parcourue, non ?
Oui. Mais de toute façon tu feras surement un traitement sur cette seconde liste, pour l'afficher par exemple. Les résultats seront évalués à ce moment. Il y a ce site que j'aime beaucoup qui permet d'observer les évaluations.
C'est marrant comment les seuls réponses que j'ai eu c'est "Je peux le faire en C / Javascript / Brainfuck".
J'ai vraiment du rater mon discours parce que je trouve le dernier point vraiment intéressant, mais il n'est pas discuté. Dommage.
Alors simplement que je ne meurs pas idiot, comment on peut faire l’exemple de structure de contrôle custom et l’exemple de parcours de liste en une fois en javascript ? A ma connaissance l'un est difficile / pas élégant / pas safe et l'autre est impossible, mais je me trompe surement.
Au départ c'est ce que je voulais faire, mais +s ne prend que le temps cpu en compte ;) Et comme ma fonction longId est un threadDelay, cela ne marche pas ;(
Ce que j'aime en haskell c'est le coté simple de la syntaxe à ce niveau, tu n'as pas à te préoccuper de qui va évaluer ton thunk, où qu'il soit évalué plusieurs fois.. Tu peux écrire une fonction qui marche sur n'importe quelle type de donnée que ce soit une donnée lazy ou pas.
Alors il est certain que cela a des défauts par certains cotés.
Tellement pas d'accord. Le parachute fait partie intégral de la pratique. Le moment sous voile est agréable, c'est beau, c'est lent, c'est rapide, c'est très rapide, c'est loin des autres, c'est proche des autres, c'est posé sur la cible, c'est posé hors zone. C'est une sous discipline a part entière qui me procure autant de plaisir que la partie chute.
Qu'entends-tu par union sécurisée? Une union avec de la "RTTI"?
Exemple :
dataObject=SquareFloat|RectangleFloatFloat
Ici, le type Object peut-être soit un carré Square avec un argument Float (le coté), soit un Rectangle avec deux arguments Float (les deux cotés). Pour faire la difference entre les différents objets, tu es forcé de "pattern match", examples :
Ici tu ne peux pas te tromper, tu ne peux pas utiliser un carré à la place d'un rectangle.
Example plus avancé, si tu veux faire une fonction qui peut ne pas renvoyer la valeur, tu fais sois appel à une valeur "sentinelle" soit tu passe par un booléen en retour. Exemple avec une fonction find qui te renvoie la position dans un tableau d'un élément.
// Valeur sentinelle, c'est l'approche stl qui renvoie std::end()// Si l’élément est trouvé, cela renvoi son index dans le vecteur// Sinon, cela renvoie "-1"template<typenameT>intfind(conststd::vector<T>&v,constTvalue);// Approche booléenne// Si l’élément est trouvé, cela renvoie true et met l'index dans `result`// Sinon, cela renvoi false et la valeur de result est indéterminétemplate<typenameT>boolfind(conststd::vector<T>&v,constTvalue,int&result);
Chaque approche a ses limitation. L'approche "sentinelle" fait que tu peux utiliser une valeur qui n'a aucun sens et l'approche Booléen fait que tu peux utiliser par erreur une valeur non initialisée.
En Haskell (et Rust, et OCaml, et sans doute d'autres), tu vas utiliser une "union typée safe" représentant le résultat où son absence :
dataMaybet=Nothing|Justt
Ici c'est un type polymorphique paramétré par t. Il peut donc contenir soit Nothing, soit Just une valeur de type t.
Maintenant la fonction find :
-- Cette fonction prend une liste de t `[t]`, une valeur-- de type `t` à trouver et renvoie `Just offset` si il est trouvé, ou alors `Nothing`find::[t]->t->MaybeInt
Lors de l'utilisation, tu n'as pas de valeur "sentinelle" abstraite ni de valeur non initialisée et c'est garantie par la compilation :
case(findmaListemaValeur)ofJustoffset->"La valeur est trouvée à l'offset: "++show(offset)Nothing->"Valeur non trouvée"
Ici la branche Nothing n'a pas le même scope que l'autre branche, ainsi tu ne peux pas utiliser offset dans la mauvaise branche.
Note que tu peux faire quelque chose de proche avec les exceptions dans de nombreux langage, mais ce n'est pas aussi propre car l'exception n’apparaît pas dans le type de la fonction et n'est pas forcement vérifiée par le compilateur.
Le prochain langage que j'apprendrais, ce sera sûrement Rust, il a pas mal de points intéressants à ce qu'il semble. (pas de GC, se base sur des techniques éprouvées, multi-paradigmes, pas d'héritage direct du C)
Rust est assez extra en effet. C'est pas aussi beau et pure que Haskell à mon sens, mais c'est un beau compromis.
(Je fournis des examples si vous voulez).
Je suis preneur, à titre de curiosité.
C'était les exemples de compositions. Continuons sur le Maybe que j'ai décris quelques lignes plus haut. On va supposer que nous avons une fonction getResult :: Int -> Maybe Int qui renvoie peut-être un Int. Tu veux renvoyer un Maybe contenant le résultat multiplié par deux ou Nothing si le résultat n'existe pas. Une implementation naive serait :
Cela devient vite lourd. Mais tu as pleins de fonction pour composer directement dans le Maybe, comme la fonction fmap ou <$> ce qui à la place donne de façon totalement équivalente :
(*2)<$>(getResultv)
Cela te permet de remonter au cran d'au dessus la gestion de l’échec. Malheureusement cela peut vite devenir un peu l'horreur si tu dois combiner plusieurs fonctions. Example, tu as 3 fonctions pour gérer tes utilisateurs qui associe leur Id à leur nom, prénom et leur salaire. Toutes ces fonctions peuvent échouer pour une raison ou une autre. Tu veux une fonction qui connaissant un Id te retourne une String présentant l'employé, ou Nothing si ce n'est pas possible :
Ici le <- signifie de sortir le résultat du Maybe où d’arrêter le calcul directement si celui-ci contient un Nothing. Cette syntaxe est polymorphique et dépend du type manipulé à condition que celui-ci correspond à une interface (la fameuse Monad). Comme cela tu peux faire une API très propre, tu pourrais imaginer une API de réseau :
Sachant que chaque fonction peut échouer sur le chemin en renvoyant un type d'erreur, tu peux crée ta fonction de plus haut niveau getRessource qui te renverra soit l'erreur soit la ressource finale, sans devoir écrire la tuyauterie et l’enchaînement de cas pour gérer toutes les erreurs.
Perso, je pense que quand on fait une fonction qui prend en paramètre un jeu de constantes, le plus simple est de définir une enum qui les définit, et si il s'avère qu'un utilisateur normal (du logiciel et non de la lib) peut impacter dessus via de la config, alors on file une fonction de conversion string<->enum.
Oui ;) Et si on pouvait avoir des vrais enums typés (i.e: pas de faux int) ce serait mieux ;)
Par défaut c'était beaucoup dire, je faisais référence par exemple au fait que les tableaux sont souvent passés par référence, et par exemple en Python dans l'exemple donné par Guillaum une copie b = a d'un tableau ne crée pas une copie du tableau, juste une copie de la référence au tableau (ce qui peut être surprenant, car la même syntaxe crée une vraie copie pour les nombres).
Techniquement ce n'est pas = qui est en faute ici, mais la sémantique de += ;)
Je pense que c'est justement là le point le plus difficile en programmation: exposer une interface assez simple et claire pour être compréhensible rapidement. Je doute que changer de langage change ça, c'est plus du niveau de l'architecture logicielle?
Le langage peut aider avec certaines construction syntaxiques. Par example, l'absence de type algébriques en C++, (i.e. des unions "safe") ne permet pas de facilement retourner un "optional". Boost et c++17 proposent des optionals, mais ils sont limités car il n'y a pas de pattern matching et donc tu peux te tromper facilement.
En Haskell ou Rust ou OCaml, tu peux retourner un Maybe, un type qui contient soit ta valeur de retour Just v soit Nothing et pour t'en servir tu es forcé de pattern matché dessus. C'est simple, clair, sécurisé et sympatique, et il faut un support du langage pour. Après tu ajoutes la syntaxe do en Haskell qui, de ce que je sais, est plutôt unique dans son genre, et tu peut t'exprimer encore plus clairement. (Je fournis des examples si vous voulez).
Ce que je veux dire c'est que si l'interface est pas claire, tu risque de passer un pire moment en Haskell (pas forcément non plus), mais surtout que même si elle est bien faite, il y a plus de chances qu'elle fasse appel à des notions théoriques nouvelles pour être comprise (par exemple la notion d'Arrow permet de voire certaines choses comme faire un filtre sur du xml de façon assez élégante, mais on comprend pas immédiatement pourquoi ça peut être bien, d'ailleurs je ne me souviens plus très bien).
Oui, c'est vrai, mais il n'est pas forcement nécessaire de comprendre une abstraction pour s'en servir et l'utiliser. Prend l'exemple des parseurs type Parsec, je trouve la syntaxe plutôt simple… Pourtant derrière cela utilise des Monades plutôt trapues.
Et puis après, d'expérience, j'ai l'impression que plus un langage propose des moyens de documenter automatiquement et formellement (signatures des fonctions, etc.), moins il y a d'exemples dans la doc en moyenne (c'est loin d'être un théorème ce que je dis là, juste une impression et qui a sans doute plus à voir avec les habitudes et traditions du public du langage en question, il y a des exceptions).
Je suis malheureusement d'accord avec toi, et surtout, en tout cas dans le cas d'Haskell, les auteurs pensent que cracher une phrase genre "Cette libraire gère des endo functors polymorphiques de rang 4 isomorphiques à des bifunctors tribales, cf. types." va aider ;)
Mais clairement le même problème existe dans de nombreuses librairies de la vraie vie en python ou en c++. Dernièrement avec une librairie standard de l'industrie, une fonction qui prend en paramètre une chaîne de caractère pouvant avoir 10 valeurs différentes. Pas documentée. Je dois regarder le code source du binding python pour voir que la chaîne est utilisée tel qu'elle pour construire une constante, genre constante = CONSTANTES["MODE_" + laChaine] et passée à la lib C++ associée qui n'est pas documentée pour cette fonction si ce n'est qu'elle prend une constante int en paramètre. Je regarde le code source de la lib C++ et après être descendu de 3 appels de fonction, je trouve enfin la comparaison avec des constantes et grâce à la lecture du code j'en deduis celle qui fait ce que je veux… J'aurais eu une fonction non documenté mais avec un type sérieux, j'aurais été plus heureux ;)
Tout n'est pas rose dans la surcharge d'opérateurs.
C'est pour cela que certains langages (Comme Haskell) interdisent la surcharge d’opérateurs et propose, si tu penses que certains types méritent de partager une interface commune, de définir une interface et des lois pour les types qui suivent cette interface. Haskell ne garantie pas que les lois soient respectés, mais la plupart des auteurs de librairies s'y collent.
for(auto &&item : etudiants), tu aurais pu écrire for(auto const& item : etudiants). C'est ce que j'aurai fait en fait. Ça aurait permis de pouvoir réutiliser la liste après les yeux fermés, parce que là j'ai un doute sur l'état des éléments de etudiants après coup.
Oui, erreur de ma part, même si je fais beaucoup d'effort pour ne pas oublier de const, j'en oublie toujours. Preuve que cela devrait être pensé dans l'autre sens ;)
Tu soulèves des points importants que je veux discuter ;) J'aime discuter, il est Dimanche, mon fils dors, j'ai rien à faire, alors discutons ;)
Juste une chose, je ne défend pas Haskell bec et ongles, je gagne ma vie en faisant du C++, j'ai contribué et je contribue encore a Python où à des projets en Python et j'apprécie Haskell. La suite de ce message peut donner l'impression que j'essaye de montrer qu'Haskell est bien meilleurs que ces langages, je ne le pense pas, je pense juste que c'est different, mais en répondant point à point à ton message, on est tenté de faire des comparaisons, j’espère donc ne pas être jugé comme un extrémiste Haskell ;)
D'un autre côté, Haskell n'est pas le langage fonctionnel le plus simple pour commencer. OCaml est par exemple bien plus accessible (afficher un "Hello world" ne fait pas apparaître des concepts compliqués, contrairement à Haskell avec la monade IO).
(Je ne connais pas du tout OCaml, mais bon…, je viens de jeter un œil, mais le hello world Ocaml print_string "Hello World!\n";; me fait peur avec ses deux ;; alors que le hello world Haskell main = putStrLn "Hello World" me fait moins peur et n'introduit aucun concept compliqué… Je troll, mais bon, j'essaye de montrer à quel point tous les arguments de cette discussion sont subjectifs.)
J'ai vraiment l'impression que Haskell fait peur alors qu'il ne devrait pas. Sans doute à cause de sa réputation de langage pour Matheux et une communauté qui n'a pas vraiment la motivation pour rendre cela accessible. J'ai aussi l'impression que beaucoup de gens oublient les concepts avancés qu'ils ont du comprendre pour écrire leur première ligne de code dans n'importe quel langage. Blague à part, je n'ai jamais fais de OCaml car en voyant que tous les étudiants de prépas "prestigieuses" en faisait, j'ai eu l'impression que c'était un langage théorique pour matheux, surtout sachant que c'était poussé par l'INRIA. J'ai donc choisis Haskell qui me semblait moins élitiste du fait de mon historique ;)
On peut écrire beaucoup de code sans rien comprendre aux monades, sans rien comprendre à IO, à partir du moment où tu acceptes sans te poser de question que pour récupérer la valeur de retour d'une fonction à effet de bord, c'est <- et pas = et que les fonctions qui font des IO retournent leur résultat avec return.
Dans un hello world C++, avant de pouvoir faire quoi que ce soit, tu dois faire au moins un include de iostream. Tu vas devoir mettre explicitement des types alors que tu ne sais pas forcement ce que cela signifie. Tu vas devoir utiliser des namespace (std::cout ou std::cin) ou le trop classique using namespace std. Tu vas devoir utiliser les opérateurs << et >>, et j'en passe. Tout autant de concepts compliqués que tu vas accepter en tant que débutant.
je n'ai pas toujours l'impression que les fold_left apportent beaucoup par rapport à une boucle.
J'étais initialement d'accord avec toi. Finalement, je trouve agréable le fait qu'un fold représente vraiment une opération de plus haut niveau. Puis une boucle, c'est ENORMEMENT de concepts d'un coup. Aux heures sombres de ma vie, j'ai enseigné à l'IUT, la fac et en école d'ingénieur à des groupes de débutant et je peux t'assurer que les boucles ce n'est pas une mince affaire, les étudiants oublient de l'initialiser, d’incrémenter l'iterateur, ne comprennent pas ce que cela fait vraiment.
Dans le cas de Haskell, c'est aussi un peu un terrain de prototypage pour plein d'idées sur les système de types, du coup il y a plein d'extensions, c'est complexe et on se cogne à un moment ou à un autre avec des concepts théoriques.
À l'inverse, beaucoup de dev C++ se cassent les dents sur certains concepts "théoriques" spécifiques au langage comme l'abondance de constructeurs / destructeurs spécifiques et les subtilités de leur implémentation. Je cite l'exemple de python sur la mutabilité des objets :
Ça c'est deux bonnes heures d'explication à des étudiants en stress énorme… Même après plus de 12 ans de python, je me plante encore des fois sur ce genre de bêtise…
Bref, là où je veux en venir c'est que je suis d'accord Haskell est un language complexe, comme beaucoup, mais pas forcement plus compliqué à comprendre que d'autre, c'est juste que les problèmes sont autre part.
Ceci dit, ce qui en pratique freine le plus sont des choses pas vraiment liées aux langages en soi : le fait que le langage offre des possibilités complexes ne veut pas dire que faire des choses simples va devenir compliqué. Mais en prenant un livre un peu au hasard sur Lisp ou Haskell, il y a de bonnes chances qu'une bonne partie du livre, surtout l'introduction, soit dédiée à expliquer pourquoi le langage est si génial (super macros ou super système de types), ce qui a de bonnes chances d'arrêter quelqu'un qui n'a pas assez de patience ou de temps pour arriver au stade où il peut écrire des choses utiles de base avant de faire des choses géniales. Et puis même une fois les bases du langage maîtrisées, c'est facile de tomber sur un package documenté à l'aide seul de la signature de ses fonctions et une ligne de description pour chacune (ou zéro), et c'est pas souvent que l'on trouve des exemples tout prêts qui permettent d'emblée de se faire une idée de comment on va utiliser le truc : on attend souvent de l'utilisateur qu'il étudie l'API et réfléchisse lui-même à la bonne façon d'agencer les types, et ça fait perdre du temps.
On en reviens au problème d’évangélisme et je suis d'accord avec toi. Mais ce n'est pas particulièrement une pathologie Haskell, comme partout, certaines libs sont bien documentées, d'autres non. Mais aux moins il y a des types expressifs qui aident et un compilateur qui gueule. Parce que les projets non documentés en python avec des API qui acceptent des chaînes de caractères à la place d'un Enum et pour lesquelles tu dois chercher dans le code source pour savoir quels sont les valeurs possibles et qui ne pètent pas le jour où la valeur choisie disparaît de la lib. Ou les API qui utilisent des types "fourre-tout", comme un vecteur à trois dimension, pour représenter des objets plus contraints, comme des directions ou des couleurs, il y en a partout et j'ai l'impression qu'en Haskell il y en a moins car les auteurs de librairies ont tendance aux types.
Il y a aussi le fait que ce n'est pas parce qu'on commence à comprendre Haskell et faire des choses utiles facilement que l'on va être capable de lire le code des autres (si ça se trouve, ils utilisent des Lens ou des Arrows ou autre nouveauté, et il faut potasser un peu de théorie avant de pouvoir faire quelque chose :)
Oui… Et non… Si l'abstraction est bien faite, tu peux comprendre ce que le code représente sans pour autant comprendre comment cela fonctionne. On fait cela tous les jours avec de nombreuses fonctions "simples" (Qui sait vraiment ce qui se passe lors d'un printf ?). Mais encore une fois, c'est un argument qui est valable dans n'importe quel langage. Une librairie C++ utilisant un peu de meta programmation template. Il faut énormément potasser de théorie pour avoir la moindre idée de ce qui est fait. Aller lire un peu de python "intelligent" où le code s'introspecte et se modifie tout seul en fonction du sens du vent. Aller lire un peu de javascript qui basé sur le nom de ta méthode injecte du html autour d'un tag dont la classe css correspond à ta méthode via un mapping auto généré selon des expression régulière… Il y a le même problème partout, seulement j'ai l'impression que quand quelqu'un n'est pas fan d'une technologie, il reproche ces problèmes a la technologie en question en ignorant que les autres souffrent de la même pathologie.
[^] # Re: On s'en bat le steak
Posté par Guillaum (site web personnel) . En réponse au journal Typage statique pour Python. Évalué à 7.
Je pensais comme cela il y a quelques temps. J'ai fais 7 ans de python acharné sur des projets libres ou sur mes projets persos et j'étais convaincu qu'une bonne couverture de test était la solution à tous les problèmes. J'ai fais des présentation en entreprise où à l'université à ce sujet. J'ai appliqué cela pendant 6 ans en entreprise. Puis un jour j'ai découvert que la couverture de test à 100 % n'existait pas, que souvent du code n'avait pas été documenté parce que pas le temps et que de toute façon, même avec des tests et de la documentation, le refactoring en python me péterait toujours à la gueule en production dans le petit bout de code bien tordu qui appelait la fonction que j'avais raté. C'est l'example donné dans la présentation sur mypy de ce journal, quand tu as une method
dosomethingsur un objet et que un grep dans ta base de code te donne 150 instances dedosomething, lequelles tu dois modifier ?Puis j'ai découverts ce qu'apporte le typage statique, en haskell principalement (mais j'ai fais un peu de rust et de ocaml). Quand tu refactor tu as une quantité d'erreur que tu ne peux plus faire du fait du typage statique. Un bon système de type évite aussi des erreurs immonde qui peuvent arriver avec un mauvais système de type (comme des casts implicites entre des matrices et des booléens, histoire vécue qui m'a fait perdre 10% de performance pendant 6 mois sur un logiciel "haute performance"). Un bon système de type évite aussi les null pointer exception. Un bon système de type te permet de modéliser avec les types et une fois que tu as trouvé les bonnes abstractions, tu peux écrire du code très rapidement. Et un bon système de type ne râle que quand tu fais des bêtises.
Après, un mauvais système de type, c'est juste lourd et contraignant, cela râle tout le temps pour rien, et malheureusement on se tape beaucoup de mauvais système de type comme example de ce qu'est le typage. Si votre idée d'un système de type est celle présentée dans Java ou C++, je vous invite vraiment à vous refaire une idée en allant voir ce qui se fait à coté. Rust est un bon point d'entrée, Haskell est énorme mais fait peur. Ocaml c'est sympa.
Bonne découverte.
[^] # Re: A force...
Posté par Guillaum (site web personnel) . En réponse au journal Typage statique pour Python. Évalué à 4.
Le système d'inférence ne fait pas d'hypothèse, seulement des observations et une propagation de contraintes.
Dans le cas de Haskell, si tu écris une fonction pgcd, supposons comme ceci :
(Note, je l'appelle
gcd'cargcdest déjà pris par une fonction qui fait la même chose… ;)Le type se déduit de façon suivante :
(>)surbet(>)est définie dans la typeclassOrdmod a b, le type demodestIntegral t => t -> t -> t, donc on conclue queaetbsont dans la type classeIntegral.Integralétant plus restrictif queOrd, alors la seule contrainte c'est queaetbsoient de typeIntegralgcd'est soit le même quegcd'(cas 1), soit le même quea(cas 2).On déduit donc le type suivant :
gcd' :: Integral t => t -> t -> t.À partir du moment où tu passes n'importe quel type qui implement
Integral, alors tu n'auras pas de soucis. Si tu passes un autre type, cela ne marchera pas, mais bon, c'est normal, ton type ne sait pas faire…Le seul problème ici c'est que, au sens de pas mal de monde,
Integralest trop restrictif, puisque que pour implementerIntegralil faut implementIntegralet toutes ses sous typeclass, dontReal,Enum,Num,OrdetEqavec, au minimum :Integral:quotRem(div et mod en gros),toIntegerReal:toRationalEnum:toEnum,fromEnumNum:(+),(*),abs,signum,fromInteger,negateou(-)Ord:(<=)Eq:(==)ou(/=)Ce qui fait beaucoup de bordel à implementer pour calculer un pgcd sur un type custom. La hiérarchie de type pour les nombres en Haskell est trop restrictive à mon gout, mais bon, personne n'est parfait ;) (En pratique cela ne pose rarement de problème sauf quand tu es un fou d'architecture par les types, mais à ce moment là Haskell n'est plus assez puissant pour tes besoins…)
De plus, il n'est pas obligatoire mais fortement conseillé de suivre des règles lors de l'implementation de ces classes (genre s'assurer que
a + b == a - (negate b)).Cependant, contrairement à de nombreux langage, ce mécanisme impose une certaine cohérence. Quand tu vois
a / b, tu sais que c'est l’opérateur/de la classeFractionalet tu peux t'attendre a un comportement. Ce n'est pas le cas en python ou C++ ou le/peut être surchargé en n'importe quoi. Par example, certains s'en servent pour concaténer des chemins de fichiers. C'est une restriction mais j'ai appris à l'aimer car cela rend le code très explicite.[^] # Re: typage statique automatique ?
Posté par Guillaum (site web personnel) . En réponse au journal Typage statique pour Python. Évalué à 4.
Il y a méprise. Ce qui est inferé c'est le type ou la classe (i.e. "interface" nécessaire d'une fonction). Mais par contre il faut bien dire ce que fait ton interface pour un type particulié. Par example si j'ai une interface
Surfaceavec une fonctionsurfaceet que j'ai un typeRectangle, il faut bien que j'écrive quelque part la fonctionsurfaceduRectangle… Par contre quand je vais écrire une fonction qui utilisesurface, son type sera inféré comme prenant un type d'interfaceSurfaceen paramètre.Oui et non.
Les types sommes sont une manière d'encapsuler plusieurs trucs dans un seul type, c'est grosso modo une union typé.
Tu peux aussi définir ta fonction dans une typeclass, c'est à dire qu'elle prendra un type qui match la typeclass (i.e. l'interface). Elle est donc plus ou moins polymorphique.
Elle peut aussi être polymorphique car on se fout des types que du passes. Example d'une fonction
swap (a, b) = (b, a), celle-ci prend un tuple de type(t, t')et retourne un tuple de type(t', t), en ce sens elle est polymorphique.[^] # Re: typage statique automatique ?
Posté par Guillaum (site web personnel) . En réponse au journal Typage statique pour Python. Évalué à 3.
Petite remarque pour ceux qui ne lisent pas l'haskell couramment. La fonction
afficherListeprend une liste deaet renvoie une String.amarche pour n'importe quelAffichable, mais il faut bien noter que la liste ne peut contenir qu'un seul type et le même.En gros, si on a
fooetbardes valeurs de typeFooetBar, eux même instances deAffichable, on peut faireafficheListe [foo, foo, foo]ouafficherListe [bar, bar, bar]mais pasafficherListe [foo, bar].En gros ce n'est pas l'équivalent des appels virtuels en C++ ou de l'héritage en python, tout est résolu à la compilation et une instance spécifique de
afficherListeest générée pour chaqueapossible. (Ce n'est pas complètement vrai ;)La notion de polymorphisme à l'execution (i.e. héritage virtuel) est plus complexe en haskell…
[^] # Re: typage statique automatique ?
Posté par Guillaum (site web personnel) . En réponse au journal Typage statique pour Python. Évalué à 2.
C'est vraiment des interfaces, en haskell cela se nomme des
typeclass. Sauf qu'elles sont définie de façon découplée du type associé, mais l'association est explicite.Par example, si je définis :
Et que j'ai le type :
Alors je peux associer la typeclass au type avec :
Tu n'as pas besoin de définir que la fonction prend un
Numou unBar, c'est l'inférence de type qui le fait tout seul (mais tu peux lui forcer la main en faisant quelque chose de plus restrictif). Donc en effet c'est en quelque sort du duck-typing statiquement typé. Example :aura comme type
foo :: Shape t => t -> FloatIci n'importe quelle type qui implémente l'interface
Shapefonctionne avec foo`.[^] # Re: typage statique automatique ?
Posté par Guillaum (site web personnel) . En réponse au journal Typage statique pour Python. Évalué à 5.
En pratique il y a l'inférence de type qui fait cela très bien. Dans un langage comme Haskell, tu peux ne jamais écrire un bout de type, mais avoir une inférence qui te calcul le type tout seul et le vérifie à la compilation.
Par example une fonction qui calcul la somme des éléments d'une liste :
Note, dans la vraie vie je ne l'écrirais pas comme cela, mais plutôt :
Enfin bon, le compilateur infère tout seul le type de
sumcomme étantNum a => [a] -> aoù dit autrement, une fonction qui prend une liste dea([a]) et renvoie unaavec la contrainte que leaest un truc qui suit une "interface"Num.De mon point de vue limité, cela donne la facilité d'écriture du code, tu ne te tracasses pas avec les types, mais au final tu as les outils du typage statique qui sont présents.
[^] # Re: Fonction
Posté par Guillaum (site web personnel) . En réponse au journal Plonk. Évalué à 2.
Et tout ces gens qui se moquent d'haskell ou autre à cause de la présence du mot-clé
letvont pouvoir maintenant aussi se moquer de javascript ;)[^] # Re: Le point de vue d'un développeur
Posté par Guillaum (site web personnel) . En réponse au journal Traduction des logiciels libres. Évalué à 7.
Pour le débat sur la traduction ponctuelle versus accompagnement longue durée, je suis tout à fait d'accord avec toi. Le monde de l'informatique est rempli de projet prometteur non maintenus.
Concernant ta vision de la différence entre langage de script et langage compilé je ne suis absolument pas d'accord avec toi pour plusieurs raisons.
En premier lieu, il n'y a pas que ELF dans la liste de dépendances d'un programme compilé, il y a une tonne de librairies qui peuvent changer d'ABI ou d'API et il y a aussi le compilateur et les librairies standards qui évoluent. Dernièrement, le passage à c++11 a cassé beaucoup de code. La librairie standard C++ a évoluée de façon non compatible avec l'existant. Pas plus tard que ce matin, j'ai fais évoluer la toolchain de notre entreprise de gcc 5 à gcc 6 et de clang 3.7 à clang 3.8 et j'ai réalisé une trentaine de commit lors de ce changement. Trois étaient nécessaires pour que cela compile !
Tu fais la différence entre langages compilés et langages de script avec un interpréteur, mais il existent de nombreux langages compilés qui ont un interpréteur (ou une VM, un runtime), comme Java, ou haskell, … La pluspart des langages "de script" que je connais sont compilées d'une manière où d'une autre. Python est compilé en byte-code intermédiaire pour sa VM mais il existe des projets de génération de code natif. Il existe des interpréteurs pour le C… Bref, la définition de langage de script est plus que floue.
Actuellement il est "simple" de garantir une unique base de code pour des technologies comme python, ruby, haskell, … qui fournissent des gestionnaire de dépendances dans lequels tu peux spécifier la version de l'intepreteur à utiliser ainsi que la version spécifique de toutes les dépendances. Tu es presque garantie que cela compilera / s'executera à l'identique. Et souvent tu n'as qu'une seul projet d'intepreteur à supporter.
D'un autre coté, en C++ par exemple, le roi des langages "compilé", il n'y a aucun consensus sur un outil de dependence / packaging / déploiement qui te donne les même garanties, il existe au moins 4 compilateurs différents que tu dois supporter (gcc / clang / visual studio / Intel cc) et des spécificité de programmation sur chaque système et des versions de compilateur différentes. Et crois moi (Je maintient une base de code C++ de plusieurs centaines de milliers de ligne qui tourne sur les 3 OS majeurs et sur plusieurs architecture matériel et sur les 4 compilateurs), on gère cela avec des directives de précompilation et c'est un enfer. Une des meilleure solution à l'heure actuelle c'est de distribuer ton logiciel dans une machine virtuelle, ou un conteneur léger (comme docker), mais ce n'est pas applicable à tous les projets.
Donc ma conclusion c'est que j'aimerais que on arrête de catégoriser certains outils dans ce groupe magique des "langages de script" qui est très péjoratif et souvent sur des arguments qui ne tiennent pas.
Petite histoire vraie vécue dans ma vraie vie de développeur. Etant à la R&D d'une boite, j'ai écris en python un prototype d'un outil. La solution a rapidement été validée et s'est posé la question de la mise en production du produit. Il fallait donc passer du prototype (100 lignes de python dans un seul fichier, pas de dépendance autre que l’interpréteur, portable sous linux / windows / mac OS) à la "vraie vie". Un ingénieur talentueux a donc réalisé le portage du langage de "script" en C++, parce que ce serait clairement plus rapide et plus robuste (compilé versus langage de script, y a pas photo non ?!). Quelques jour / homme plus tard, il revenait avec une version (quelques milliers de ligne de C++, qui ne marchait que dans visual studio, pas portable et avec 5 librairies en dépendance) et surtout un GROS problème. Quand ma version en python tournait en 2s, la sienne prenait 10 minutes. Impossible ! Bon, alors en fait, il avait utilisé un algo en O(N2) au lieu du mien qui était en O(N) pour la raison simple qu'il pensait que le C++ irait de toute manière plus vite. Une nouvelle dépendence plus tard (Pour avoir une hash map en C++ car il n'y en avait pas dans la standard libraire à cette époque, on aurait pu utiliser un std::set), son programme tournait en 3s… Toujours plus lent. Damned… Après profiling, c'était un problème d'allocation mémoire parce que l’algorithme créait énormément d'objet. Après avoir changé d'allocateur (à l'époque le gars a écrit un allocateur maison, mais bon, on aurait pu prendre un projet existant, je ne sais juste pas ce qui existait de bien à l'époque), le temps est passé à 2s. Pas plus rapide que la version python… Drame… En fait le temps était passé en entrée sortie avec le disque, donc il serait pas aller plus vite quoi qu'il arrive… Plus tard beaucoup de temps on été investis pour réaliser le portage de ce programme sous linux et sous mac os X et pour corriger les bugs dans l'allocateur custom. Encore plus tard, il a fallu corriger de nombreux problèmes liés à la portabilité sur différents compilateurs. Et pendant ce temps là la version python marchait toujours car elle servait de test de non régression ;)
Alors oui, ce cas est extrême, mais c'est l’exemple même de la peur des langages de "scripts". On peut reprocher beaucoup de choses à tous ces langages qui ne sont pas tout roses (python 3 ;), mais de là à dire que du code "compilé" à une meilleure durée de vie… Cela n'a rien à voir ;)
[^] # Re: Le point de vue d'un développeur
Posté par Guillaum (site web personnel) . En réponse au journal Traduction des logiciels libres. Évalué à 2.
Je supplie tout le monde de ne pas faire cette classification "langages de script". Après on se retrouve avec des gens qui pensent que le "script" (python, ruby, …) ne sert qu'à initialiser des vrais programmes (En Java / C / C++)… Je l'ai déjà vécu en école d’ingénieur et cela fait mal de se prendre une sale note parce "qu'un script n'est pas un programme professionnel".
D'autant que dans le contexte que tu donnes, la différence entre python et C d'un point de vu gettext est minimum…
[^] # Re: Visualiser les réductions
Posté par Guillaum (site web personnel) . En réponse au journal Haskell -- Évaluation paresseuse. Évalué à 4.
C'est le mieux que je connaisse pour l'instant :
http://chrisuehlinger.com/LambdaBubblePop/
[^] # Re: pour implémenter : enrober dans une fonction
Posté par Guillaum (site web personnel) . En réponse au journal Haskell -- Évaluation paresseuse. Évalué à 2.
Oui mais avec quelques subtilités. Principalement l'ordonnancement des opérations est garantie. En gros tu est certain que une utilisation de
s'te retourne la somme complète, et pas une somme partielle en milieu de route.[^] # Re: Dernier cas
Posté par Guillaum (site web personnel) . En réponse au journal Haskell -- Évaluation paresseuse. Évalué à 2.
Oui, l’implémentation doit être faite avec attention pour ne pas boucler.
Je n'arrive pas à reproduire ta boucle en copiant ton implémentation de
g. Es-tu certain que c'est bien celle que tu utilises ?Voici mon implémentation de
g:Ici, dans le cas 0, je traite la liste d'un seul élément. Le résultat c'est
[x / s]et la somme pour un unique élément c'estx.Dans le cas 1, je traite la liste de plus d'un élément. Je réalise d'abord un appel récursif
g s xspour récupérer le résultatressur la sous liste et la sommes'sur la sous liste. Puis je renvois le résultat courant(x / s) : res)et la somme mise à jourss' + x.[^] # Re: pour implémenter : enrober dans une fonction
Posté par Guillaum (site web personnel) . En réponse au journal Haskell -- Évaluation paresseuse. Évalué à 4.
Je viens de réaliser une explication d'une heure et j'ai fais un raccourcis clavier à la con sur mon navigateur et j'ai tout perdu… Je me déteste.
C'est autant un argument qu'un résultat. On peut s’intéresser à un cas très simple :
Ici
(:)représente l'ajout en tête d'une valeur sur une liste tel que1:2:[3,4] == [1,2,3,4]On peut appeler la fonction blork avec une valeur et voir son comportement :
J'ai crée une liste infinie de
1avec une expression qui s'auto-référence. L'évaluation paresseuse fait que la liste ne sera jamais évaluée, seul les éléments nécessaires le seront. Si je demande avectake 2les deux premiers éléments, il peux s’arrêter à1:1:lsans évaluer la suite.Oui. Mais de toute façon tu feras surement un traitement sur cette seconde liste, pour l'afficher par exemple. Les résultats seront évalués à ce moment. Il y a ce site que j'aime beaucoup qui permet d'observer les évaluations.
[^] # Re: pour implémenter : enrober dans une fonction
Posté par Guillaum (site web personnel) . En réponse au journal Haskell -- Évaluation paresseuse. Évalué à 2.
C'est marrant comment les seuls réponses que j'ai eu c'est "Je peux le faire en C / Javascript / Brainfuck".
J'ai vraiment du rater mon discours parce que je trouve le dernier point vraiment intéressant, mais il n'est pas discuté. Dommage.
Alors simplement que je ne meurs pas idiot, comment on peut faire l’exemple de structure de contrôle custom et l’exemple de parcours de liste en une fois en javascript ? A ma connaissance l'un est difficile / pas élégant / pas safe et l'autre est impossible, mais je me trompe surement.
[^] # Re: valeur vs pointeur
Posté par Guillaum (site web personnel) . En réponse au journal Haskell -- Évaluation paresseuse. Évalué à 4.
Tu es forcé de explicitement gérer toi même ces fonctions.
Si par exemple tu veux faire quelque chose du genre :
Sans connaitre le contenu de
a,betc, tu ne peux avoir aucune certitude sur le nombre d'appel aux fonctionslongCalculA,longCalculB.[^] # Re: astuce
Posté par Guillaum (site web personnel) . En réponse au journal Haskell -- Évaluation paresseuse. Évalué à 2.
Au départ c'est ce que je voulais faire, mais
+sne prend que le temps cpu en compte ;) Et comme ma fonctionlongIdest unthreadDelay, cela ne marche pas ;([^] # Re: pour implémenter : enrober dans une fonction
Posté par Guillaum (site web personnel) . En réponse au journal Haskell -- Évaluation paresseuse. Évalué à 2.
Ce que j'aime en haskell c'est le coté simple de la syntaxe à ce niveau, tu n'as pas à te préoccuper de qui va évaluer ton thunk, où qu'il soit évalué plusieurs fois.. Tu peux écrire une fonction qui marche sur n'importe quelle type de donnée que ce soit une donnée lazy ou pas.
Alors il est certain que cela a des défauts par certains cotés.
# Disposition clavier par érgonomique
Posté par Guillaum (site web personnel) . En réponse au journal SSH Tron. Évalué à 6.
Je ne comprend pas ces touches, elles sont placées n'importe où…
[^] # Re: Skydive...
Posté par Guillaum (site web personnel) . En réponse à la dépêche Skydive, un nouvel outil d’analyse de votre réseau. Évalué à 2.
Tellement pas d'accord. Le parachute fait partie intégral de la pratique. Le moment sous voile est agréable, c'est beau, c'est lent, c'est rapide, c'est très rapide, c'est loin des autres, c'est proche des autres, c'est posé sur la cible, c'est posé hors zone. C'est une sous discipline a part entière qui me procure autant de plaisir que la partie chute.
[^] # Re: Skydive...
Posté par Guillaum (site web personnel) . En réponse à la dépêche Skydive, un nouvel outil d’analyse de votre réseau. Évalué à 3.
Il me faut un sticker linuxfr à coller sur mon casque pour pouvoir se reconnaître entre moules volantes.
[^] # Re: Moi == pas doué, je suppose....
Posté par Guillaum (site web personnel) . En réponse au journal Haskell et le tri. Évalué à 5.
Exemple :
Ici, le type
Objectpeut-être soit un carréSquareavec un argumentFloat(le coté), soit unRectangleavec deux arguments Float (les deux cotés). Pour faire la difference entre les différents objets, tu es forcé de "pattern match", examples :Ici tu ne peux pas te tromper, tu ne peux pas utiliser un carré à la place d'un rectangle.
Example plus avancé, si tu veux faire une fonction qui peut ne pas renvoyer la valeur, tu fais sois appel à une valeur "sentinelle" soit tu passe par un booléen en retour. Exemple avec une fonction
findqui te renvoie la position dans un tableau d'un élément.Chaque approche a ses limitation. L'approche "sentinelle" fait que tu peux utiliser une valeur qui n'a aucun sens et l'approche Booléen fait que tu peux utiliser par erreur une valeur non initialisée.
En Haskell (et Rust, et OCaml, et sans doute d'autres), tu vas utiliser une "union typée safe" représentant le résultat où son absence :
Ici c'est un type polymorphique paramétré par
t. Il peut donc contenir soitNothing, soitJustune valeur de type t.Maintenant la fonction
find:Lors de l'utilisation, tu n'as pas de valeur "sentinelle" abstraite ni de valeur non initialisée et c'est garantie par la compilation :
Ici la branche
Nothingn'a pas le même scope que l'autre branche, ainsi tu ne peux pas utiliseroffsetdans la mauvaise branche.Note que tu peux faire quelque chose de proche avec les exceptions dans de nombreux langage, mais ce n'est pas aussi propre car l'exception n’apparaît pas dans le type de la fonction et n'est pas forcement vérifiée par le compilateur.
Rust est assez extra en effet. C'est pas aussi beau et pure que Haskell à mon sens, mais c'est un beau compromis.
C'était les exemples de compositions. Continuons sur le
Maybeque j'ai décris quelques lignes plus haut. On va supposer que nous avons une fonctiongetResult :: Int -> Maybe Intqui renvoie peut-être un Int. Tu veux renvoyer unMaybecontenant le résultat multiplié par deux ou Nothing si le résultat n'existe pas. Une implementation naive serait :Cela devient vite lourd. Mais tu as pleins de fonction pour composer directement dans le
Maybe, comme la fonctionfmapou<$>ce qui à la place donne de façon totalement équivalente :Cela te permet de remonter au cran d'au dessus la gestion de l’échec. Malheureusement cela peut vite devenir un peu l'horreur si tu dois combiner plusieurs fonctions. Example, tu as 3 fonctions pour gérer tes utilisateurs qui associe leur
Idà leur nom, prénom et leur salaire. Toutes ces fonctions peuvent échouer pour une raison ou une autre. Tu veux une fonction qui connaissant un Id te retourne uneStringprésentant l'employé, ou Nothing si ce n'est pas possible :C'est lourd. Il y a d'autres solutions, mais rien de bien sympathique, sauf la syntaxe
doqui permet d'exprimer cela :Ici le
<-signifie de sortir le résultat duMaybeoù d’arrêter le calcul directement si celui-ci contient unNothing. Cette syntaxe est polymorphique et dépend du type manipulé à condition que celui-ci correspond à une interface (la fameuseMonad). Comme cela tu peux faire une API très propre, tu pourrais imaginer une API de réseau :Sachant que chaque fonction peut échouer sur le chemin en renvoyant un type d'erreur, tu peux crée ta fonction de plus haut niveau
getRessourcequi te renverra soit l'erreur soit la ressource finale, sans devoir écrire la tuyauterie et l’enchaînement de cas pour gérer toutes les erreurs.Oui ;) Et si on pouvait avoir des vrais enums typés (i.e: pas de faux int) ce serait mieux ;)
[^] # Re: Moi == pas doué, je suppose....
Posté par Guillaum (site web personnel) . En réponse au journal Haskell et le tri. Évalué à 3.
Techniquement ce n'est pas
=qui est en faute ici, mais la sémantique de+=;)Pour la suite je répond au deux ;)
Le langage peut aider avec certaines construction syntaxiques. Par example, l'absence de type algébriques en C++, (i.e. des unions "safe") ne permet pas de facilement retourner un "optional". Boost et c++17 proposent des optionals, mais ils sont limités car il n'y a pas de pattern matching et donc tu peux te tromper facilement.
En Haskell ou Rust ou OCaml, tu peux retourner un
Maybe, un type qui contient soit ta valeur de retourJust vsoitNothinget pour t'en servir tu es forcé de pattern matché dessus. C'est simple, clair, sécurisé et sympatique, et il faut un support du langage pour. Après tu ajoutes la syntaxedoen Haskell qui, de ce que je sais, est plutôt unique dans son genre, et tu peut t'exprimer encore plus clairement. (Je fournis des examples si vous voulez).Oui, c'est vrai, mais il n'est pas forcement nécessaire de comprendre une abstraction pour s'en servir et l'utiliser. Prend l'exemple des parseurs type
Parsec, je trouve la syntaxe plutôt simple… Pourtant derrière cela utilise des Monades plutôt trapues.Je suis malheureusement d'accord avec toi, et surtout, en tout cas dans le cas d'Haskell, les auteurs pensent que cracher une phrase genre "Cette libraire gère des endo functors polymorphiques de rang 4 isomorphiques à des bifunctors tribales, cf. types." va aider ;)
Mais clairement le même problème existe dans de nombreuses librairies de la vraie vie en python ou en c++. Dernièrement avec une librairie standard de l'industrie, une fonction qui prend en paramètre une chaîne de caractère pouvant avoir 10 valeurs différentes. Pas documentée. Je dois regarder le code source du binding python pour voir que la chaîne est utilisée tel qu'elle pour construire une constante, genre
constante = CONSTANTES["MODE_" + laChaine]et passée à la lib C++ associée qui n'est pas documentée pour cette fonction si ce n'est qu'elle prend une constante int en paramètre. Je regarde le code source de la lib C++ et après être descendu de 3 appels de fonction, je trouve enfin la comparaison avec des constantes et grâce à la lecture du code j'en deduis celle qui fait ce que je veux… J'aurais eu une fonction non documenté mais avec un type sérieux, j'aurais été plus heureux ;)[^] # Re: merci
Posté par Guillaum (site web personnel) . En réponse au journal Haskell et le tri. Évalué à 2.
Non, j'ai inversé l'ordre des champs ;)
[^] # Re: Moi == pas doué, je suppose....
Posté par Guillaum (site web personnel) . En réponse au journal Haskell et le tri. Évalué à 4.
C'est pour cela que certains langages (Comme Haskell) interdisent la surcharge d’opérateurs et propose, si tu penses que certains types méritent de partager une interface commune, de définir une interface et des lois pour les types qui suivent cette interface. Haskell ne garantie pas que les lois soient respectés, mais la plupart des auteurs de librairies s'y collent.
Oui, erreur de ma part, même si je fais beaucoup d'effort pour ne pas oublier de const, j'en oublie toujours. Preuve que cela devrait être pensé dans l'autre sens ;)
[^] # Re: Moi == pas doué, je suppose....
Posté par Guillaum (site web personnel) . En réponse au journal Haskell et le tri. Évalué à 5.
Tu soulèves des points importants que je veux discuter ;) J'aime discuter, il est Dimanche, mon fils dors, j'ai rien à faire, alors discutons ;)
Juste une chose, je ne défend pas Haskell bec et ongles, je gagne ma vie en faisant du C++, j'ai contribué et je contribue encore a Python où à des projets en Python et j'apprécie Haskell. La suite de ce message peut donner l'impression que j'essaye de montrer qu'Haskell est bien meilleurs que ces langages, je ne le pense pas, je pense juste que c'est different, mais en répondant point à point à ton message, on est tenté de faire des comparaisons, j’espère donc ne pas être jugé comme un extrémiste Haskell ;)
(Je ne connais pas du tout OCaml, mais bon…, je viens de jeter un œil, mais le hello world Ocaml
print_string "Hello World!\n";;me fait peur avec ses deux;;alors que le hello world Haskellmain = putStrLn "Hello World"me fait moins peur et n'introduit aucun concept compliqué… Je troll, mais bon, j'essaye de montrer à quel point tous les arguments de cette discussion sont subjectifs.)J'ai vraiment l'impression que Haskell fait peur alors qu'il ne devrait pas. Sans doute à cause de sa réputation de langage pour Matheux et une communauté qui n'a pas vraiment la motivation pour rendre cela accessible. J'ai aussi l'impression que beaucoup de gens oublient les concepts avancés qu'ils ont du comprendre pour écrire leur première ligne de code dans n'importe quel langage. Blague à part, je n'ai jamais fais de OCaml car en voyant que tous les étudiants de prépas "prestigieuses" en faisait, j'ai eu l'impression que c'était un langage théorique pour matheux, surtout sachant que c'était poussé par l'INRIA. J'ai donc choisis Haskell qui me semblait moins élitiste du fait de mon historique ;)
On peut écrire beaucoup de code sans rien comprendre aux monades, sans rien comprendre à
IO, à partir du moment où tu acceptes sans te poser de question que pour récupérer la valeur de retour d'une fonction à effet de bord, c'est<-et pas=et que les fonctions qui font des IO retournent leur résultat avecreturn.Dans un hello world C++, avant de pouvoir faire quoi que ce soit, tu dois faire au moins un include de
iostream. Tu vas devoir mettre explicitement des types alors que tu ne sais pas forcement ce que cela signifie. Tu vas devoir utiliser des namespace (std::coutoustd::cin) ou le trop classiqueusing namespace std. Tu vas devoir utiliser les opérateurs<<et>>, et j'en passe. Tout autant de concepts compliqués que tu vas accepter en tant que débutant.J'étais initialement d'accord avec toi. Finalement, je trouve agréable le fait qu'un fold représente vraiment une opération de plus haut niveau. Puis une boucle, c'est ENORMEMENT de concepts d'un coup. Aux heures sombres de ma vie, j'ai enseigné à l'IUT, la fac et en école d'ingénieur à des groupes de débutant et je peux t'assurer que les boucles ce n'est pas une mince affaire, les étudiants oublient de l'initialiser, d’incrémenter l'iterateur, ne comprennent pas ce que cela fait vraiment.
À l'inverse, beaucoup de dev C++ se cassent les dents sur certains concepts "théoriques" spécifiques au langage comme l'abondance de constructeurs / destructeurs spécifiques et les subtilités de leur implémentation. Je cite l'exemple de python sur la mutabilité des objets :
Ça c'est deux bonnes heures d'explication à des étudiants en stress énorme… Même après plus de 12 ans de python, je me plante encore des fois sur ce genre de bêtise…
Bref, là où je veux en venir c'est que je suis d'accord Haskell est un language complexe, comme beaucoup, mais pas forcement plus compliqué à comprendre que d'autre, c'est juste que les problèmes sont autre part.
On en reviens au problème d’évangélisme et je suis d'accord avec toi. Mais ce n'est pas particulièrement une pathologie Haskell, comme partout, certaines libs sont bien documentées, d'autres non. Mais aux moins il y a des types expressifs qui aident et un compilateur qui gueule. Parce que les projets non documentés en python avec des API qui acceptent des chaînes de caractères à la place d'un Enum et pour lesquelles tu dois chercher dans le code source pour savoir quels sont les valeurs possibles et qui ne pètent pas le jour où la valeur choisie disparaît de la lib. Ou les API qui utilisent des types "fourre-tout", comme un vecteur à trois dimension, pour représenter des objets plus contraints, comme des directions ou des couleurs, il y en a partout et j'ai l'impression qu'en Haskell il y en a moins car les auteurs de librairies ont tendance aux types.
Oui… Et non… Si l'abstraction est bien faite, tu peux comprendre ce que le code représente sans pour autant comprendre comment cela fonctionne. On fait cela tous les jours avec de nombreuses fonctions "simples" (Qui sait vraiment ce qui se passe lors d'un
printf?). Mais encore une fois, c'est un argument qui est valable dans n'importe quel langage. Une librairie C++ utilisant un peu de meta programmation template. Il faut énormément potasser de théorie pour avoir la moindre idée de ce qui est fait. Aller lire un peu de python "intelligent" où le code s'introspecte et se modifie tout seul en fonction du sens du vent. Aller lire un peu de javascript qui basé sur le nom de ta méthode injecte du html autour d'un tag dont la classe css correspond à ta méthode via un mapping auto généré selon des expression régulière… Il y a le même problème partout, seulement j'ai l'impression que quand quelqu'un n'est pas fan d'une technologie, il reproche ces problèmes a la technologie en question en ignorant que les autres souffrent de la même pathologie.