Journal Typage statique pour Python

Posté par . Licence CC by-sa
Tags :
31
30
mai
2016

https://www.dropbox.com/s/efatwr0pozsargb/PyCon%20mypy%20talk%202016.pdf?dl=0

Une équipe de dropbox, dont GvR fait partie, présente l'état des lieux du typage statique en Python. La présentation est très complète avec le pourquoi, le comment, l'historique et le futur.

Le pourquoi : à des fins de documentation, pour trouver des bugs, pour les perfs, pour les besoins de Dropbox (perf et upgrade du code de py2 à py3)
Le comment : la syntaxe, possible en py3 et py2, utilisable tout de suite avec http://mypy-lang.org/ dynamique toujours possible par défaut
L'historique : depuis 1998, avec les travaux de Jim Fulton (zope.interface)
Le futur : performance, intégration avec les éditeurs, inférence de type etc…

Sur le coup j'ai cru que c'était une présentation de Go ! Sans dec, je suis vraiment impressionné par cette approche graduelle, cette évolution mûrement réfléchie et sans précipitation.

Je pense que cela va aider à migrer du code vers py3, mais n'ayant pas encore essayé moi-même je n'en dirai pas plus…

  • # A force...

    Posté par (page perso) . Évalué à -10.

    A force de reprendre toutes les idées du C/C++, on va se retrouver avec du C/C++… pas plus joli, pas plus réfléchi, juste à la forme différente.

    def gcd(a: int, b: int) -> int:
    while b:
        a, b = b, a % b
    return a
    

    Ca vous semble vraiment plus lisible / beau / mettre ici tout adjectif hype que :

    int gcd(int a, int b)
    {
      int c;
      while ( a ) {
         c = a; a = b%a;  b = c;
      }
      return b;
    }
    

    J'ai l'impression surtout qu'on a posé une lib standard et remplacé des point-virgules par des tabulation/retour à la ligne forcés, à force de vouloir un "nouveau langage différent mais qu'on veut rapprocher des langage d'avant finalement car en fait ça marche pas si bien que ça d'être différent plus simple".

    PS : faudrait pour le fun que je benche les deux algos, pour voir si Python arrive à s'approcher de la perf de C.

    • [^] # Re: A force...

      Posté par (page perso) . Évalué à 10.

      J'ai l'impression surtout qu'on a posé une lib standard et remplacé des point-virgules par des tabulation/retour à la ligne forcés, à force de vouloir un "nouveau langage différent mais qu'on veut rapprocher des langage d'avant finalement car en fait ça marche pas si bien que ça d'être différent plus simple".

      Impression fausse. Ça reste du "duck typing" à la base avec un langage de script très dynamique, ça marche bien mais pour quoi c'est fait, mais c'est améliorable… d'où l'ajout des annotations de types. Mais elles restent optionnelles, et ne sont pour le moment pas utilisées à l'exécution.

      PS : faudrait pour le fun que je benche les deux algos, pour voir si Python arrive à s'approcher de la perf de C.

      Et le C va évidemment écraser le Python pour peu qu'il y ait quelques boucles avec des expressions de calcul. On n'utilise pas Python pour des perfs en calcul (pour ça on code en C/C++/Fortran et on plug du Python par dessus pour les enchaînements de haut niveau, les synchros multiprocess, les logs, etc).

      Python 3 - Apprendre à programmer en Python avec PyZo et Jupyter Notebook → https://www.dunod.com/sciences-techniques/python-3

    • [^] # Re: A force...

      Posté par . Évalué à 4.

      Pour ce qui est des performances, l'optimisation du code grâce au typage statique est prévue dans le futur, pour l'instant ça ne change rien.

      Cela dit, je doute qu'ils arrivent aux performances du C et comme tu dis, s'ils veulent du C, ils n'ont qu'à écrire du C.

      De mémoire (pas de référence, désolé), Clojure a fait pareil: tout le monde s'est mis à vouloir ajouter des annotations de types parce que "ah ben en fait, le typage statique c'est utile", puis au bout d'une paire d'années, beaucoup en ont eu marre, ça demandait trop d'effort, rendait le code moins compréhensible, pour un gain faible.

      Y a-t-il des langages où le typage statique est optionnel et pour lesquels cela a été un succès?

      • [^] # Re: A force...

        Posté par . Évalué à 8.

        rendait le code moins compréhensible

        Comment ça ? Je comprend pas.  Justement il y a bien moins d’ambigüité .

        • [^] # Re: A force...

          Posté par . Évalué à 0.

          Le choix de l'adjectif est mauvais, je pense que j'aurais voulu dire "lisible", "aéré". Bref, c'est plus chargé visuellement et c'est plus difficile à lire.

          (à part ça, je comprends l'intérêt du typage explicite dans d'autres langages)

          • [^] # Re: A force...

            Posté par . Évalué à 2.

            Mouai, sachant que la déclaration est fait au début ça ne surcharge pas tant que ça l'écriture. Pas convaincu pour le coup

      • [^] # Re: A force...

        Posté par . Évalué à 3.

        Y a-t-il des langages où le typage statique est optionnel et pour lesquels cela a été un succès?

        Objective-C : il y a le type id qui veut dire « n’importe quel objet ». En pratique, les développeurs préfèrent généralement typer explicitement.

        • [^] # Re: A force...

          Posté par . Évalué à 2.

          Le compilo te gueule beaucoup dessus si tu fais ça, ça aide pas.
          Et ça aide encore moins si tu traites les warnings comme des erreurs.

          Ya un débat un peu à la con en ce moment chez les anciens, sur le typage strict de Swift, comme quoi c'est la fin du monde par rapport à la flexibilitéWW les bugs dormants d'objective c, donc j'imagine qu'il en a qui utilisent régulièrement le kvo, id et autres horreurs de ce genre.

          Linuxfr, le portail francais du logiciel libre et du neo nazisme.

          • [^] # Re: A force...

            Posté par . Évalué à 2. Dernière modification le 31/05/16 à 09:56.

            Le compilo te gueule beaucoup dessus si tu fais ça, ça aide pas.

            Ça fait des années que j’ai plus touché à Objective-C, mais à l’époque non seulement ça ne faisait aucun warning, mais en plus c’était indispensable pour certaines constructions (+(id)alloc et -(id)init, de tête)

            • [^] # Re: A force...

              Posté par . Évalué à 2.

              -alloc et -init sont des fonctions "magiques" dans le sens où le compilo les connais et peut les traiter à part (Objc est plein de trucs comme ça).

              Appeler une fonction sur id, c'est bon (tant que le selector est définit qq part dans un .h en scope). Par contre passer un id en paramètre, ou le retourner, en place d'un truc type différemment (en gros, downcast implicite), et tu te prends un warning il me semble.

              Notes aussi que tout coder en id peut jouer des tours assez vilains à bas niveau, si ton sélecteur rentre en conflit avec un autre du même nom, mais qui retourne un struct plutôt qu'un pointeur. Le compilo peut générer du mauvais code dans ce cas, en fonction duquel il choisi.

              Linuxfr, le portail francais du logiciel libre et du neo nazisme.

    • [^] # Re: A force...

      Posté par . Évalué à 8.

      J'ai essayé mais le point blocant pour moi est qu'il faut mixer les déclarations dans le code et les déclarations dans les commentaires ce qui oblige à :

      1. Regarder à deux endroits différents.
      2. Détourner les commentaires (qui font maintenant partis de la sémantique du code).

      Ça laisse un goût de proposition à l'arrache où l'on joint les deux bouts comme on peut.

    • [^] # Re: A force...

      Posté par . Évalué à 1. Dernière modification le 30/05/16 à 14:38.

      Ou alors ils pourraient implémenter un système d'inférence de type à la Hindley-Milner ce qui leur permettrait de faire du duck-typing avec un typage statique et fort. :-)

      (* définition avec annotations *)
      let rec pgcd (a:int) (b:int):int =
        match a mod b with
        | 0 -> b
        | r -> pgcd b r
      ;;
      val pgcd : int -> int -> int = <fun>
      
      (* la même sans les annotations *)
      let rec pgcd' a b =
        match a mod b with
        | 0 -> b
        | r -> pgcd' b r
      ;;
      val pgcd' : int -> int -> int = <fun>
      (* le système d'inférence s'en est sorti tout seul et a su typer correctement la fonction *)
      
      (* je ne sais plus le type de la fonction, je demande à la boucle REPL *)
      pgcd;;
      - : int -> int -> int = <fun>

      Sapere aude ! Aie le courage de te servir de ton propre entendement. Voilà la devise des Lumières.

      • [^] # Re: A force...

        Posté par . Évalué à 3.

        faire du duck-typing avec un typage statique

        C'est ce qu'on appel le typage structurel, non ?

        Tous les contenus que j'écris ici sont sous licence CC0 (j'abandonne autant que possible mes droits d'auteur sur mes écrits)

        • [^] # Re: A force...

          Posté par . Évalué à 2. Dernière modification le 30/05/16 à 16:03.

          D'après ce que wikipédia dit des systèmes structurels de types cela a l'air de correspondre.

          En tout cas, moi je trouve la syntaxe concrète plus esthétique. Je ne trouve pas que le recours aux accolades pour délimiter les blocs et l'usage du point-virgule a tout bout de champ soit particulièrement joli. De plus, mettre un objet de type int dans une expression booléenne est surprenant sur le plan du typage (la transtypage 0 = false et true pour les entiers non nuls a son charme pour certain, moi je n'aime pas le mélange des genres). Le code qui s'approche de la manipulation des registres dans sa forme, c'est assez bas niveau : je préfère celui qui mime le principe même de l'algorithme d'Euclide pour le calcul du pgcd (on calcul le reste de la division euclidienne des opérandes, s'il est nul on renvoie le diviseur, sinon on poursuit avec le calcul du pgcd du diviseur et du reste : la traduction c'est l'affaire du compilateur). Enfin niveau performance, si l'on compile en natif, on se retrouve proche du C/C++.

          Sapere aude ! Aie le courage de te servir de ton propre entendement. Voilà la devise des Lumières.

      • [^] # Re: A force...

        Posté par . Évalué à 1.

        Mais que se passe-t-il si je donne à la fonction pgcd sans annotations des objets pour lesquels sont définis les opérations mod et comparaison à 0 (une classe polynôme par exemple) ? Est ce que le systême d'inférence va me mettre des bâtons dans les roues parce qu'il a fait de mauvaise hypothèses ?

        • [^] # Re: A force...

          Posté par (page perso) . É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 :

          gcd' a b
            | b > 0 = gcd' b (mod a b) -- 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.

        • [^] # Re: A force...

          Posté par . Évalué à 1.

          Mais que se passe-t-il si je donne à la fonction pgcd sans annotations des objets pour lesquels sont définis les opérations mod et comparaison à 0 (une classe polynôme par exemple) ? Est ce que le systême d'inférence va me mettre des bâtons dans les roues parce qu'il a fait de mauvaise hypothèses ?

          En OCaml il n'y as pas de typeclasses comme en Haskell (il y a des travaux pour rajouter cela au langage) et on ne peut pas surcharger les opérateurs. Les opération mod et la comparaison à 0 ne fonctionnent que sur les types int et donc le système d'inférence identifie que les paramètres de la fonction sont nécessairement de type int.

          ( mod );;
          - : int -> int -> int = <fun>
          
          0;;
          - : int = 0

          Néanmoins on peut définir une module qui implémente le type polynôme avec sa propre fonction mod et sa valeur zero.

          Pour reprendre le cas du pgcd si je compare la valeur à un float comme 0.0 le système se plaindra :

          let rec pgdc a b =
            match a mod b with
            | 0. -> b
            | r -> pgcd b r
          ;;
          Error: This pattern matches values of type float 
                 but a pattern was expected which matches values of type int

          Pour comparer avec python :

          >>> def f(i):
          ...   print("l'entier %i" % i)
          ... 
          >>> f(2)
          l'entier 2
          >>> def g():
          ...   f("a")
          ... 
          >>> g()
          Traceback (most recent call last):
            File "<stdin>", line 1, in <module>
            File "<stdin>", line 2, in g
            File "<stdin>", line 2, in f
          TypeError: %i format: a number is required, not str
          

          là où avec l'inférence de type l'erreur sur g est determiné statiquement à la compilation

          let f i = Printf.printf "l'entier %i" i;;
          val f : int -> unit = <fun>
          
          f 2;;
          l'entier 2- : unit = ()
          let g () = f "a";;
          Error: This expression has type string but an expression was expected of type int

          Sapere aude ! Aie le courage de te servir de ton propre entendement. Voilà la devise des Lumières.

    • [^] # Re: A force...

      Posté par . Évalué à 6.

      Ca vous semble vraiment plus lisible / beau / mettre ici tout adjectif hype que :

      >>> import fractions
      >>> fractions.gcd(10,15)
      5

      Réponse : Oui.

      Parce qu'on ne cherche pas à coder un PGCD en python avec du typage statique, mais à faire des choses bien plus complexes. Le PGCd c'est un exemple simple et facile à présenter pour montrer le comportement, c'est tout.

      Yth.

    • [^] # Re: A force...

      Posté par (page perso) . Évalué à 4.

      En prenant un cas caricatural et en oubliant tous les autres avantages (lib standard, écosystème, absence de compilation, …), ça ne change pas grand-chose, forcément.

    • [^] # Re: A force...

      Posté par . Évalué à 2.

      A force de reprendre toutes les idées du C/C++, on va se retrouver avec du C/C++…

      Au "petit détail" près qu'en Python les int sont des "big int" non?
      Adieu les débordements entiers si fun en C/C++..

  • # Rigolo

    Posté par . Évalué à 10.

    C'est rigolo car il y a d'un coté les langages statiques/bas niveau qui cherchent à donner l’expressivité utile pour permettre décrire des parties de ton code de très haut niveau (=> avec une grande abstraction) et de l'autre tu as les langages très haut niveaux/dynamiques qui eux cherchent à te donner les clefs pour gagner en performance là où tu en a vraiment besoin.

    Je ne sais pas qu'elle est LA bonne approche (s'il y en a une), mais c'est rigolo à voir.

    Tous les contenus que j'écris ici sont sous licence CC0 (j'abandonne autant que possible mes droits d'auteur sur mes écrits)

    • [^] # Re: Rigolo

      Posté par (page perso) . Évalué à 10.

      Pour faire du python depuis plus de 8 annnées en techno principale, en équipe et en solo, en développement et en scripting, le vrai problème est la maintenance et la compréhension du code.

      L'absence de typage statique / annontation complique la maintenance du code car tu ne sais pas ce que tu peux / ce que tu ne peux pas faire, ce que tu dois / ce que tu ne dois pas faire.

      J'utilise autant que faire se peut python 3 avec les annotations, et lorsqu'on travaille en équipe, ça simplifie énormément la reprise de code de l'un par l'autre.

      Quant aux problématiques de performance, elles ne sont généralement pas résolues en python.

      • [^] # Re: Rigolo

        Posté par . Évalué à 2.

        le vrai problème est la maintenance et la compréhension du code

        c'est pas le probleme avec tous les langages cela?

        Quant aux problématiques de performance, elles ne sont généralement pas résolues en python

        Parceque cela n'est pas fait pour…

        Un des problemes que moi je rencontre avec python c'est le global lock qui fait chier sa mere pour passer sur de gros systeme et donc tu es oblige d'utiliser un autre langage pour paralleliser.

        • [^] # Re: Rigolo

          Posté par (page perso) . Évalué à 3.

          Un des problemes que moi je rencontre avec python c'est le global lock qui fait chier sa mere pour passer sur de gros systeme et donc tu es oblige d'utiliser un autre langage pour paralleliser.

          Pourrais-tu donner plus de détails sur les problèmes que te posent le gil ? De mon expérience ça se contourne facilement (y'a qu'à voir les YouTube/Dropbox qui ont des systèmes en python avec des exigences de stabilité énormes). En plus les modules de multithreading/multiprocessing sont assez bon et permettent par exemple de remplacer le threads par des processus quand on veux du calcul parallèle.

          • [^] # Re: Rigolo

            Posté par . Évalué à 1.

            OpenMP et python ca fait deux a cause du GIL.

  • # typage statique automatique ?

    Posté par . Évalué à 2. Dernière modification le 30/05/16 à 12:09.

    Ce qui serait intéressant, c'est de faire de typage dynamique en phase de prototypage, ET de lancer un script qui rajouterait une couche de typage statique une fois le code prêt pour un commit. Ça donnerait une couche supplémentaire de vérification, sans se taper le typage statique à la main.

    Ça doit être automatisable, non ?

    • [^] # Re: typage statique automatique ?

      Posté par (page perso) . É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 :

      sum list = if null list
                   then 0
                   else (head list) + sum (tail list)

      Note, dans la vraie vie je ne l'écrirais pas comme cela, mais plutôt :

      sum [] = 0
      sum (x:xs) = x + sum xs

      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.

      • [^] # Re: typage statique automatique ?

        Posté par . Évalué à 3.

        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.

        Si je ne m'abuse ce n'est pas une interface, mais bien du typage structurel (c'est à dire, du « duck typing » statique). Tu n'a pas besoin de suivre une interface, tu as juste les bonnes méthodes. C'est très différent, parce que ça te permet de ne pas avoir à modifier le type que tu utilise pour utiliser la méthode en question.

        Tous les contenus que j'écris ici sont sous licence CC0 (j'abandonne autant que possible mes droits d'auteur sur mes écrits)

        • [^] # Re: typage statique automatique ?

          Posté par . Évalué à 1.

          Tu n'a pas besoin de suivre une interface, tu as juste les bonnes méthodes. C'est très différent, parce que ça te permet de ne pas avoir à modifier le type que tu utilise pour utiliser la méthode en question.

          Je ne comprends pas ce que tu veux dire … Il faut bien un jour dire comment traiter cet objet non ? En tout cas en haskell, il existe une « classe » Num (qui est plutôt une interface en fait), et tu peux dire que n'importe quel type est un Num du moment qu'un certain nombre de fonctions de base sont définies … pour ce type particulier.

          En pratique :

          data MonSuperType = Vide
          
          instance Num MonSuperType where
              (+) Vide Vide = Vide
              (-) Vide Vide = Vide 
              ... -- autres fonctions 
          • [^] # Re: typage statique automatique ?

            Posté par . Évalué à 3.

            tu peux dire que n'importe quel type est un Num du moment qu'un certain nombre de fonctions de base sont définies

            D'une part que ce lien n'est pas défini préalablement (tu n'a pas défini ton type comme étant une spécialisation du Num).

            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.

            Tous les contenus que j'écris ici sont sous licence CC0 (j'abandonne autant que possible mes droits d'auteur sur mes écrits)

            • [^] # Re: typage statique automatique ?

              Posté par (page perso) . É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 :

              class Shape t where
                  area :: t -> Float

              Et que j'ai le type :

              data Square = Square {side :: Float}

              Alors je peux associer la typeclass au type avec :

              instance Shape Square where
                  area square = let
                                   c = side square
                                in c * c

              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 :

              foo s = area s * 2

              aura comme type foo :: Shape t => t -> Float

              Ici n'importe quelle type qui implémente l'interface Shape fonctionne avec foo`.

              • [^] # Re: typage statique automatique ?

                Posté par . Évalué à 4.

                l'association est explicite

                Je suis un peu déçu, je pensais que c'était inféré.

                Tu n'as pas besoin de définir que la fonction prend un Num ou un Bar

                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.

                Tous les contenus que j'écris ici sont sous licence CC0 (j'abandonne autant que possible mes droits d'auteur sur mes écrits)

                • [^] # Re: typage statique automatique ?

                  Posté par (page perso) . Évalué à 4.

                  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.

            • [^] # Re: typage statique automatique ?

              Posté par . Évalué à 2.

              ce lien n'est pas défini préalablement

              Peut-être est-ce un abus de langage, mais par interface, je voulais dire comme en Java par exemple

              Java includes a concept called interfaces. A Java interface is a bit like a class, except a Java interface can only contain method signatures and fields. An Java interface cannot contain an implementation of the methods, only the signature (name, parameters and exceptions) of the method.

              Suivi de

              Before you can really use an interface, you must implement that interface in some Java class.

              Dans ce document. Dans l'exemple en haskell, il suffit de faire une première substitution classe -> type puis interface -> classe pour avoir quelque chose d'équivalent.

              Ou alors comme les protocols en Objective-C (bien que je ne sois pas sûr que cela représente la même chose).

              (que ta méthode pourrait prendre Num ou Bar)

              En fait, il n'y a pas de surcharge en haskell (ou presque) : deux fonctions qui ont le même nom doivent être dans des espaces de nommage différents, sinon le compilateur râle. Or, si on veut définir une fonction sur les Bar et une fonction sur les Foo il faut deux types différents pour la fonction, et donc on n'y arrivera pas … (enfin, pas à ma connaissance).

              Pour ne pas parler dans le vide voilà un exemple concret

              class Affichable a where -- Un type « a » est affichable quand ... 
                  affiche :: a -> String -- on peut avoir une représentation textuelle
              
              
              data UnSuperType = Gauche String | Droite String 
              
              instance Affichable UnSuperType where
                  affiche (Gauche s) = "gauche : " ++ s
                  affiche (Droite s) = "droite : " ++ s
              
              -- Un exemple de fonction qui est polymorphique 
              afficherListe :: (Affichable a) => [a] -> String
              afficherListe = intercalate "," . map affiche

              Dans l'exemple ci-dessus, la fonction polymorphe dit qu'il existe pour le type a une fonction affiche :: a -> String. Si on veut être polymorphe sur plusieurs classes avec des fonctions qui sont communes … comment choisir ? Soit on crée une classe parente qui regroupe les éléments communs, soit on ne sait pas ce qu'on veut vraiment faire.

              • [^] # Re: typage statique automatique ?

                Posté par (page perso) . Évalué à 3.

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

              • [^] # Re: typage statique automatique ?

                Posté par . Évalué à 4.

                Peut-être est-ce un abus de langage, mais par interface, je voulais dire comme en Java par exemple

                Oui, oui c'est juste que je pensais que l'inférence de type était capable de faire ce lien de lui même. Comme c'est le cas avec les lambdas de Java justement.

                public int foo(char c) {}
                
                public interface MyType {
                    int vroumvroum(char plonk);
                }
                
                public void bar(MyType cool) {}
                
                bar(this::foo);

                On passe la référence à la première méthode à la méthode bar et lui vérifie qu'elle peut être utilisée comme un MyType.

                En fait, il n'y a pas de surcharge en haskell (ou presque)

                Un type somme permet ça, non ?

                Tous les contenus que j'écris ici sont sous licence CC0 (j'abandonne autant que possible mes droits d'auteur sur mes écrits)

                • [^] # Re: typage statique automatique ?

                  Posté par . Évalué à 1. Dernière modification le 30/05/16 à 20:23.

                  Par surcharge j'entends « déclarer plusieurs fonctions avec des types différents mais qui ont le même nom et la fonction la plus spécifique1 est choisie à l'évaluation » (sous-typage). Ce truc (qui est faisable) n'est pas directement dans haskell, mais oui, on peut toujours l'émuler avec un type somme, mais ça devient très vite lourd quand même : il faut gérer manuellement dans chaque fonction chaque cas, ou alors faire des fonctions indépendantes pour chaque « sous-type » et puis faire une grosse fonction qui détermine quelle fonction appliquer …

                  1 : et là il faut savoir ce que l'on fait. On peut décider de prendre la plus spécifique sur le terme d'entrée et générique sur le terme de sortie, ou l'inverse, enfin, il y a un choix à faire.

      • [^] # Re: typage statique automatique ?

        Posté par (page perso) . Évalué à 4.

        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.

        Ah ?
        Pour avoir écrit du Haskell professionnellement, je dirais que je ne suis pas d'accord. En Haskell, quand tu commences à écrire du code un peu compliqué, tu dois écrire tes types bien souvent si tu veux que ça compile. En revanche, ce problème s'est beaucoup moins posé en OCaml…

        • [^] # Re: typage statique automatique ?

          Posté par (page perso) . Évalué à 2. Dernière modification le 01/06/16 à 18:09.

          Tu as la chance de faire du haskell professionnellement, j'aimerais bien ;)

          De mon expérience amateur, je met des types sur les "top level", mais c'est tout. Il y a deux-trois cas tordus, quand tu commences à abuser de typeclass ou OverloadedLists / OverloadedStrings, mais en pratique cela ne m'a jamais trop embeté.

          Mais il est clair que ma remarque était en comparaison avec par exemple C++ où tu met des types partout.

          (Edit: la raison qui fait que ce problème existe moins en OCaml est surement du justement à l'absence de typeclass, donc l'absence des ambiguïtés qui peuvent en résulter).

  • # On s'en bat le steak

    Posté par . Évalué à -6.

    Python marche bien sans typage, là c'est se faire ch… pour rien. Tu veux des perfs, tu écris en C. Tu sais pas le type que va te balancer la fonction? C'est que le dev ne l'a pas bien documentée, et à la rigueur tu fais un test, tu lis le code et tu comprends. Franchement, le typage statique, on s'en bat le steak

    • [^] # Re: On s'en bat le steak

      Posté par . Évalué à 10.

      Pas du tout. Ça permet de se rendre compte de beaucoup d'erreurs à la compilation.

      Je préfère un programme qui refuse de compiler qu'un programme qui va planter uniquement quand je fais ceci ou cela (il faut que j'y pense, que je test, et cela à chaque fois. Pourquoi ne pas faire travailler la machine ?)

      "Quand certains râlent contre systemd, d'autres s'attaquent aux vrais problèmes." (merci Sinma !)

      • [^] # Re: On s'en bat le steak

        Posté par . Évalué à 3.

        Pas du tout. Ça permet de se rendre compte de beaucoup d'erreurs à la compilation.

        Effectivement, quel est l'intérêt de laisser compiler un code qui de toute façon ne pourra que planter à l'exécution ?1 et cela, tout en refusant de le compiler parce qu'il est mal indenté… Il y a vraiment des choix de design que je ne comprends pas dans ce langage. :-/


        1. ce n'est pas comme s'il n'existait pas des techniques éprouvées depuis des décennies pour éviter ce genre de désagréments. 

        Sapere aude ! Aie le courage de te servir de ton propre entendement. Voilà la devise des Lumières.

      • [^] # Re: On s'en bat le steak

        Posté par (page perso) . Évalué à 7.

        Avant même la compilation : dès l'écriture du code. Pourquoi compiler du code alors que la compilation échouera forcément ?

        Quand je code en Python, j'utilise PyCharm qui fait de l'inférence de type assez efficace (d'autant plus que je l'aide pas mal à ce sujet) et me force à respecter la PEP008. C'est maintenant rarissime que j'obtienne des erreurs de type au lancement.

        • [^] # Re: On s'en bat le steak

          Posté par (page perso) . Évalué à 2.

          C'est maintenant rarissime que j'obtienne des erreurs de type au lancement.

          Rarissime c'est trop ;) De mon coté, je suis moins chanceux / doué et je ne compte plus le nombre de fois où je lance un code python qui se vautre immédiatement sur une TypeError, où un mauvais nombre d'argument à une fonction, où une typo dans le nom d'une fonction ou même une erreur d'imbrication (Je pensais que j'avais un object mais en fait je manipulais une liste d'objets). Et le pire c'est quand cela ne se vautre pas immédiatement ;)

          Pour ne pas me contenter d'un troll, quel type d'erreurs obtient-tu au lancement qui ne sont pas des erreurs de type ? Plus le typage est avancé, plus de nombreuses erreurs de "logique" peuvent être captées par le système de type lors de la compilation, enfin du moins c'est l’expérience que j'ai.

          • [^] # Re: On s'en bat le steak

            Posté par (page perso) . Évalué à 3.

            Malheureusement, l'inférence de type n'est pas parfaite, surtout avec les bibliothèques tierce-partie qui n'aident pas toujours beaucoup (voire rarement) à ce sujet :(

            Ça serait pas mal de n'avoir que les erreurs de type, mais il y a tant d'autres raisons… l'algo peut être faux, on peut oublier de vérifier certaines conditions, les cas limites (j'essaie d'utiliser hypothesis pour ça), mauvaise gestion des exceptions, …

            Bon, globalement j'ai quand même bien plus de chances que ça fonctionne dès le premier lancement que l'inverse depuis que je fais les choses proprement (PyCharm + indications de type + tests + hypothesis + intégration continue).

            • [^] # Re: On s'en bat le steak

              Posté par (page perso) . Évalué à 2.

              conditions, les cas limites (j'essaie d'utiliser hypothesis pour ça)

              D'ailleurs ça serait pas mal qu'hypothesis sache lire les annotations et en deduise tout seul les types.

    • [^] # Re: On s'en bat le steak

      Posté par (page perso) . É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 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.

      Bonne découverte.

      • [^] # Re: On s'en bat le steak

        Posté par . Évalué à 2.

        Haskell est énorme mais fait peur. Ocaml c'est sympa.

        Honnêtement, pour ce qui est de leur système de types, c'est du pareil au même entre les deux; en dehors de l'absence de typeclasses en OCaml mais plus pour longtemps (modular implicits). Leur système prend leur inspiration dans la correspondance de Curry-Howard (ou correspondance preuve-programme, correspondance formule-type), ce qui en fait un système très expressif mais très exigeant : quand le compilateur gueule, c'est comme un mathématicien qui se plaint que la preuve d'une énoncé n'est pas correcte.

        Là où Haskell peut faire peur, c'est que comme il fait du fonctionnel pur il faut recourir à des monades pour les effets de bords : ça peut effrayer au premier abord. OCaml de son côté permet de mélanger les paradigmes impératif, fonctionnel et objet; et faire de la réutilisation de code (comme pour le paradigme objet) via les variants polymorphes (je ne sais pas s'il y a un équivalent en Haskell).

        Sapere aude ! Aie le courage de te servir de ton propre entendement. Voilà la devise des Lumières.

        • [^] # Re: On s'en bat le steak

          Posté par (page perso) . Évalué à 3.

          Là où Haskell peut faire peur, c'est que comme il fait du fonctionnel pur il faut recourir à des monades pour les effets de bords : ça peut effrayer au premier abord.

          C'est ce qui m'a séduit dans Haskell.

          OCaml de son côté permet de mélanger les paradigmes impératif, fonctionnel et objet; et faire de la réutilisation de code (comme pour le paradigme objet) via les variants polymorphes (je ne sais pas s'il y a un équivalent en Haskell).

          Non, pas l'impression que ce soit possible en effet.

          • [^] # Re: On s'en bat le steak

            Posté par . Évalué à 8.

            C'est ce qui m'a séduit dans Haskell.

            Je peux comprendre, les effets de bords et les valeurs mutables ça peut avoir des comportements surprenants :

            >>> class Test:
            ...   def __init__(self, items=[]):
            ...     self.items = items
            ...   
            ...   def add(self, item):
            ...     self.items.append(item)
            ... 
            >>> a = Test()
            >>> a.add("foo")
            >>> b = Test()
            >>> b.add("bar")
            >>> a.items
            ['foo', 'bar']
            >>> b.items
            ['foo', 'bar']

            La valeur optionnelle, qui est mutable, est initialisée une fois et partagée par toutes les instances ! \o/

            Sapere aude ! Aie le courage de te servir de ton propre entendement. Voilà la devise des Lumières.

            • [^] # Re: On s'en bat le steak

              Posté par . Évalué à 3.

              Je suis entrain de me former à Python. Comment expliquer ce comportement, et comment faudrait-il modifier le code pour avoir un bon fonctionnement ?

              Merci ;)

              • [^] # Re: On s'en bat le steak

                Posté par . Évalué à -6. Dernière modification le 01/06/16 à 15:01.

                class Test2:
                    def __init__(self):
                        self.classeur = list()
                
                    def add(self,feuille):
                        self.feuille = feuille
                        self.classeur.append(feuille)

                Pour l'explication j'imagine que la raison c'est que feuille est déclarée à l'intérieur de la classe alors que dans le code que tu as écris c'est un argument de init() donc il est appelé en dehors de ta classe.
                Ce qui fait que les deux objets on les arguments en communs.

                Dans certains cas cela peut être un avantage.

                • [^] # Re: On s'en bat le steak

                  Posté par . Évalué à -6.

                  la ligne self.feuille = feuille ne sert a rien désolé

                • [^] # Re: On s'en bat le steak

                  Posté par . Évalué à 4.

                  Rien à voir avec la classe. On peut reproduire le problème avec juste une méthode :

                  def foo(item, bar=[]):
                       bar.append(item)
                       return bar
                  
                  foo("coucou")
                  -> ['coucou']
                  foo("toto")
                  -> ['coucou', 'toto']

                  Les paramètres optionnels sont évalué une seule fois lors de la définition de celle-ci ensuite tu te trimbale toujours la même référence1. Tu obtiens aussi des comportements marrants si tu fais un time.time().

                  Dans le cas générale2 les valeurs par défaut des paramètres en python devraient être immuables (donc pas de structure de données que tu modifie, pas de valeur qui dépendent d'un contexte) et le reste du temps tu donne la valeur None et tu gère cette valeur dans le reste de ton code.


                  1. Note : ça reproduit le comportement des variables statiques du C, mais il vaut probablement mieux utiliser les générateurs (avec yield) si tu as ce genre de besoins. 

                  2. c'est à dire tout le temps sauf quand tu sais pourquoi tu ne le fais pas 

                  Tous les contenus que j'écris ici sont sous licence CC0 (j'abandonne autant que possible mes droits d'auteur sur mes écrits)

                  • [^] # Re: On s'en bat le steak

                    Posté par . Évalué à -8.

                    None est une valeur et elle est définie lors du premier appel de la méthode, c'est donc un empreint au caractère d'héritage propre aux langages objets.

                    En revanche je ne comprend pas ton code qui ressemble a l'héritage multiple des classes car il renvoit un objet de type class list qui semble être dépendant le structure immuable que tu as crée et pourtant en passant issubclass() et isinstance() je n'ai aucun retour positif.

                    • [^] # Re: On s'en bat le steak

                      Posté par . Évalué à 3.

                      None est une valeur et elle est définie lors du premier appel de la méthode, c'est donc un empreint au caractère d'héritage propre aux langages objets.

                      Je vois plus ça comme le static de C, mais peut être. Ce que je voulais montrer c'est que le problème existe dans la manière que python gère les valeurs par défaut des paramètres que ce soit des méthodes d'une classe ou une fonction.

                      En revanche je ne comprend pas ton code qui ressemble a l'héritage multiple des classes car il renvoit un objet de type class list qui semble être dépendant le structure immuable que tu as crée et pourtant en passant issubclass() et isinstance() je n'ai aucun retour positif.

                      Hein ? Il n'y a rien d'immuable. [] est une liste vide de type list et donc foo renvoie aussi un objet de type list (dans les exemples que j'ai donné).

                      Tous les contenus que j'écris ici sont sous licence CC0 (j'abandonne autant que possible mes droits d'auteur sur mes écrits)

              • [^] # Re: On s'en bat le steak

                Posté par (page perso) . Évalué à 3.

                La méthode recommandée est :
                ```

                class Test:
                … def init(self, items=None):
                … self.items = [] if items is None else items


                … def add(self, item):
                … self.items.append(item)
                ```
                None est un objet dont il existe une unique instance (comme False ou True), on peut donc indifféremment utiliser « x is None » et « x == None ».

                • [^] # Re: On s'en bat le steak

                  Posté par . Évalué à 3.

                  « x is None » et « x == None » donnent effectivement le même résultat, mais pas exactement les mêmes performances :

                  In [6]: timeit coin == None
                  10000000 loops, best of 3: 34.4 ns per loop

                  In [7]: timeit coin is None
                  10000000 loops, best of 3: 22.5 ns per loop

                  L'opérateur is est plus simple et plus rapide, il ne vérifie que l'égalité entre deux id.

                  • [^] # Re: On s'en bat le steak

                    Posté par (page perso) . Évalué à 2.

                    Oui, c'est d'ailleurs pour ça que j'ai mis « x is None » ; cependant il n'est possible que parce qu'il n'y a qu'une seule instance de None.

              • [^] # Re: On s'en bat le steak

                Posté par . Évalué à 3.

                Je suis entrain de me former à Python. Comment expliquer ce comportement, et comment faudrait-il modifier le code pour avoir un bon fonctionnement ?

                Le comportement observé est du au fait que les arguments optionnels ne sont évalués qu'une seule fois lors de la définition de la fonction. S'ils sont mutables, et qu'on les affecte comme attribut aux objets de la classe, ils ne sont alors pas propre aux objets mais deviennent des variables de classe. Pour éviter cela, si ce n'est pas ce que l'on souhaite, il faut utiliser la solution de flan et utiliser la valeur None comme paramètre par défaut, puis tester dessus pour avoir des listes vides différentes pour chaque objet de la classe.

                >>> class Test:
                ...   def __init__(self, items=None):
                ...     self.items = [] if items is None else items
                ...   
                ...   def add(self, item):
                ...     self.items.append(item)
                ... 
                >>> a = Test()
                >>> a.add('foo')
                >>> b = Test()
                >>> b.add('bar')
                >>> a.items
                ['foo']
                >>> b.items
                ['bar']

                En OCaml, par exemple, les arguments optionnels sont implicitement représentés par le type polymorphe 'a option :

                let foo ?x () =
                  match x with
                  | None -> print_endline "pas d'argument optionnel"
                  | Some i -> Printf.printf "l'entier %i\n" i
                ;;
                val foo : ?x:int -> unit -> unit = <fun>
                
                foo ~x:2 ();;
                l'entier 2
                - : unit = ()
                
                foo ();;
                pas d'argument optionnel
                - : unit = ()

                Sapere aude ! Aie le courage de te servir de ton propre entendement. Voilà la devise des Lumières.

                • [^] # Re: On s'en bat le steak

                  Posté par (page perso) . Évalué à 3.

                  Dans ce cas particulier, j'aime bien aussi :

                  class Test:
                      def __init__(self, items=()):
                          self.items = list(items)

                  Défaut:
                  - il y a forcément une copie, même si elle est potentiellement inutile (et du coup ça illustre bien comment, par exemple, les notions de propriétaire/temps de vie de Rust sont intéressantes pour des projets gros et devant être performants).

                  Avantages:
                  - puisque la liste nous appartient on peut la modifier sans trop de crainte (évidemment si les objets contenus sont mutable ils pourraient être modifiés de l'extérieur malgré ça).
                  - la fonction accepte du coup n'importe quel objet itérable (comme un générateur par exemple). Dans ton code items doit posséder une méthode append().
                  - même si ça ne dispense pas d'un commentaire sur l'API, la signature est plus explicite.

                  Dans le cas où 'items' n'a pas vocation à être très long (et qu'on n'a pas un 2ème argument du même genre), on pourrait aussi faire:

                  class Test:
                      def __init__(self, *items):
                          self.items = list(items)

                  du coup l'appel à la fonction serait Test(1, 2, 3) plutôt que Test([1, 2, 3]).

                  Comme quoi le "one way to do it" c'est (heureusement) une intention plus qu'une réalité :)

                  • [^] # Re: On s'en bat le steak

                    Posté par . Évalué à 1.

                    C'est même plus propre par rapport à ma solution et au problème initial : si on passe une liste dans l'appel au constructeur, on peut se retrouver au même désagrément sur les effets de bords.

                    >>> class Test:
                    ...    def __init__(self, items=None):
                    ...      self.items = [] if items is None else items
                    ...    
                    ...    def add(self, item):
                    ...      self.items.append(item)
                    ... 
                    >>> l = [1, 2, 3]
                    >>> a = Test(l)
                    >>> l.append(4)
                    >>> a.items
                    [1, 2, 3, 4]

                    Là où avec ta solution, en plus d'accepter plus de type comme paramètre du constructeur, l'attribut est un peu mieux décorrélé (mais pas totalement) de l'argument.

                    >>> class Test:
                    ...     def __init__(self, items=()):
                    ...         self.items = list(items)
                    ... 
                    >>> l = [1, [2, 3], 4]
                    >>> a = Test(l)
                    >>> l.append(5)
                    >>> a.items
                    [1, [2, 3], 4]
                    >>> l[1].append(6)
                    >>> a.items
                    [1, [2, 3, 6], 4]
                    >>>

                    Je ne sais pas ce que sont les notions de propriétaires/temps de vie en Rust, mais je dirais pour ma part : comme quoi le fonctionnel pur (ou l'observationnellement immuable) ça a son intérêt ! :-)

                    Sapere aude ! Aie le courage de te servir de ton propre entendement. Voilà la devise des Lumières.

                    • [^] # Re: On s'en bat le steak

                      Posté par (page perso) . Évalué à 2.

                      l'attribut est un peu mieux décorrélé (mais pas totalement) de l'argument

                      Oui c'est ce que je disais dans mon commentaire avec "si les objets contenus sont mutable ils pourraient être modifiés de l'extérieur malgré ça"

                      Je ne sais pas ce que sont les notions de propriétaires/temps de vie en Rust, mais je dirais pour ma part : comme quoi le fonctionnel pur (ou l'observationnellement immuable) ça a son intérêt ! :-)

                      Tout à fait, d'ailleurs en Rust les variables sont immuables par défaut. Mais comme c'est un langage système qui vise aussi les performances optimales, il y a moyen de déclarer explicitement des variables mutables. Et le système d'emprunt de références va tout de même garantir qu'un seul endroit du code a la responsabilité de modification à un instant T. Ce qu'on perd en souplesse (le compilateur est encore plus hargneux, puisqu'en plus des règles sur types, on doit respecter les règles sur les références—et le système de typage est moins puissant qu'en Haskell/OCaml), on le gagne en performance.

                      • [^] # Re: On s'en bat le steak

                        Posté par . Évalué à 1.

                        Oui c'est ce que je disais dans mon commentaire avec "si les objets contenus sont mutable ils pourraient être modifiés de l'extérieur malgré ça"

                        J'avais bien compris, mais je préférais illustrer par un exemple pour que ceux qui se demandent comment éviter ces effets de bords puissent avoir une illustration du problème. Résultat cela m'a amené à regarder s'il n'y avait pas une fonction de copie profonde d'une structure en Python, et voilà : copy.deepcopy() :-)

                        >>> class Test:
                        ...     def __init__(self, items=()):
                        ...       from copy import deepcopy
                        ...       self.items = list(deepcopy(items))
                        ... 
                        >>> l = [1, [2, 3], 4]
                        >>> a = Test(l)
                        >>> l.append(5)
                        >>> l[1].append(6)
                        >>> l
                        [1, [2, 3, 6], 4, 5]
                        >>> a.items
                        [1, [2, 3], 4]

                        La décorrélation est totale ! \o/

                        Et le système d'emprunt de références va tout de même garantir qu'un seul endroit du code a la responsabilité de modification à un instant T.

                        C'est un système de lock pour les problèmes d'accès concurrents ? Ou si plusieurs structures se partagent une référence, seule une peut la modifier jusqu'à ce qu'elle soit désallouée et une autre en prend le contrôle, et ainsi de suite ? (j'essaye de traduire les notions de propriétaire/temps de vie).

                        le système de typage est moins puissant qu'en Haskell/OCaml

                        Il y a des travaux pour améliorer encore celui d'OCaml pour pouvoir faire du polymorphisme ad-hoc et de la surcharge d'opérateur en utilisant son système de module et les foncteurs. Et on peut aussi manipuler des références (mais là c'est sans garde-fou) et faire de la programmation système en OCaml (la seule limite actuelle, c'est le GC qui ne gère pas les accès concurrents sur la major heap ce qui pose problème pour le parallélisme, cf multicore OCaml).

                        Sapere aude ! Aie le courage de te servir de ton propre entendement. Voilà la devise des Lumières.

                        • [^] # Re: On s'en bat le steak

                          Posté par (page perso) . Évalué à 2.

                          C'est un système de lock pour les problèmes d'accès concurrents ? […]

                          Non pas du tout. Alors n'étant (encore :) ) pas un spécialiste du langage, mon explication va être un peu légère. En gros tout est fait à la compilation (un lock serait fait au runtime), où le compilateur, via son borrow checker, vérifie plusieurs choses comme:
                          - les temps de vie des objets. Par exemple si on retourne une référence sur un attribut d'un objet, alors le temps de vie de cette référence ne peut excéder celui de l'objet lui-même. Le borrow checker peut donc détecter des cas où on essaye de stocker cette référence en dehors du scope de vie de l'objet lui-même (un cas de dangling pointer comme on peut en trouver en C/C++).
                          - lorsqu'on passe une référence mutable à une fonction, on lui délègue la responsabilité de cette référence, et on ne peut plus utiliser cette référence dans la suite du code.

                          En pratique cela oblige à écrire le code de manière à satisfaire le borrow checker (sachant qu'il est régulièrement amélioré pour être moins stupide—il y a des fois où il est évident pour l'humain qu'il n'y a pas de problème, mais ça ne l'est pas pour le borrow checker). L'écriture des structure de données classiques est donc tout un art (heureusement le boulot est déjà fait !).

                          Au final c'est un peu comme quand les langages fonctionnels, qui en promouvant l'immuabilité et l'abandon des variables globales par exemple (ce que Rust fait aussi), obligent à penser son code d'une manière naturellement plus robuste.

                          J'espère avoir été clair.

                          Et on peut aussi manipuler des références (mais là c'est sans garde-fou) et faire de la programmation système en OCaml (la seule limite actuelle, c'est le GC qui ne gère pas les accès concurrents sur la major heap ce qui pose problème pour le parallélisme, cf multicore OCaml).

                          Ocaml permet de faire de la programmation système (ce qui ne veut pas dire grand chose, mais disons qu'un langage compilé nativement, avec de bonnes performances, qui permet de manipuler threads/sockets…, rentre dans cette appellation), mais à l'instar de Go par exemple, il ne peut pas être qualifié de langage système à cause de la présence obligatoire du GC (attention je n'ai jamais dit que tu avais affirmé que c'était un langage système—mais la précision me tient à cœur), tandis que Rust peut se passer complètement de runtime. Ça n'en fait pas un meilleur langage, c'est juste qu'ils ont des desseins différents.

                          Au passage, un petit article, que j'ai lu récemment, sur les problèmes qu'un GC peut poser (ici en Haskell): https://blog.pusher.com/latency-working-set-ghc-gc-pick-two/

                          • [^] # Re: On s'en bat le steak

                            Posté par . Évalué à 1. Dernière modification le 05/06/16 à 10:38.

                            Je vois un peu mieux le principe du borrow checker, je regarderai peu être plus en détail la façon dont Rust gère les références. Cela étant, au début de Rust il y avait un GC qu'ils ont abandonné, mais l'idée de remettre de la gestion automatique est toujours présente, cela semble être néanmoins compliqué. Il y a deux articles sur le sujet sur le blog d'un des membres de l'équipe du compilateur :

                            Sinon quelle différence fais-tu entre un langage qui peut faire de la programmation système et un langage système ? Il y a des personnes qui écrivent des noyaux, sous la forme d'unikernel, en OCaml :

                            MirageOS is a library operating system that constructs unikernels for secure, high-performance network applications across a variety of cloud computing and mobile platforms. Code can be developed on a normal OS such as Linux or MacOS X, and then compiled into a fully-standalone, specialised unikernel that runs under the Xen hypervisor.

                            MirageOs

                            Il y a bien des problèmes de latence liés au GC qui peuvent apparaître (contrairement à la gestion totalement manuelle du C par exemple) mais cela se gère aussi en adaptant son fonctionnement à l'application. Je ne connais pas le fonctionnement du GC de Haskell (mais d'après l'article c'est similaire à celui de OCaml), mais pour celui de OCaml ce chapitre de Real World OCaml en détail bien le fonctionnement.

                            Le premier commentaire de l'article que tu cites renvoie sur un benchmark (dont je n'ai pas estimé la pertinence) qui mesure la latence d'un serveur web qui stocke une hashtable (pour le serveur web en OCaml, il utilise d'ailleurs une bibliothèque développée par l'équipe de MirageOS) :

                            benchresult

                            J'ai eu une polémique récemment sur le coup de la latence du GC de OCaml en rapport à ce benchmark du benchmarkgame de chez Debian. J'avais mal estimé le rôle du GC dans les mauvais résultats : comme pour le cas de ton article, il y a des objets trop gros qui restent vivants trop longtemps. En adaptant la taille de la minor heap on obtient de meilleurs résultats (j'utilise aussi la bibliothèque functory, qui fait une sorte de MapReduce, pour le parallélisme ) :

                            open Functory.Cores
                            let () = set_number_of_cores 4
                            
                            let set_minor_heap size = Gc.set {(Gc.get()) with Gc.minor_heap_size=size}
                            
                            type tree = Empty | Node of tree * int * tree
                            
                            let rec make i d =
                              if d = 0 then Node(Empty, i, Empty)
                              else let i2 = 2 * i and d = d - 1 in Node(make (i2 - 1) d, i, make i2 d)
                            
                            let rec check = function Empty -> 0 | Node(l, i, r) -> i + check l - check r
                            
                            let min_depth = 4
                            let max_depth = 
                              let n = try int_of_string(Array.get Sys.argv 1) with _ -> 10 in
                              max (min_depth + 2) n
                            
                            (*
                            * c'est ici que j'adapte la taille de la minor heap 
                            * ce qui évite de nombreuse copie entre la minor et la major heap
                            * et réduit grandement la latence due au GC 
                            *)
                            let () = set_minor_heap (8 lsl max_depth)
                            
                            let stretch_depth = max_depth + 1
                            
                            let () =
                              let c = check (make 0 stretch_depth) in
                              Printf.printf "stretch tree of depth %i\t check: %i\n" stretch_depth c
                            
                            let long_lived_tree = make 0 max_depth
                            
                            let worker (niter, d) =
                              let c = ref 0 in
                              for i = 1 to niter do 
                                Gc.minor();
                                c := !c + check(make  i d) + check(make (-i) d)
                              done;
                              !c
                            
                            let () =
                              flush stdout;
                              let tasks =
                                let l = ref [] in
                                for i = ((max_depth - min_depth) / 2 + 1) - 1 downto 0 do
                                  let d = min_depth + i * 2 in
                                  let niter = 1 lsl (max_depth - d + min_depth) in
                                  l := ((niter, d), None) :: !l
                                done;
                                !l
                              in
                              let res = ref [] in
                              let master ((niter, d), _) c =
                                res := (2*niter, d, c) :: !res;
                                []
                              in
                              (* il n'y a plus de major collection pendant ce calcul *)
                              compute ~worker ~master tasks;
                              let log (niter, d, c) =
                                let s = Printf.sprintf "%i\t trees of depth %i\t check: %i" niter d c in
                                print_endline s;
                              in
                              List.iter log (List.sort (fun (_, d, _) (_, d', _) -> compare d d') !res);
                              Printf.printf "long lived tree of depth %i\t check: %i\n"
                                max_depth (check long_lived_tree)

                            avec ce code, sur ma machine à 4 cœurs, j'obtiens comme résultat standard avec un time btree 20 :

                            real    0m5.411s
                            user    0m16.320s
                            sys     0m0.492s
                            

                            là où avec le code C le plus performant je suis dans cet ordre de grandeur :

                            real    0m3.721s
                            user    0m13.144s
                            sys     0m0.204s
                            

                            c'est pas si mal et bien mieux que le 25.82 s du meilleur code OCaml du test. D'autant que, n'étant pas moi même programmeur (je suis mathématicien et logicien, pas informaticien), il est fort probable qu'un spécialiste du langage puisse encore améliorer cela.

                            Sapere aude ! Aie le courage de te servir de ton propre entendement. Voilà la devise des Lumières.

                            • [^] # Re: On s'en bat le steak

                              Posté par (page perso) . Évalué à 3.

                              Cela étant, au début de Rust il y avait un GC qu'ils ont abandonné

                              Alors quelques précisions:
                              - le GC qu'il y avait au début de Rust n'étant pas un vrai GC, mais du comptage de références (parce que le travail n'était pas terminé).
                              - l'utilisation du GC a toujours pensée comme optionnelle : en gros on déclarait une référence comme étant "garbage collectable" (le langage a bien changé mais c'était dfans mon journal sur Rust 0.7 ).
                              - même quand le GC était partie intégrante du langage, le but était de s'en servir le moins possible, et par exemple la bibliothèque standard a vite atteint l'objectif de 0 GC.
                              - avec le temps l'équipe s'est aperçu qu'elle pouvait aller plus loin que ce qu'ils pensaient au début, et que le système de typage permettait de sortir le GC et de le penser comme une dépendance externe, sans besoin d'une syntaxe dédiée.

                              mais l'idée de remettre de la gestion automatique est toujours présente, cela semble être néanmoins compliqué

                              La gestion est déjà automatique, via le système de référence que j'ai (mal) expliqué, mais un GC optionnel serait un confort pour certaines partie de code. Ce qui complique la tâche ce sont les prérogatives assez élevées qu'imposent les buts du langage ('You pay for what you use' entre autres), quand Haskell/Ocaml/Java/Python… n'ont pas à se soucier de tels problèmes (mais ils en ont d'autres évidemment).

                              Sinon quelle différence fais-tu entre un langage qui peut faire de la programmation système et un langage système ?

                              La différence peut être mince, comme tu le soulignes avec les unikernels, mais je dirais qu'un langage système permet de maîtriser suffisamment parfaitement le comportement du code pour pouvoir écrire du code temps-réel (au sens temps réel dur, pas au sens globalement performant).

                              c'est pas si mal et bien mieux que le 25.82 s du meilleur code OCaml du test. D'autant que, n'étant pas moi même programmeur (je suis mathématicien et logicien, pas informaticien), il est fort probable qu'un spécialiste du langage puisse encore améliorer cela.

                              C'est très gentil de t'être donné la peine d'écrire un commentaire aussi argumenté, mais tu prêches un convaincu :) . Je sais très bien que Haskell ou OCaml ont de très bonnes performances dans la majorité des cas. Mais justement le lien que j'ai donné explique bien que Haskell va donner de très bon débit, mais va avoir des problèmes de latences ; ton benchmarck au final est équivalent à tester un débit moyen, mais ne va pas du tout tester les problèmes de latences, parce que 1) c'est pénible à tester 2) ce n'est pas ton problème.

                              • [^] # Re: On s'en bat le steak

                                Posté par . Évalué à 2. Dernière modification le 05/06/16 à 21:43.

                                Alors quelques précisions: […]

                                C'est globalement ce que j'en avais compris des deux articles du membre de l'équipe chargée du compilateur. L'idée étant de pouvoir rajouter un smart pointer Gc<T> dont l'intention serait similaire à Rc<T>. Si la gestion automatique est basée sur du comptage de références, je comprends pourquoi la politique de gestion est drastique via le borrow checker (comme éviter des cycles dans le graphe des pointeurs qui sont gérés par des éphémérons dans le cas du GC d'OCaml).

                                La différence peut être mince, comme tu le soulignes avec les unikernels, mais je dirais qu'un langage système permet de maîtriser suffisamment parfaitement le comportement du code pour pouvoir écrire du code temps-réel (au sens temps réel dur, pas au sens globalement performant).

                                Mais dans ce cas ne faut-il pas une gestion totalement manuelle de la mémoire pour faire ce genre de chose ? Un langage tel que Rust peut-il s'attaquer à ce genre de problématique avec son mode de gestion automatique ?

                                Mais justement le lien que j'ai donné explique bien que Haskell va donner de très bon débit, mais va avoir des problèmes de latences ; ton benchmarck au final est équivalent à tester un débit moyen, mais ne va pas du tout tester les problèmes de latences, parce que 1) c'est pénible à tester 2) ce n'est pas ton problème.

                                Effectivement mon benchmark ne montre que le débit moyen, j'utilise time, mais parce que n'étant pas programmeur je ne connais pas les outils pour faire de la mesure de latence. En réalité, la modification que j'ai apporté au code OCaml de départ est plus un sketch of proof pour régler des problèmes de latence liés au GC. Ce qui fait que je suis passé de 25 s à 5.5 s est que j'ai fait disparaître une action du GC qui est responsable de la latence observée dans l'article que tu as cité sur le GC de Haskell : un parcours de la major heap.

                                Le tas est composé de deux memory pool : une minor heap et une major heap. La première sert pour les objets à courte durée de vie et est très efficace pour ce qui concerne les temps d'allocation et de désallocation mémoire. Lorsqu'elle est pleine le GC nettoie et transfert les structures encore en vie vers la major heap, puis à chaque fois qu'il fait cela, il nettoie par tranche cette major heap (de manière incrémentale) : c'est ce processus en mode stop the world qui prend plus du temps et génère de la latence. Dans mon code, j'ai adapté la gestion de la mémoire pour ne pas y avoir recours. Cela se passe en trois temps :

                                (* le memory pool de la minor heap est définie avec une taille
                                 * suffisante pour les besoins du calcul : un gros malloc une
                                 * bonne fois pour toute *)
                                let () = set_minor_heap (8 lsl max_depth)
                                
                                (* c'est la partie proprement calculatoire du code *)
                                let worker (niter, d) =
                                  let c = ref 0 in
                                  for i = 1 to niter do
                                    (* je nettoie la minor heap pour éviter des transferts
                                     * vers la major heap *)
                                    Gc.minor();
                                    c := !c + check(make  i d) + check(make (-i) d)
                                  done;
                                  !c
                                
                                (* le bout de code qui enchaîne les calculs
                                 * ne subit plus de latence *)
                                compute ~worker ~master tasks;

                                Sans cela j'avais un grand nombre de major collection, qui sont responsables des latences observées dans le fonctionnement du GC, là où maintenant je n'en ai plus aucune. ;-)

                                Sapere aude ! Aie le courage de te servir de ton propre entendement. Voilà la devise des Lumières.

                                • [^] # Re: On s'en bat le steak

                                  Posté par (page perso) . Évalué à 2.

                                  Un langage tel que Rust peut-il s'attaquer à ce genre de problématique avec son mode de gestion automatique ?

                                  Je pense que oui, parce que la gestion est certes automatique, mais prédictible (en plus pour les applications vraiment temps réelle l'allocation dynamique est proscrite en générale, et Rust permet ça aussi grâce aux allocateurs personnalisés).

                                  • [^] # Re: On s'en bat le steak

                                    Posté par . Évalué à 1.

                                    Dans ce cas, ce n'est peut être pas totalement hors de portée de OCaml non plus. Dans cette direction, outre sa date de sortie ainsi que le premier exemple qui peut engendrer un stack overflow si on ne fait pas attention et n'est pas très optimisé, il y a cet article qui donne des pistes de réflexions.

                                    As you may know, there is a subset of Javascript that compiles efficiently to assembly used as backend of various compilers including a C compiler like emscripten. We’d like to present you in the same spirit how never to allocate in OCaml. […]

                                    Now that we can manage almost any control flow without allocating, we need also to manipulate some values. That’s the point where we simply suggest to use the same approach as ASM.js: allocate a single large bigarray (this is some kind of malloc), consider integers as pointers and you can do anything. We won’t go into too much details here as this would require another post for that topic.

                                    Sapere aude ! Aie le courage de te servir de ton propre entendement. Voilà la devise des Lumières.

                                • [^] # Re: On s'en bat le steak

                                  Posté par . Évalué à 2.

                                  Sans cela j'avais un grand nombre de major collection […]

                                  Il y a un outillage autour d'Ocaml pour mesurer ce genre de choses ?

                                  Le tas est composé de deux memory pool : une minor heap et une major heap. La première sert pour les objets à courte durée de vie et est très efficace pour ce qui concerne les temps d'allocation et de désallocation mémoire. Lorsqu'elle est pleine le GC nettoie et transfert les structures encore en vie vers la major heap, puis à chaque fois qu'il fait cela, il nettoie par tranche cette major heap (de manière incrémentale) : c'est ce processus en mode stop the world qui prend plus du temps et génère de la latence. Dans mon code, j'ai adapté la gestion de la mémoire pour ne pas y avoir recours. Cela se passe en trois temps

                                  C'est très intéressant (merci !). En fait c'est un gc générationnel assez classique :)
                                  Ce qui est marrant c'est ta solution (en tout cas pour moi). Dans le monde JVM, on va plutôt essayer de faire passer ses objets dans une vielle génération et à limiter la création de nouveaux objets. Pour ça on va utiliser un pool d'objets, que l'on va réutiliser autant que possible. Ainsi on se protège des problèmes de débit réduisant la pression sur le débit (il y a d'autres gains potentiels comme le fait de réduire la fragmentation mémoire).

                                  Il est possible de jouer sur les tailles de génération de la JVM, mais ça se fait de manière externe. Ce sont des paramètres à passer à la JVM :

                                  • -XX:NewRatio= pour indiquer le ratio entre la young et la old génération
                                  • -XX:NewSize= taille minimale de la young génération
                                  • -XX:MaxNewSize=size taille maximale de la young génération

                                  La doc est là pour ceux qui ça intéressent : https://docs.oracle.com/cd/E19900-01/819-4742/abeik/index.html

                                  Du coup c'est des paramètres pour toute la JVM sur toute sa durée de vie. Tu ne peux pas régler ça pour une partie de ton programme.

                                  D'ailleurs comment se passe une redéfinition plus petite de la minor heap en Ocaml ? Il fait un stop the world et envoie tout ce qu'il faut dans la major heap ?

                                  Tous les contenus que j'écris ici sont sous licence CC0 (j'abandonne autant que possible mes droits d'auteur sur mes écrits)

                                  • [^] # Re: On s'en bat le steak

                                    Posté par . Évalué à 2.

                                    Ce qui est marrant c'est ta solution (en tout cas pour moi).

                                    N'étant pas développeur, ce n'est peut être pas très conventionnel ce que j'ai fait : mais ça marche ! :-P
                                    Disons que je me suis adapté au contrainte du test, normalement on évite de faire autant d'allocation dans du code réel. Selon le cas pratique qui amène ce genre de calcul, je dirais même que l'on peut totalement se passer d'allocation. C'est du code qui pourrait ressembler à un interpréteur d'un langage algébrique très très mal optimisé. En pratique, on ferai plutôt comme dans ce journal avec une solution en C++ (comparer le premier commentaire du journal avec le code de ma fonction worker ;-).

                                    En OCaml que ce soit avec du bytecode ou du code natif, on peut aussi gérer le fonctionnement du GC par des variables d'environnement : voir la doc à la variable OCAMLRUNPARAM (c'est ce que j'ai fait au départ avant de le mettre en dur dans le code).

                                    D'ailleurs comment se passe une redéfinition plus petite de la minor heap en Ocaml ? Il fait un stop the world et envoie tout ce qu'il faut dans la major heap ?

                                    Une redéfinition de la taille de la minor heap entraîne une collection mineur : il nettoie, envoie ce qui est encore en vie dans la major heap, et redimensionne. Documentation du module GC.

                                    En fait j'ai opté pour cette solution parce que l'algorithme du test consiste à allouer un arbre binaire parfait de profondeur d (la fonction make) puis à faire un checksum dessus (la fonction check) et répéter cela sur de nombreux arbres (le paramètre iter). Alors j'ai adapté la taille de la minor heap (le memory pool le plus efficace) pour qu'elle puisse contenir deux arbres les plus profonds qu'il y aura à allouer. :-)

                                    Sapere aude ! Aie le courage de te servir de ton propre entendement. Voilà la devise des Lumières.

                                  • [^] # Re: On s'en bat le steak

                                    Posté par . Évalué à 2.

                                    Il y a un outillage autour d'Ocaml pour mesurer ce genre de choses ?

                                    Apparemment c'est encore léger et ce n'en est qu'au début, il y a des efforts à faire de ce côté (c'est apparu avec le nouveau compilateur 4.03 et les efforts de Damien Doligez pour réduire le worst-case latency).

                                    Il y a un message (de gasche ;-) sur la mailing list : Measuring GC latencies for OCaml program dans lequel il renvoie à son article de blog plus complet Measuring GC latencies in Haskell, OCaml, Racket en réaction au billet pointé par GuieA_7 sur les problèmes de latence du GC de Haskell.

                                    Sapere aude ! Aie le courage de te servir de ton propre entendement. Voilà la devise des Lumières.

    • [^] # Re: On s'en bat le steak

      Posté par . Évalué à 5.

      Parle pour toi.

      J'adore écrire en Python. Mais il est parfois frustrant de tomber sur une erreur au runtime parce qu'on a fait une faute de code qui aurai été détecter à la compilation (et donc par l'IDE si tu en utilises un).

      Tu peux vouloir de bonne perf sans forcément vouloir les perfs du C et surtout sans te compliquer la vie à écrire du C.

      Oui au typage statique ou à l'inférence de type !

Suivre le flux des commentaires

Note : les commentaires appartiennent à ceux qui les ont postés. Nous n'en sommes pas responsables.