Journal C++17 est sur les rails

Posté par . Licence CC by-sa
Tags :
51
12
mar.
2016

À la dernière réunion du comité de normalisation de C++ qui a eu lieu à Jacksonville (Floride), les fonctionnalités de C++17 ont été plus ou moins gelées. On sait désormais ce qu'il y aura dedans mais aussi ce qu'il n'aura pas dedans. Et ça crée pas mal de remous.

Dans les nouveautés attendues :

  • une API pour gérer le système de fichier (basée sur Boost.Filesystem)
  • des algorithmes parallèles (ceux de la STL où on a ajouté un argument en premier pour dire qu'on veut qu'ils s'exécutent en parallèle)
  • plein de petites classes utiles qui étaient présentes dans Boost pour la plupart : any, optional, string_view; ainsi que des compléments à des trucs déjà existants
  • des fonctions mathématiques spéciales
  • la disparition de fonctionnalités obsolètes comme auto_ptr

Et pleins d'autres petits trucs.

Où est le problème alors ? Et bien, beaucoup de gens avaient de grands espoirs pour cette version de C++17. Il faut dire qu'elle avait été annoncée comme une version majeure de longue date : au moment de la publication de C++11, il avait été annoncé que C++14 serait une version mineure pour corriger C++11 (ce qu'elle a été) et que C++17 serait la prochaine version majeure avec une version mineure C++20.

Et puis, dès le départ, Bjarne Stroustrup, créateur du C++ et qui joue encore un rôle important dans le comité de normalisation, avait annoncé la couleur. Il voulait : les modules, les contrats, le type variant (destiné à avoir un remplaçant mieux typé de union), une bibliothèque pour le réseau, des co-routines, de la mémoire transactionnelle, les concepts, une nouvelle STL basée sur les intervalles (Range), la syntaxe d'appel uniforme, etc. Et au final, rien de tout ça n'a été intégré à la version C++17.

Les raisons sont variées mais la plupart du temps, les propositions n'étaient pas assez mûres : les modules ont deux implémentations très différentes et incompatibles pour l'instant (une de MS, une de Clang) ; le type variant a donné lieu à de très nombreux débats quant à savoir s'il fallait un état invalide (notamment par défaut) ou pas ; la bibliothèque réseau (basée sur Boost.asio) recoupe tout un tas de propositions (notamment sur la partie asynchone) qu'il faut finaliser ; les co-routines provoquent des guerres de religion pour savoir s'il faut une pile ou pas et disposent d'au moins 3 propositions dont aucune ne fait vraiment l'unanimité ; les concepts ne disposent pour l'instant d'aucune implémentation qui permettent de voir si la proposition est valide ; la nouvelle STL est basée sur les concepts ; la syntaxe d'appel uniforme ne fait pas du tout consensus à cause de problèmes intrinsèques importants.

Pourtant, les plus gros morceaux parmi ces propositions ont été poussé dans des Spécifications Techniques (TS), qui sont une sorte d'antichambre officielle pour les futures fonctionnalités du C++. C'est d'ailleurs par là que sont passé les propositions qui ont été intégrées à C++17. Mais même avec ça, la déception est grande. Certains ont pointé du doigt le comité de normalisation et son fonctionnement à l'ancienne, même s'il s'est beaucoup ouvert depuis 2011 et que tout le monde peut participer et faire des propositions. D'autres ont critiqué le fait que peu de gens sont salariés pour travailler sur la norme et les nouvelles propositions et que ça freine leur inclusion dans la norme. Pour répondre à tout ça, il a plus ou moins été décidé que la prochaine version sortirait en 2019 pour raccourcir un peu le délai entre deux versions.

Mon avis d'utilisateur du C++, c'est qu'il y a eu beaucoup trop d'attente et pas assez de travail. Je préfère un comité de normalisation prudent, qui pèse bien toutes les conséquences que peut avoir une proposition sur l'ensemble du langage existant. Oui, on peut parfois être envieux des mécanismes qu'on peut voir sur des langages comme Rust, mais nous ne sommes pas dans le même contexte : Rust est encore très jeune et dynamique et peut se permettre des évolutions majeures rapides, C++ a une base de code énorme qu'il ne faut pas casser sans pour autant laisser le langage dans le formol. Les nouveautés annoncés pour C++17 me vont bien (Boost.Filesystem doit être la bibliothèque Boost que j'utilise le plus, de très loin), et pour le reste, on attendra que ça soit prêt (façon Debian).

  • # Un petit ajout

    Posté par . Évalué à 9.

    Ceux qui lisent la langue de la perfide Albion pourront lire le compte-rendu de Herb Sutter, le grand patron du comité de normalisation (et qui travaillent chez Microsoft au passage), qui détaille un peu plus que moi tous les problèmes ainsi que tous les ajouts pour C++17.

  • # cppfix ?

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

    C++ devient de plus en plus complexe, avec plusieurs syntaxes pour faire les mêmes choses.

    Peut être faudrait-il de C++1X un subset et avoir un petit outil de migration des anciennes sources (un peu comme https://golang.org/cmd/fix/).

    http://devnewton.bci.im

    • [^] # Re: cppfix ?

      Posté par . Évalué à 6.

      Il existait un outil appelé cpp11-migrate dont on trouve encore quelques traces. Si j'ai bien suivi, ça s'est transformé en clang-modernize puis en clang-tidy qui permet de faire la même chose et bien plus encore.

    • [^] # Re: cppfix ?

      Posté par . Évalué à 1.

      Je pense surtout qu'il faudrait faire un autre langage, une surcouche C-next-gen rétrocompatible C++ (de manière explicite comme quand on utilise du C en C++).

      Le problème est le même que partout ailleurs, des projets qui ont une ou plusieurs décennies qu'on tente de faire évoluer sur des bases qui ne sont plus du tout solide.

      Le cas du C au C++ a quand même sacrément bien fonctionné, alors pourquoi pas du C++ au C-next-gen.

      • [^] # Re: cppfix ?

        Posté par . Évalué à 9.

        Quel intérêt ? Si tu as besoin d'un autre langage prends un autre langage. Ce n'est pas ce qui manque.

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

        • [^] # Re: cppfix ?

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

          Sur le segment de la perf / haut et bas niveau en même temps, C++ n'a pas (encore) de concurrent sérieux. Peut être Rust un jour…

          http://devnewton.bci.im

          • [^] # Re: cppfix ?

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

            pourquoi "un jour" ? Rust est stable maintenant. Ça en fait un concurrent sérieux selon moi.

            • [^] # Re: cppfix ?

              Posté par . Évalué à 3.

              Il en est où question performance ? Je présume que les optimisations du compilateur n'étaient pas la priorité au début.

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

              • [^] # Re: cppfix ?

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

                Il en est où question performance ? Je présume que les optimisations du compilateur n'étaient pas la priorité au début.

                Il s'est bien amélioré depuis qu'ils utilisent un backend LLVM, et pas qu'un petit peu.

                Sur le "The Computer Language Benchmarks Game" de Debian, Rust arrive à faire jeu égale avec C/C++ sur certains benchmarks.

                http://benchmarksgame.alioth.debian.org/u64q/rust.html

                http://benchmarksgame.alioth.debian.org/u64q/program.php?test=mandelbrot&lang=rust&id=1

                • [^] # Re: cppfix ?

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

                  Il s'en sort même mieux dans pas mal de cas

                  « Il n’y a pas de choix démocratiques contre les Traités européens » - Jean-Claude Junker

                  • [^] # Re: cppfix ?

                    Posté par . Évalué à 1.

                    Mieux ? C'en est presque de la désinformation.

                    En tant d’exécution peut-être, mais si tu regardes niveau mémoire ou CPU load…

              • [^] # Re: cppfix ?

                Posté par . Évalué à 3.

                Les optimisations sont essentiellement celles de LLVM pour le moment, ce qui est pas mal. Après rustc donne une énorme quantité d'IR à LLVM, ce qui lui rend l'optimisation non triviale (et longue). Mais ça marche plutôt bien en pratique.

                Il y a du travail en cours pour réduire la quantité de LLVM IR émise (et ainsi avoir certaines passes d'optimisation plus haut dans la chaine de compilation). Mais ça va prendre du temps à arriver.

        • [^] # Re: cppfix ?

          Posté par . Évalué à 2.

          Quel intérêt ? Si tu as besoin d'un autre langage prends un autre langage.

          Pour un nouveau projet oui. Mais quand tu as une application ancienne qui a coûtée des années de développement, ce n’est pas facile de tout reprendre à Zéro…

        • [^] # Re: cppfix ?

          Posté par . Évalué à 0.

          Un bien meilleur monde selon moi.

          La possibilité de réutiliser tes sources en C++, de tous ces avantages, tout en profitant des évolutions positives des nouveaux langages.

          Par ailleurs si à chaque fois que quelqu'un décidait de créer un nouveau langage se voyait dire "pas la peine, choisis-en un parmi les autres" on serait pas aller bien loin.

          • [^] # Re: cppfix ?

            Posté par . Évalué à 5.

            La possibilité de réutiliser tes sources en C++, de tous ces avantages, tout en profitant des évolutions positives des nouveaux langages.

            Ça rend les choses vachement complexes. Se permettre d'intégrer du C dans du C++ quand tu sais qu'historiquement le C++ a démarré comme une sur-ensemble du C, pourquoi pas. Mais créer un langage plus simple et se fader un langage complexe comme module en rab', c'est pas génial je trouve.

            Par ailleurs si à chaque fois que quelqu'un décidait de créer un nouveau langage se voyait dire "pas la peine, choisis-en un parmi les autres" on serait pas aller bien loin.

            Euh… Il y a une différence entre « créer un langage » et « créer un nouveau langage pour être compatible avec un existant ». Même en se plaignant que le C++ n'évolue pas assez vite au goût de chacun, il évolue. Crée un langage qui va t'apporter ce que tu aura quelques années plus tard directement en C++, ça ne me semble pas particulièrement pertinent. AMHA la première fonction d'un langage c'est d'occuper un scope clair (faire du fonctionnel) et pas la compatibilité source avec un langage existant.

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

  • # C'est bien dommage

    Posté par . Évalué à 2.

    Ce que j'attendais le plus de C++17, c'était les modules.

    Il faut absolument sortir du langage les commandes préprocesseurs. Aujourd'hui, il est impossible de faire un logiciel en C++ sans utiliser la commande #include. C'est pas normal qu'un langage doive s'appuyer sur un autre pour fonctionner.

    • [^] # Re: C'est bien dommage

      Posté par . Évalué à 2.

      Le préprocesseur ne disparaîtra pas. Et justement, dans les discussions sur les modules, il y a la question : est-ce que les macros font partie d'un module ou pas ? Cette question a été repoussée à un peu plus tard pour l'instant.

      • [^] # Re: C'est bien dommage

        Posté par . Évalué à 2.

        Il y a un parsing bien précis pour le préprocesseur qui est différent de celui du langage lui même, donc on peut dire qu'il y a deux langage en un.

        Je sais même pas pourquoi on veut en faire un module, ça me parait assez fou. Tu aurais des sources/liens de discussion à ce propos ?

        Mais en ce sens je comprends ce que veux dire Creak, est-ce qu'on fait dépendre le C++ de son langage de préprocesseur ou non.
        Actuellement on est pas obligé d'utiliser "#include" dans la théorie. Mais dans la pratique oui dès lors qu'on a deux fichiers .h.

    • [^] # Re: C'est bien dommage

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

      Il faut absolument sortir du langage les commandes préprocesseurs. Aujourd'hui, il est impossible de faire un logiciel en C++ sans utiliser la commande #include. C'est pas normal qu'un langage doive s'appuyer sur un autre pour fonctionner.

      Je dirais que c'est pas tellement un problème de préprocesseur. Le fond du problème est que le systeme d'include est a la fois trop bas niveau pour être simple d'utilisation et trop primitif pour être efficace.

      • Trop bas niveau car contrairement aux "import X" des autres languages, les includes C/C++ force encore à gérer manuellement la séparation déclarations / implémentations, les forwards, les guards, etc, etc

      • Trop primitif car les temps de compilation affreusement long du C++ viennent principalement du fait que par design, les headers doivent etre re-parsé a chaque inclusion.

      • [^] # Re: C'est bien dommage

        Posté par . Évalué à 6.

        Trop primitif car les temps de compilation affreusement long du C++ viennent principalement du fait que par design, les headers doivent etre re-parsé a chaque inclusion.

        T'es sur que c'est pas a cause de la complexite du langage plutot, avec les templates qui remportent la palme?
        Parce que c et objective c ont exactement le meme probleme de #include (bon, pas objc qui a #import, mais bon), et surtout, #ifndef est quand meme vachement utile pour eviter de parser 25 fois le meme header.
        Et ces 2 ne sont pas affreusement long a compiler.

        Apres, oui, pour avoir goute a @import en objc, ca change vachement la vie quand meme.

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

        • [^] # Re: C'est bien dommage

          Posté par (page perso) . Évalué à 4. Dernière modification le 14/03/16 à 20:38.

          #ifndef est quand meme vachement utile pour eviter de parser 25 fois le meme header.

          Ça rends les choses "moins pire" on va dire. #ifndef t'empechera de parser 25 fois le même header pour compiler le même fichier source.cpp mais il te fera quand même parser 100 fois le même header si tu l'inclues 100 fois depuis différents fichiers.

          Ça reste acceptable si tes headers sont de simple déclarations et des forwards comme en C. Ça l'est malheureusement beaucoup moins quand les headers contiennent du code templaté comme en C++.

          Le gros soucis avec le "#include header" c'est que c'est stateful. Le résultat de ton include varie suivant les déclarations ou pre-processor précédent ton include…. Et ça c'est un cauchemar à optimiser / pre-compiler pour un compiler.

          • [^] # Re: C'est bien dommage

            Posté par . Évalué à 2.

            Ça l'est malheureusement beaucoup moins quand les headers contiennent du code templaté comme en C++.

            Pour ça, il y a le mot clé "export".
            Tapez pas, je suis déjà sorti …

            • [^] # Re: C'est bien dommage

              Posté par . Évalué à 1.

              Depuis C++11, le mot-clef export a perdu sa signification C++98 mais reste un mot-clef réservé (il sera sans doute réutilisé par les modules). De toute façon, un seul compilateur a jamais réussi à implémenter cette fonctionnalité.

          • [^] # Re: C'est bien dommage

            Posté par . Évalué à 2.

            Et dieu créa les headers précompilés…

            • [^] # Re: C'est bien dommage

              Posté par . Évalué à -3.

              C'est ça la réponse des concepteurs de compilateur a cette horreur que sont les templates?

              Pourquoi les concepteurs du langage ne se sont pas occupés de ce problème en amont? En obligeant l'instantiation explicite des templates, en séparant l'implémentation des templates dans des fichiers séparés..

              Sur le plan juridique, les headers ne sont pas censé être copyrightable, il ne doit pas y avoir un germe de quelque chose pouvant être non trivial pour un cabinet juridique. Est-ce que les concepteurs ce sont souciés de ce genre de problème ?

              • [^] # Re: C'est bien dommage

                Posté par . Évalué à 3.

                C'est ça la réponse des concepteurs de compilateur a cette horreur que sont les templates?

                C'est la réponse de gcc au temps de compilation désastreux. La précompilation des headers est un détails d'implémentation non-couvert par la norme si je ne m'abuse.

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

              • [^] # Re: C'est bien dommage

                Posté par . Évalué à 3.

                C'est ça la réponse des concepteurs de compilateur a cette horreur que sont les templates?

                En quoi est-ce une horreur?

                Je fais du c++ et du java, et je peux te dire que l'absence de template est vraiment handicapante dans pas mal de cas.

                Il ne faut pas décorner les boeufs avant d'avoir semé le vent

              • [^] # Re: C'est bien dommage

                Posté par . Évalué à 4.

                Sur le plan juridique, les headers ne sont pas censé être copyrightable

                Quelle rôle d'idée? Qu'est-ce qui peut bien te faire croire que ça puisse être vrai?

              • [^] # Re: C'est bien dommage

                Posté par (page perso) . Évalué à 2. Dernière modification le 15/03/16 à 10:54.

                C'est ça la réponse des concepteurs de compilateur a cette horreur que sont les templates?

                Pourquoi les concepteurs du langage ne se sont pas occupés de ce problème en amont? En obligeant l'instantiation explicite des templates, en séparant l'implémentation des templates dans des fichiers séparés

                Si c'était si simple, le problème aurait été réglé depuis longtemps.

                La généricité des templates en C++ est bien plus puissante et poussée que les demi-generics dans des languages comme Java par exemple.

                Ça empêche tout type d'instanciation / compilation tant que les types dont ils dépendent n'ont pas été spécifiés. Forcer leur implémentation dans des fichiers séparés ne changeraient rien au problème, il faudrait toujours les parser à chaque spécification.

              • [^] # Re: C'est bien dommage

                Posté par . Évalué à 2.

                Sur le plan juridique, les headers ne sont pas censé être copyrightable,

                Tu retardes.. C'est ce que tout le monde pensait mais bon ça c'était avant.

          • [^] # Re: C'est bien dommage

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

            Pour remplacer #ifndef FOO_H on peu utiliser #pragma once. Moins sujet à l'erreur (pas de problèmes de collision ou d'oublier de changer quand on renomme un fichier). Et supporté par tout les compilateurs.

            • [^] # Re: C'est bien dommage

              Posté par . Évalué à 4.

              Je sais pas si c'est une super idée. C'est pas standard donc quand le langage va avoir sa solution. Soit ça va casser soit on va se coltiner des trucs non standard pendant encore longtemps après que ça n'est plus d'intérêt.

              Par contre c'est vachement plus pratique si tu l'oblige et que tu veux valider tes sources, tu peux facilement tester que toutes tes entêtes l'ont (c'est un peu plus compliqué mais tu peux aussi vérifier de la même façon que tu n'a pas 2 fichiers avec le même marqueur ifndef).

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

              • [^] # Re: C'est bien dommage

                Posté par . Évalué à 1.

                On peut aussi générer automatiquement les valeur du guard, avec une partie aléatoire, de cette manière chaque fichier possède un marqueur ifndef unique.

              • [^] # Re: C'est bien dommage

                Posté par . Évalué à 2.

                généralement on a un

                #ifndef NOMDUFICHIER_H
                #define NOMDUFICHIER_H
                [...]
                #endif

                et comme on a pas deux fois le même nom d'include ça passe; par contre dans le cas des bibliothèques externes, on est pas à l'abri d'une boulette.

                Il ne faut pas décorner les boeufs avant d'avoir semé le vent

                • [^] # Re: C'est bien dommage

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

                  idéalement un

                  #ifndef NOMDUFICHIER_H
                  #define NOMDUFICHIER_H
                  [...]
                  #endif /* NOMDUFICHIER_H */

                  pour savoir à quoi faire référence le #endif vu que le fichier peut être long

                  • [^] # Re: C'est bien dommage

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

                    Idéalement un

                    #ifndef NOMDUPROJET_NOMDUMODULE_NOMDUFICHIER_H

                    Pour éviter les conflits avec une lib quelconque, voire avec ses propres fichiers si le projet est gros.

                    • [^] # Re: C'est bien dommage

                      Posté par . Évalué à 5.

                      J'avais des templates pour faire ce genre de choses, mais je me dis que tu peux très bien y mettre un UUID et tu te pose plus la question. Il n'y a pas de logique particulière à mettre sur cet identifiant donc autant prendre un vrai identifiant. Il n'a jamais à être lu ou compris par un humain, il faut juste s'assurer que c'est le même sur les 2 lignes. Comme c'est aligné ça se fait très bien :

                      #ifndef GUARD_911DBF09_3D99_438E_9221_C3B91FE7D7A3
                      #define GUARD_911DBF09_3D99_438E_9221_C3B91FE7D7A3

                      C'est bourrin, mais au moins tu ne te pose plus de question. C'est de la technique, ça n'a pas besoin d'être lu par l'humain moyen.

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

                      • [^] # Re: C'est bien dommage

                        Posté par . Évalué à 1.

                        J'approuve des deux mains, ça marche tres bien.
                        Et avec un éditeur digne de ce nom, on génère automatiquement la valeur de l'UUID, et le triplet ifndef/define/endif qui va avec.

                  • [^] # Re: C'est bien dommage

                    Posté par . Évalué à 5.

                    Bof c'est la dernière ligne du fichier, tu sait que c'est pour ne pas l'inclure 2 fois.

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

      • [^] # Re: C'est bien dommage

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

        Trop bas niveau car contrairement aux "import X" des autres languages, les includes C/C++ force encore à gérer manuellement la séparation déclarations / implémentations, les forwards, les guards, etc, etc

        Et parfois, les problèmes d'ordre des #include… La bonne perte de temps bien inutile quand ça t'arrive :[

        • [^] # Re: C'est bien dommage

          Posté par . Évalué à 4.

          Comment ça arrive ?

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

          • [^] # Re: C'est bien dommage

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

            Quand dans un header tu dépends de déclarations qui sont faites dans un autre header.

            Ça arrive quand c'est mal fichu quoi, mais ça arrive.

    • [^] # Re: C'est bien dommage

      Posté par . Évalué à 4.

      C'est pas normal qu'un langage doive s'appuyer sur un autre pour fonctionner

      Il y a quelques années, j'aurais sûrement dit ça aussi. Aujourd'hui je ne serais plus aussi catégorique : d'une manière générale générer du code peut paraître "crade" mais c'est une manière pragmatique de s'appuyer sur des outils/compilateurs stables et qui font consensus et doivent continuer de faire fonctionner du code existant tout en exposant une syntaxe plus sympathique ou plus puissante ou pour faire de la metaprogrammation (e.g. les gars de Trolltech disaient depuis longtemps "code generators are good"). Après, je suis d'accord que le préprocesseur C est limité alors que d'autres solutions sont sûrement plus puissantes (e.g. Python/jinja pour de la metaprogrammation, voir aussi ici). Certains ont d'ailleurs fait ça avec d'autres langages récents qui ne requiert pas de préprocesseur., e.g. générer du Go

      • [^] # Re: C'est bien dommage

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

        Générer dans un autre langage, ça pose une problème si ton générateur n'attrape pas toutes les erreurs. Dans ce cas là, tu te retrouves avec les erreurs du langage d'en dessous mais qui a en plus été généré (donc pas de commentaires ou de variables avec des noms utiles (enfin ça dépend mais ça fini toujours par arriver)) et là, le debug devient compliqué.

        « Rappelez-vous toujours que si la Gestapo avait les moyens de vous faire parler, les politiciens ont, eux, les moyens de vous faire taire. » Coluche

        • [^] # Re: C'est bien dommage

          Posté par . Évalué à 2.

          Clang arrive à remonter jusqu'à la macro fautive quand le code généré provoque une erreur. Donc ça doit être possible de le faire. Après, ça implique que tout soit traité au même endroit par le même compilateur pour pouvoir tracer les transformations.

          • [^] # Re: C'est bien dommage

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

            Plus je vous lis, plus je suis convaincu de

            « Any sufficiently complicated C or Fortran program contains an ad hoc, informally-specified, bug-ridden, slow implementation of half of Common Lisp.—Greenspun's tenth rule of programming »

            • [^] # Re: C'est bien dommage

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

              Une bonne partie de la force (et probablement faiblesse) de Lisp réside dans une idée un peu analogue: tout programme Lisp suffisamment compliqué n'est pas écrit en Lisp, mais dans un dialecte de Lisp. L'apparence homogène d'un programme Common Lisp cache trompeusement qu'il s'agit probablement du langage avec le plus de syntaxe (en compétition avec Racket) déjà dans la spécification, et que cette syntaxe est étendue par tout programme utilisant des macros, donc au final il faut un peu apprendre un langage par programme, et je pense que tout ça y est pour quelque chose dans l'enthousiasme inégal qu'il produit (ce n'est pas uniquement la faute aux parenthèses :) ). L'approche génération de code externe, puis après on regarde le code est moins élégante et expressive, mais l'avantage c'est que le résultat est dans le même langage (donc analysable immédiatement par n'importe qui et par des outils aussi), et plus simple d'un point de vue compilation.

              • [^] # Re: C'est bien dommage

                Posté par . Évalué à 2.

                Attention, "dialecte de Lisp" désigne en général un langage de la famille Lisp (Common Lisp, souvent appelé Lisp, Scheme (dont Racket est une des implémentations/variantes), Emacs Lisp, Clojure, …).

                C'est vrai pour la création de langages basés sur Lisp, mais si tu utilises une bibliothèque de fonctions dans n'importe quel langage il faut apprendre les fonctions et le traitement qu'elles réalisent. En Lisp il y a en plus des macros et de la même manière il faut apprendre ce qu'elles font. Il s'agit juste de poser des couches d'abstraction par-dessus le langage de base.

                Et c'est vrai qu'il vaut mieux faire attention à ne pas écrire des macros pour tout et n'importe quoi, mais la contrepartie est qu'il est possible d'atteindre des niveaux d'abstraction plus élevées que dans d'autres langages. Donc de programmer dans un langage plus spécifique au domaine et en écrivant moins de choses. Et en relisant du code plus clair quand on doit relire/modifier le programme, avec moins de détails à apprendre. Avec les avantages classiques associés aux fonctions par exemple : écrire une macro c'est capturer une syntaxe, une forme qui revient dans le programme, et avoir du code généré (de manière transparente, sans le voir étalé partout) toujours de la même manière plutôt que du code de bas niveau qui se ressemble à différents endroits. Donc moins pratique pour quelqu'un qui arrive et qui veut faire une petite modification parce qu'il y a plus de choses à apprendre avant de commencer mais plus pratique pour faire des modifications à plus long terme. Et encore, si la modification est d'assez haut niveau il faut juste comprendre le langage construit sur la syntaxe de base et on peut se dispenser d'en connaître tous les détails : faire des modifications de haut niveau, même mineures, sur le code est plus commode.

                Évidemment écrire des macros qui servent deux fois dans un programme et truffer un petit programme de macros et spécificités diverses n'est pas une bonne idée. Mais si le programme est important capturer certaines formes qui n'entrent pas dans des fonctions et écrire le programme à l'aide de concepts de plus haut niveau est un gains de temps important, y compris pour le relire et comprendre ce qu'il fait ou même faire des modifications de haut niveau.

                On note que dans divers langages on voit passer l'expression "langage spécifique au domaine" (ou "domain specific language" / "DSL" parfois) mais en général on a soit une syntaxe moche soit un langage externe comme un truc à base de XML.

                Et pour les macros on peut afficher le code qu'elles génèrent, si on veut voir les détails :) .

                • [^] # Re: C'est bien dommage

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

                  Et c'est vrai qu'il vaut mieux faire attention à ne pas écrire des macros pour tout et n'importe quoi, mais la contrepartie est qu'il est possible d'atteindre des niveaux d'abstraction plus élevées que dans d'autres langages.

                  Oui, la théorie est simple.

                  La plupart des langages généralistes essaient d'identifier les éléments de syntaxe qui leur semblent les plus importants pour leurs objectifs. Ils favorisent certaines constructions, rendent plus faciles certains motifs et un peu plus verbeux d'autres. Certains langages sont plus ou moins conservateurs avec l'ajout de nouvelles syntaxes qui seront utilisées peu souvent, mais l'idée reste quand même d'identifier l'important, encourager des idioms et définir une syntaxe qui sera concise pour la plupart des usages de prédilection du langage, tout en essayant d'étudier les usages moins fréquents pour s'assurer que ça ne devient pas trop verbeux non plus, même si pas totalement naturel ou optimal.

                  Lisp de son côté, partant du principe qu'il est impossible de savoir à l'avance ce qui sera vraiment important pour tous les utilisateurs, plutôt que de prendre parti et décider si telle ou telle syntaxe sera plus demandée qu'une autre, met tout sur un même niveau, et propose un potentiel d'adaptabilité impressionnant. Le prix (d'un point de vue langage, mettant de côté les conséquences sur la complexité du compilo), c'est que tout se ressemble, le simple et le compliqué sont difficiles à distinguer, la syntaxe n'encourage pas certaines approches par rapport à d'autres, et les idioms ont tendance à être moins prévisibles.

                  Il y a sûrement des programmes où ce degré d'adaptabilité fait vraiment la différence, où il n'est pas possible de prévoir un langage généraliste suffisamment adapté à l'avance, sinon des livres comme le « On Lisp » de Graham n'existerait pas, je suppose, mais je ne suis pas convaincu que le gain soit déterminant aussi souvent que le laissent penser les introductions philosophiques à ce langage.

                  Et pour les macros on peut afficher le code qu'elles génèrent, si on veut voir les détails :) .

                  Bien sûr, avec un bon éditeur de texte (je dirais emacs en général pour du Lisp), il y a moyen, avec de l'entraînement, de démêler la syntaxe, mais c'est quand même une étape de plus par rapport à une syntaxe qui se voit et fait partie du langage de base: il faut vraiment une factorisation significative pour que ce soit rentable (et qu'il n'existe pas d'alternative sans macros qui factorise presque autant, quitte à être moins élégante, ce qui néanmoins l'avantage de signaler qu'effectivement c'est un point délicat du programme). Et analyser du Lisp avec un outil externe statiquement semble peu évident.

                  • [^] # Re: C'est bien dommage

                    Posté par . Évalué à 1.

                    Le prix (d'un point de vue langage, mettant de côté les conséquences sur la complexité du compilo), c'est que tout se ressemble, le simple et le compliqué sont difficiles à distinguer, la syntaxe n'encourage pas certaines approches par rapport à d'autres, et les idioms ont tendance à être moins prévisibles.

                    Si on parle de Common Lisp, le langage se présente effectivement comme multi-paradigme, mais c'est moins le cas pour Scheme ou Clojure.

                    J'aime bien cette approche assez souple mais ce qui me plaît le plus c'est vraiment la capacité à construire des couches de plus haut niveau, à finir par ne plus te demander comment faire entrer ce que tu veux exprimer dans le moule du langage mais à te demander assez librement d'un côté ce que tu veux réaliser et de l'autre comment tu veux l'écrire, Lisp te permettant presque automatiquement de faire le lien entre les deux (il n'y a pas que les macros, c'est un ensemble).

                    Et effectivement il n'y a pas vraiment d'avantage pour un programme de quelques centaines de lignes : pour un programme relativement modeste tu choisis une approche et tu t'y tiens. J'aime bien Lisp quand même dans ce cas, je trouve les choses assez simples à lire/écrire, mais ce n'est clairement pas là qu'il se distingue. Je ne trouve pas gênant d'utiliser du Lisp pour un petit programme ni de lire un petit programme en Lisp (en général il n'y a pas tellement d'approches mélangées à cette échelle), mais l'avantage réel n'apparaît que pour des programmes plus importants.

                    Malheureusement je pratique mais ça ne se montre pas encore, pour des raisons diverses. Et décrire comment tous les éléments de Lisp s'emboîtent pour arriver à ce résultat est assez difficile, c'est mieux avec des exemples. Je suppose que cette discussion resurgira d'elle-même quand je présenterai ici une partie de ce que je fais. J'essaierai de donner des exemples plus concrets parce que j'imagine que là ça fait un peu discussion entre gens qui savent déjà de quoi ils parlent et que pour le reste du monde c'est difficile à saisir sans avoir vraiment mis le nez dedans.

                    Pour juste un petit exemple : écrire un générateur html qui permet de mêler du code Lisp et du code html (comme les jsp de java mais avec tout le langage Lisp sous la main, sans un (en fait deux dans le cas des jsp, sans compter les balises personnalisées) langages supplémentaires à apprendre) ça nécessite même pas 100 lignes de code, avec les échappements html appliqués automatiquement. Et si tu écris des applications web tu sais à quel point la sécurité de la génération des pages est difficile (sinon je te le dis ;) : ajouter du texte dans des pages dont les méta-données sont du texte c'est assez délicat parce qu'il faut faire attention partout et que par définition les humains ne sont pas très doués dans ce domaine). Étendre ce générateur pour qu'il traite tous les cas (échappement des paramètres javascript, échappement correct des url, jetons dans les urls, …) c'est quelques centaines de lignes de code. Avec une syntaxe finale qui mêle harmonieusement lisp et html (ou ce qui le représente), sans avoir rien d'autre à apprendre, y compris pour créer tes propres composants de présentation (de simples fonctions/macros Lisp). C'est un exemple où il y a peu de choses à apprendre pour savoir comment ça marche, qui libère le développeur de pas mal de problèmes (moins de choses à penser partout) et qui permet à des gens ayant une expérience modérée du développement web d'éviter les conneries. À l'échelle d'un site de 3 pages ce n'est pas intéressant mais si tu réalises des applications plus complexes c'est vite rentabilisé.

                    Pour l'analyse statique on en reparle dans un peu plus longtemps :) . Mais rien ne s'oppose a priori à réaliser une analyse statique du code : ce n'est pas parce que le langage est dynamique qu'en pratique on redéfinit pendant son exécution les fonctions existantes.

                    • [^] # Re: C'est bien dommage

                      Posté par . Évalué à 2.

                      Pour juste un petit exemple : écrire un générateur html qui permet de mêler du code Lisp et du code html (comme les jsp de java mais avec tout le langage Lisp sous la main, sans un (en fait deux dans le cas des jsp, sans compter les balises personnalisées) langages supplémentaires à apprendre) ça nécessite même pas 100 lignes de code, avec les échappements html appliqués automatiquement. Et si tu écris des applications web tu sais à quel point la sécurité de la génération des pages est difficile (sinon je te le dis ;) : ajouter du texte dans des pages dont les méta-données sont du texte c'est assez délicat parce qu'il faut faire attention partout et que par définition les humains ne sont pas très doués dans ce domaine). Étendre ce générateur pour qu'il traite tous les cas (échappement des paramètres javascript, échappement correct des url, jetons dans les urls, …) c'est quelques centaines de lignes de code. Avec une syntaxe finale qui mêle harmonieusement lisp et html (ou ce qui le représente), sans avoir rien d'autre à apprendre, y compris pour créer tes propres composants de présentation (de simples fonctions/macros Lisp). C'est un exemple où il y a peu de choses à apprendre pour savoir comment ça marche, qui libère le développeur de pas mal de problèmes (moins de choses à penser partout) et qui permet à des gens ayant une expérience modérée du développement web d'éviter les conneries. À l'échelle d'un site de 3 pages ce n'est pas intéressant mais si tu réalises des applications plus complexes c'est vite rentabilisé.

                      Cela ressemble à ocsigen ce que tu décris, en moins développé. C'est écrit en OCaml (c'est la même famille que Lisp : \lambda-calcul avec très impératif, et aussi objet, mais en mieux typé et sans les horreurs du paranthésage et des macros — pour moi, avoir besoin de macros est un défaut de conception dans un langage) et ça génère du html garanti conforme (grâce au système de types du langage) ainsi que de la compilation OCaml vers javascript : ce qui permet d'écrire une webapp en full-ocaml en codant les parties client et serveur dans le même langage, tout en bénéficiant de la puissance et de la sécurité de son système de types.

                      En exemple, tu as l'application graffiti : proof of concept d'une application collaborative de dessin (si tu ouvres deux navigateurs sur la page, ce que tu dessines dans l'un est répercuté sur le caneva de l'autre) dont le code fait 200 lignes.

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

                      • [^] # Re: C'est bien dommage

                        Posté par . Évalué à 3.

                        Je ne vais pas entrer dans le sujet pour ou contre les déclarations et vérifications statiques de types. Et je ne connais pas tellement OCaml, donc je dis peut-être des conneries dans ce qui suit… J'ajoute que du coup j'ai jeté un œil sur tout ça mais que le projet semble assez complexe et que lire du OCaml c'est assez rude (je comprends que c'est subjectif, mais de mon côté je ne vois pas d'"horreur du parenthésage" dans Lisp : les parenthèses disparaissent assez vite, il n'y a même pas besoin de les regarder).

                        Dans mon message je faisais remarquer que créer une structure de données (html), y ajouter du code écrit dans langage complet (pas un langage créé pour l'occasion et moins complet) et les mêler simplement ça nécessite quelques dizaines de lignes de code. Je prenais un exemple très simple à réaliser en Lisp, je n'ai pas dit qu'il n'existait pas de choses plus complexes (y compris pour écrire du javascript en Lisp ou vérifier l'arbre html). Dans le lien que tu cites, la partie Eliom seule compte 30.000 lignes de code… Dans la plupart des langages de programmation il existe des outils de génération html / de sites dynamiques, ce n'est pas ce que je mettais en avant.

                        Un des intérêts des macros est de permettre d'étendre la syntaxe du langage, je ne vois pas en quoi c'est un problème de conception. S'il n'y a pas de mécanisme d'extension tu peux moins écrire les choses comme tu veux. Et dans le code d'Eliom (c'est peut-être là que je n'ai pas tout compris) j'ai l'impression que des outils de compilation intégrés au langage sont utilisés. Donc pas de macros mais on utilise des outils propres aux compilateurs pour étendre la syntaxe…

                        Un des bouts de code en question, mais il y en a un peu partout :

                        (** Signature of specific code of a preprocessor. *)
                        
                        module type Pass = functor (Helpers: Helpers) -> sig
                        
                          open Helpers.Syntax
                        
                          (** How to handle "{shared{ ... }}" str_item. *)
                          val shared_str_items: Ast.Loc.t -> Ast.str_item list -> Ast.str_item
                        
                          (** How to handle "{server{ ... }}" str_item and toplevel str_item. *)
                          val server_str_items: Ast.Loc.t -> Ast.str_item list -> Ast.str_item
                        
                          (** How to handle "{client{ ... }}" str_item. *)
                          val client_str_items: Ast.Loc.t -> Ast.str_item list -> Ast.str_item
                        
                          [...]
                        • [^] # Re: C'est bien dommage

                          Posté par . Évalué à 3. Dernière modification le 18/03/16 à 11:34.

                          Un des intérêts des macros est de permettre d'étendre la syntaxe du langage, je ne vois pas en quoi c'est un problème de conception. S'il n'y a pas de mécanisme d'extension tu peux moins écrire les choses comme tu veux. Et dans le code d'Eliom (c'est peut-être là que je n'ai pas tout compris) j'ai l'impression que des outils de compilation intégrés au langage sont utilisés. Donc pas de macros mais on utilise des outils propres aux compilateurs pour étendre la syntaxe…

                          Tu as bien compris, et l'intérêt de passer par des outils propres aux compilateurs pour étendre la syntaxe plutôt que d'utiliser des macros c'est :

                          • uniformiser et simplifier l'écriture d'extension
                          • conserver les gardes-fou du typage statique

                          Jusqu'à il y a deux ans, pour étendre la syntaxe OCaml on utilisait un pré-processeur camlp4 (ce qui, au fond, est similaire à de la macro) mais c'était chiant à écrire et on se retrouvait avec des tonnes de syntaxes différentes entre les projets : la coopération entre projet était rendue plus compliqué. Avec la nouvelle façon de faire, via ppx, cela se passe au niveau de la manipulation des AST et les extensions sont uniformisées : la coopération est simplifiée et s'est accrue. Par exemple dans le cas de Eliom :

                          let f x = x + 1 (* définition usuelle d'une fonction en ocaml, ici le successeur *)
                          let%client f  x = x + 1 (* extension de syntaxe pour dire que cette fonction est définie côté client *)
                          let%server f x = x + 1 (* la même mais définie côté serveur *)
                          let%shared f x = x + 1 (* elle est accessible aussi bien côté serveur que côté client *)

                          Étendre la syntaxe d'un langage est une chose fort utile, mais il ne faut pas que cela défigure complètement la syntaxe initiale (ce qui est tentant avec les macros) et quitte à générer du code par modification de la syntaxe autant le faire au niveau de l'AST : ce que montre la signature du foncteur que tu cites (une signature en OCaml c'est l'équivalent des headers en C ou C++, et un foncteur est proche des templates du C++ : une fonction dont les paramètres sont des modules, i.e. des structures ayant une signature donnée).

                          Les foncteurs (comme les templates en C++) est aussi une manière beaucoup plus simple et sécurisée (meilleur contrôle du typage) de générer du code. Un exemple simple, en OCaml, et la cas de la structure d'ensemble : pour construire des ensembles sur un type de donné quelconque, il suffit de le munir d'une relation d'ordre et d'utiliser le foncteur Set.Make

                          (* on définit une structure d'ordre sur les entiers *)
                          module IntOrd = struct
                          type t = int
                          let compare = Pervasives.compare (* c'est une fonction d'ordre générique sur n'importe quel type *)
                          end
                          
                          (* on la passe en arguments au foncteur Set.Make *)
                          module IntSet = Set.Make(IntOrd)
                          
                          (* et cela génère automatiquement toutes ces fonctions *)
                          module IntSet :
                          sig  
                           type elt = int                                                              
                           type t = Set.Make(IntOrd).t
                           val empty : t
                           val is_empty : t -> bool
                           val mem : elt -> t -> bool
                           val add : elt -> t -> t
                           val singleton : elt -> t
                           val remove : elt -> t -> t
                           val union : t -> t -> t
                           val inter : t -> t -> t
                           val diff : t -> t -> t
                           val compare : t -> t -> elt
                           val equal : t -> t -> bool
                           val subset : t -> t -> bool
                           val iter : (elt -> unit) -> t -> unit
                           val fold : (elt -> 'a -> 'a) -> t -> 'a -> 'a
                           val for_all : (elt -> bool) -> t -> bool
                           val exists : (elt -> bool) -> t -> bool
                           val filter : (elt -> bool) -> t -> t
                           val partition : (elt -> bool) -> t -> t * t
                           val cardinal : t -> elt
                           val elements : t -> elt list
                           val min_elt : t -> elt
                           val max_elt : t -> elt
                           val choose : t -> elt
                           val split : elt -> t -> t * bool * t
                           val find : elt -> t -> elt
                           val of_list : elt list -> t
                          end
                          
                          (* que je peux utiliser dans la foulée *)
                          let e = IntSet.empty (* e est l'ensemble vide *)
                          let e = IntSet.add 1 e (* je lui ajoute l'élément 1 *)
                          
                          (* je teste si 1 est élément de l'ensemble *)
                          InSet.mem 1 e
                          - : bool = true

                          Pour ce qui est du parenthésage, c'est une question de goût, mais la syntaxe de Lisp qui colle à la syntaxe du lambda-calcul n'est pas toujours des plus simples à lire. Par exemple la fonction qui consiste à échanger l'ordre d'application des arguments d'une fonction s'écrit en \lambda-calcul : \lambda z.(\lambda x.(\lambda y.(z y)x)), là où je trouve la syntaxe let swap z x y = z y x plus « parlante » et moins « lourde ».

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

                          • [^] # Re: C'est bien dommage

                            Posté par . Évalué à 4.

                            Je ne suis pas expert, mais les macros de lisp ne partagent que leur nom avec les macro de C et de C++. Elles ont une analyse du type (après c'est pas binaire, je ne sais pas si le typage est aussi puissant qu'en OCaml ou dans le reste de lisp) et ne passent pas par un préprocesseur, si je ne m'abuse.

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

                          • [^] # Re: C'est bien dommage

                            Posté par . Évalué à 3.

                            D'accord, donc en fait je pense que tu ne connais pas du tout Lisp… Comme je ne connais rien en OCaml c'est un peu délicat comme conversation :) .

                            Je vais essayer de préciser quelques trucs, je pense que ça va simplifier.

                            En Lisp tout ce qui est entre parenthèses est une liste. Donc le code source est lui même constitué de listes imbriquées : (print (+ 1 2)) est constitué d'une liste contenant le symbole "print" et une sous-liste, qui elle-même contient un symbole et deux entiers. On remarque que la notation préfixée + les imbrications de liste => on écrit directement l'AST. Contrairement à la syntaxe de la plupart des langages où on a des notations mélangées (préfixée dans le cas des fonctions, infixée dans le cas des opérateurs mathématiques par exemple), qui est traduite par l'analyseur syntaxique en un arbre, en Lisp le code représente déjà cet arbre.

                            Il ne faut pas confondre les macros du C avec celles de Lisp. Les macros du C font du remplacement de texte. Les macros de Lisp sont des fonctions qui reçoivent en paramètre des expressions Lisp. Donc des atomes (comme des nombres ou des chaînes) et des listes. La représentation uniforme entre le code et les données (les listes essentiellement) permet de manipuler ça très simplement. Si je crée une macro tructruc qui s'utilise de cette manière : (tructruc 1 (bidule 2)), la macro est une bout de code Lisp (comme une fonction) qui reçoit deux paramètres : le nombre entier 1 et une liste contenant un symbole ("bidule") et le nombre entier 2. L'analyse syntaxique est déjà faite, la macro reçoit en paramètre des expressions faciles à manipuler avec les opérations standards sur les listes. Et sans outils plus ou moins complexes de compilation. Pour générer du code c'est très simple : le code n'est jamais qu'une liste contenant des éléments simples et des sous-listes, le tout représentant l'AST. De ce que je comprends de ton message, OCaml est passé d'outils de bas niveau des compilateurs (analyse syntaxique notamment, avec le préprocesseur) à des outils de plus haut niveau sur les AST, ce qui est plus simple. Et ça a l'air bien, c'est plus proche du Lisp ;) . Par contre j'ai cru comprendre que ça ne permettait de créer que des variantes des opérateurs let, fun et if. Sinon la syntaxe pour définir le nouvel AST a l'air horrible (en Lisp il suffit de construire l'arbre sous forme de listes imbriquées : dans une page présentant la fonctionnalité je vois cette phrase : "For example, Exp.tuple [Exp.constant (Const_int 1); Exp.constant (Const_int 2)] would construct the AST for (1, 2). While unwieldy, this is much better than elaborating the AST directly." ; c'est vrai, mais en lisp on aurait juste écrit '(1 2)) et ça a l'air compliqué de lire les arbres/sous-arbres de l'expression d'origine pour en tirer des informations à placer dans l'arbre transformé.

                            Pour ce qui est de "défigurer" la syntaxe : en Lisp c'est facile, il n'y a "pas de syntaxe", juste les AST. C'est-à-dire qu'il n'y a rien à défigurer. En fait on peut modifier le sens de caractères (par exemple dire que si j'écris un truc entre crochets ça veut dire quelque chose de particulier) mais ce n'est pas avec les macros. Donc par exemple imaginons que je veux ouvrir un fichier et être sûr de le fermer correctement, même si une erreur est signalée, … Certains langages ont une syntaxe pour ça (c'est le cas de Lisp aussi mais on va faire comme si ce n'était pas le cas). Je crée une macro with-open-file (celle qui existe déjà en Lisp, mais on aurait pu l'écrire nous-mêmes) et j'utilise ça comme ça :

                            (with-open-file (fic "/chemin/du/fichier")
                              ...)
                            

                            La syntaxe n'est pas défigurée, la macro a la même forme que tout le reste : une expression sous forme d'une liste dont le premier élément est le nom de la macro. Comme pour une fonction en fait (voir le code avec print plus haut). Comme un bloc try-with-resources en java.

                            Pareil si je veux une macro qui définit des classes ayant certaines caractéristiques : je crée une macro "def-classe" qui prend en paramètre les noms des attributs de la classe et génère tout ce que je veux (accesseurs, opérations particulières, copie des objets, … ce qui me passe par la tête). Et ça s'utilisera comme ça :

                            (def-classe personne
                              nom
                              prenom
                              date-naissance)
                            

                            Il n'y a rien de défiguré : ça ressemble à l'utilisation de la macro defclass de Lisp (qui permet de définir les classes).

                            Défigurer la syntaxe c'est beaucoup plus facile dans les langages qui ont une syntaxe.

                            Pour ce qui est du lambda-calcul : euh… oui, bon, le lambda-calcul c'est un truc un peu théorique quand même :) . Si la syntaxe de Lisp colle à quelque chose c'est aux AST. Pour échanger les valeurs de deux variables on peut écrire (rotatef x y). Pour plus je ne sais pas s'il existe une macro (rotatef les décale d'un rang, donc pour 3 ça doit aller mais au-delà c'est moins évident ; d'un autre côté utiliser une seule instruction pour permuter plein de valeurs, bon… ou alors il y a let). En cas de besoin on pourrait écrire une macro qui s'utilise comme ça par exemple :

                            (swap (x y z) (z x y))
                            

                            Ou bien comme ça :

                            (swap (x z) (y x) (z y))
                            

                            pour mieux voir les paires.
                            On peut faire avec moins de parenthèses, mais séparer les deux listes c'est plus clair qu'écrire (swap x y z z x y).

                            Rien à voir avec l'expression de lambda-calcul que tu indiques, quelle idée… :)

                            • [^] # Re: C'est bien dommage

                              Posté par . Évalué à 3.

                              Pour ce qui est du lambda-calcul : euh… oui, bon, le lambda-calcul c'est un truc un peu théorique quand même :) . Si la syntaxe de Lisp colle à quelque chose c'est aux AST.

                              Alors euh, je suis d'accord avec tout ce que tu dis dans l'ensemble de ce post, sauf ce que j'ai cité au-dessus : j'ai appris LISP avant d'être mis en contact avec le lambda calcul, et sérieusement, oui, LISP colle au lambda calcul avant tout. Dire que ce langage colle aux AST c'est à mon avis trompeur, dans le sens où un AST peut être représenté différemment (on l'a vu avec la syntaxe d'OCaml, ou bien si tu manipules Clang/LLVM directement depuis C++), et manipulé différemment. La syntaxe fonction <liste> vient directement du lambda calcul. D'ailleurs, qu'on parle de Haskell, OCaml, F#, etc., même les opérateurs infixes sont secrètement des fonctions à qui on a ajouté un peu de sucre syntaxique.

                              • [^] # Re: C'est bien dommage

                                Posté par . Évalué à 1.

                                Pour les AST : les listes imbriquées sont une manière de représenter des arbres, donc le code lisp forme un arbre. C'est une représentation directe de l'AST, contrairement à une syntaxe infixe. On peut avoir d'autres représentations bien sûr. Mais le fait que le code représente directement l'arbre est une caractéristique importante du langage (pour écrire les macros notamment).

                                Pour le lambda-calcul : je ne connais pas tellement, je veux juste dire que personne n'écrit de vraies expressions de lambda-calcul telles quelles, c'est quand même assez violent. L'exemple fourni dans le message auquel je répondais ne ressemble en rien à du Lisp. D'ailleurs si j'en crois cette expression ou l'article de wikipédia ça n'a pas l'air d'être systématiquement enfermé dans des parenthèses : exemples de fonctions, parenthésage.

                                • [^] # Re: C'est bien dommage

                                  Posté par . Évalué à 3.

                                  Oui pour la reproduction de la structure en arbre, mais on pourrait dire exactement la même chose de pas mal de langages déclaratifs ou fonctionnels (y compris OCaml, c'est juste que le langage autorise une notation infixe qui est plus « naturelle » dans certains cas).

                                  Pour le rapprochement avec le lambda-calcul, il me semble que le « parenthésage » n'est qu'un effet de bord du besoin de de séparer les expressions d'une manière ou d'une autre : en 1958, utiliser un opérateur de composition comme on le ferait en maths ou en lambda-calcul n'était pas raisonnable pour des raisons de limitation de caractères de toute manière. :-) Mais honnêtement, entre

                                  (f 
                                      (g 
                                          (h '(...)
                                  )))

                                  et

                                  f∘g∘h (...)

                                  … tient juste à l'utilisation de parenthèses ouvrantes et fermantes pour groupe les termes, mais — surprise ! — il faut faire pareil en lambda-calcul pour indiquer la priorité des opérations sur certains opérandes. :-) En LISP, toute parenthèse ouvrante implique l'utilisation d'une fonction (ou d'une macro) sur une liste (potentiellement vide) d'arguments. En lambda-calcul, toute expression commence par l'utilisation d'une fonction…

                                  De même en OCaml si une fonction s'applique au résultat de l'appel de deux fonctions qui opèrent sur des paramètres séparés, il faudra utiliser les parenthèses pour les grouper pour casser les ambiguïtés.

                                  let sqr x = x * x
                                  let sum2 a b = 2*a + 2*b
                                  let f a b = (sqr a) + (sum2 a b)
                                  
                                  (* On pourrait écrire la même chose ainsi : *)
                                  let sqr x = (*) x x 
                                  let sum2 a b = + ((*) 2 a) ((*) 2 b)
                                  let f a b = (+) (sqr a) (sum2 a b)

                                  Et soit dit en passant, écrire f∘g∘h a b reproduit relativement explicitement un « AST »…

                                  • [^] # Re: C'est bien dommage

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

                                    Et soit dit en passant, écrire f∘g∘h a b reproduit relativement explicitement un « AST »…

                                    La différence c'est qu'en OCaml tu ne peux pas écrire une fonction qui prend une telle expression quotée en argument et la manipule comme un « AST », c'est à dire, avant d'évaluer quoi que ce soit, fait des trucs avec f, g et h, et au final calcule hogof ou n'importe quoi d'autre. Il faut aller chercher les ppx et l'AST pour ce genre de choses (et encore, je ne connais pas trop les limitations), voire définir soi-même une sorte d'AST avec des types (mais qui ne serait pas l'AST OCaml) pour représenter ce genre d'objets.

                                    • [^] # Re: C'est bien dommage

                                      Posté par . Évalué à 2.

                                      Je suis d'accord avec ta remarque sur OCaml. Je réagissais principalement sur « LISP n'a pas vraiment l'air si lié au lambda-calcul ». La remarque sur OCaml était là principalement pour dire que si on se passe du sucre syntaxique, mis à part la paire de parenthèses la plus extérieure, OCaml ressemble quand même fortement à un langage qui « colle » aussi pas mal au lambda-calcul, et donc à une représentation d'un programme en arbre (ce qui est logique : tout langage de programmation finira par être converti sous une forme d'AST ou une autre…)

                            • [^] # Re: C'est bien dommage

                              Posté par . Évalué à 3. Dernière modification le 18/03/16 à 17:34.

                              Effectivement je ne connaissais pas la terminologie Lisp, une macro n'est rien d'autre qu'une fonction si je comprend bien ? ou sont-ce des fonctions d'une forme particulière ? De fait la syntaxe Lisp représente directement son AST, là où en OCaml on passe par des modules du compilateur pour le manipuler. Mais il me semble que c'est essentiellement pour des raisons de typage : le Lisp est du lambda-calcul faiblement typé, là où OCaml est du lambda-calcul fortement typé statiquement.

                              Après le lambda-calcul est certes un outil essentiellement théorique (utile pour la théorie des langages), mais c'est juste du Lisp encore plus fruste : il n'y a qu'un seul opérateur, à savoir lambda, et on ne peut pas donner de nom aux termes (il n'y a pas d'opérateur d'affectation comme defun ou let). Par exemple ((lambda x) (x)) est la fonction identité ou encore ((lambda x) (y)) est la fonction constante égale à y. Ainsi si on applique le premier terme à n'importe quoi (n'importe quelle s-expression dont le seul opérateur de construction est lambda) alors il le renvoie, du genre (((lambda x) (x))((lambda z) (u v))) s'évalue en ((lambda z) (u v)).

                              Sinon, on peut aussi coder du OCaml à la manière de Lisp en notation purement préfixée. Il suffit d'entourer un opérateur infixe par des parenthèses pour l'utiliser en préfixe comme dans ( + ) 1 2 qui vaut 3.

                              Exemple sur la fonction factorielle en récursif terminal :

                              (defun factorial (n &optional (acc 1))
                                (if (<= n 1)
                                    acc
                                  (factorial (- n 1) (* acc n))))

                              dans le même style en OCaml

                              (* je commence par renommer les opérateurs infixes sous forme préfixe *)
                              let le = ( <= ) and mul = ( * ) and minus = ( - )
                              
                              (* je code le test if-then-else en fonctionnel pure *)
                              let iff b e1 e2 = if b then e1 else e2
                              
                              (* maintenant je code à la Lisp *)
                              let rec factorial ?(acc=1) n =
                              (* pour des raisons de typage on rajoute le mot clé rec et le paramètre optionnel est en premier *)
                              
                               (iff (le n 1)
                                  acc
                                  (factorial ~acc:(mul acc n) (minus n 1)))
                              
                              (* en OCaml plus idomiatique cela donnerait *)
                              let factorial n =
                                let rec loop acc n =
                                  match n with
                                  | 1 -> acc
                                  | _ -> loop (acc * n) (n - 1)
                                in loop 1 n

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

                              • [^] # Re: C'est bien dommage

                                Posté par . Évalué à 2.

                                Effectivement je ne connaissais pas la terminologie Lisp, une macro n'est rien d'autre qu'une fonction si je comprend bien ? ou sont-ce des fonctions d'une forme particulière ?

                                En pratique elles sont définies avec defmacro plutôt que defun, mais a priori derrière ça génère une fonction. À l'intérieur c'est juste du code Lisp, comme dans une fonction.

                                Première différence importante : alors que pour une fonction les paramètres sont évalués puis leur valeur passée à la fonction, pour une macro les expressions sont passées telles quelles. Donc si j'écris le code :

                                (truc 1 (+ 1 2))
                                

                                Si truc est une fonction, elle reçoit en paramètres 1 et 3, qui est le résultat de l'évaluation de l'expression (+ 1 2).
                                Si truc est une macro, elle reçoit en paramètre 1 et (+ 1 2), qui est une liste contenant 3 valeurs : un symbole et deux nombres. En Lisp les symboles sont des objets manipulables, comme les fonctions, les nombres, les chaînes et autres : ils peuvent être stockés dans une variable, dans une liste, … comme n'importe quelle autre valeur.

                                Deuxième différence importante : les macros renvoient une expression Lisp (une valeur simple ou une liste, cette liste représentant le code qui remplacera l'utilisation de la macro). La macro reçoit donc des paramètres et exécute du code Lisp qui génère le code qui doit la remplacer.

                                Troisième différence importante : les macros sont évidemment exécutées au moment de la compilation : dans mon exemple précédent, si truc est une macro le compilateur exécute la macro en lui passant les deux expressions, récupère une nouvelle expression qui remplace celle qui se trouvait dans le code. Puis il continue son parcours (si la racine de l'expression résultat est à nouveau une macro il la développe).

                                De fait la syntaxe Lisp représente directement son AST, là où en OCaml on passe par des modules du compilateur pour le manipuler. Mais il me semble que c'est essentiellement pour des raisons de typage : le Lisp est du lambda-calcul faiblement typé, là où OCaml est du lambda-calcul fortement typé statiquement.

                                En fait Lisp est plutôt fortement typé, même par rapport à Java (que je connais mieux qu'OCaml). Par rapport à OCaml je ne sais pas, mais "faiblement typé" je ne pense pas. Par contre c'est typé dynamiquement, effectivement. Tout ça si on suit le découpage suivant, qui représente bien les choses je trouve : typage faible/fort, statique/dynamique, explicite/implicite. Pour Lisp c'est typage fort, dynamique et implicite, pour OCaml typage fort, statique et implicite (si j'ai bien compris, c'est implicite et on précise dans les cas où le compilateur est perdu, la plupart du temps il se débrouille pour déduire les types).

                                La "syntaxe" de Lisp est une représentation assez simple de l'AST. L'intérêt de la notation préfixée c'est que les parenthèses délimitent des listes et qu'une macro n'est qu'une fonction un peu particulière qui reçoit des listes en paramètre et qui doit générer une liste (l'arbre représentant l'expression qui la remplace). Et tout ça est assez simple à manipuler avec les fonctions de base du langage.

                                Pour OCaml je ne connais pas donc j'ai un peu de mal à comparer avec les outils de manipulation de l'arbre. En Lisp il faut générer l'arbre pour vérifier le code par exemple (visuellement, en l'exécutant ou avec un analyseur de code, mais pour avoir une idée du code généré il faut exécuter la macro), avec OCaml je ne sais pas si le compilateur doit générer l'arbre pour le vérifier ou s'il peut vérifier directement le code de la fonction de remplacement pour savoir si le résultat sera valide, ce qui serait une différence.

                                Par curiosité, dans ton exemple en OCaml tu écris ça :

                                (* je code le test if-then-else en fonctionnel pure *)
                                let iff b e1 e2 = if b then e1 else e2

                                Dans un langage comme Java ça ne marche pas parce que les expressions e1 et e2 seraient évaluées systématiquement avant d'être passées en paramètre à la fonction iff.
                                En Common Lisp ça marche si iff est une macro, ce qui permet de ne pas évaluer systématiquement les deux expressions avant de choisir le résultat à renvoyer.
                                En OCaml ça marche parce que le langage est purement fonctionnel et que le compilateur va se débrouiller pour n'évaluer que les expressions nécessaires ? Ou tu as fait un exemple simplifié ?

                                Sur ce, bon week-end à tout le monde :) . Je lirai la suite (s'il y a) dimanche ou lundi sans doute.

                                • [^] # Re: C'est bien dommage

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

                                  Dans un langage comme Java ça ne marche pas parce que les expressions e1 et e2 seraient évaluées systématiquement avant d'être passées en paramètre à la fonction iff.

                                  C'est le cas aussi en OCaml, c'est un langage strict, contrairement à Haskell qui est paresseux, par exemple. Le iff est ici juste une fonction. En OCaml la seule façon d'étendre la syntaxe et de contrôler l'évaluation, et créer une nouvelle structure de contrôle, est d'utiliser camlp4/5 ou maintenant les ppx. Je suppose qu'avec la directive lazy pour évaluer paresseusement on pourrait faire un iff à peu près raisonnable, mais pas totalement naturel à utiliser.

                                  OCaml, du fait de son typage statique poussé et des garanties qui vont avec se heurte à pas mal de difficultés pour faire du méta-programming. Utiliser les ppx c'est clairement moins facile pour un programmeur OCaml lambda (perso, il me faudrait potasser l'AST un moment) que faire une macro en Common Lisp (tout programmeur connait l'AST). Quels que soient les progrès, faire du méta-programming ne sera probablement jamais aussi facile qu'en Lisp.

                                  L'approche c'est plutôt : le méta-programming n'est pas quelque chose dont a vraiment besoin un programmeur quotidiennement, donc on se contente d'une approche moins accessible et plus limitée, mais compatible avec le système de types pointu d'OCaml. Du coup, on ne fait pas une extension dès qu'une petite factorisation potentielle se présente. Par exemple, des fois avec un match sur un type somme avec beaucoup de cas, on s'aperçoit qu'on fait syntaxiquement plusieurs fois une chose quasiment identique, et parfois pas moyen de raccourcir, mais ça n'arrive pas très souvent : il semble improbable que ça représente une proportion significative du code. Personnellement, je trouve le compromis plutôt intéressant en pratique : un langage avec suffisamment de syntaxe au départ pour traiter les cas courants n'a pas tous les jours besoin d'une extension, même s'il arrive toujours un moment où sans cela on écrit du code plus verbeux.

                                • [^] # Re: C'est bien dommage

                                  Posté par . Évalué à 1.

                                  D'accord, si je comprends bien ce qui distingue une fonction d'une macro c'est le mode d'évalution des paramètres : pour une fonction ils sont passés par valeur, là où pour une macro ils sont passés par nom (dans ce cas l'évaluation est-elle paresseuse, i.e évalué une seule fois à la première utilisation du terme, ou recalculer à chaque usage du terme ?).

                                  Par curiosité, dans ton exemple en OCaml tu écris ça :

                                  (* je code le test if-then-else en fonctionnel pure *)
                                  let iff b e1 e2 = if b then e1 else e2

                                  Dans un langage comme Java ça ne marche pas parce que les expressions e1 et e2 seraient évaluées systématiquement avant d'être passées en paramètre à la fonction iff.
                                  En Common Lisp ça marche si iff est une macro, ce qui permet de ne pas évaluer systématiquement les deux expressions avant de choisir le résultat à renvoyer.
                                  En OCaml ça marche parce que le langage est purement fonctionnel et que le compilateur va se débrouiller pour n'évaluer que les expressions nécessaires ? Ou tu as fait un exemple simplifié ?

                                  Comme l'a dit anaseto, en OCaml les paramètres sont passés par valeur, ils sont évalués par l'appelant avant d'être passés à la fonction appelée (à la manière des fonctions Lisp et non des macros). La seule exception concerne les opérateurs sur les booléens : && (et) et || (ou) qui sont évalués de gauche à droite et de façon paresseuse. Cela étant en Comme Lisp if est une macro ou une fonction ?

                                  Comme l'a aussi précisé anaseto, il est possible de faire de l'évaluation paresseuse en OCaml via le module Lazy. Le même exemple (factoriel) avec iff en mode macro :

                                  let le = ( <= ) and mul = ( * ) and minus = ( - );;
                                  val le : 'a -> 'a -> bool = <fun>
                                  val mul : int -> int -> int = <fun>
                                  val minus : int -> int -> int = <fun>
                                  
                                  (* maintenant iff n'évalue que e1 ou que e2 suivant la valeur du booléen *)
                                  let iff b e1 e2 = if b then Lazy.force e1 else Lazy.force e2
                                  val iff : bool -> 'a lazy_t -> 'a lazy_t -> 'a = <fun>
                                  
                                  let rec factorial ?(acc=1) n =
                                   (iff (le n 1)
                                      (lazy acc)
                                      (lazy (factorial ~acc:(mul acc n) (minus n 1)))) (* on pourrait utiliser (pred n) au lieu de (minus n 1) *)
                                  val factorial : ?acc:int -> int -> int = <fun>
                                  
                                  factorial 5;;
                                  - : int = 120

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

                                  • [^] # Re: C'est bien dommage

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

                                    (dans ce cas l'évaluation est-elle paresseuse, i.e évalué une seule fois à la première utilisation du terme, ou recalculer à chaque usage du terme ?)

                                    Une occurrence d'une macro est développée une seule fois à la compilation (donc en particulier si elle apparaît à l'intérieur d'une fonction ou d'une boucle, son expansion n'est calculée qu'une seule fois).

                                    Cela étant en Comme Lisp if est une macro ou une fonction ?

                                    Une macro (en Common Lisp les fonctions sont évaluées strictement comme en OCaml).

                                  • [^] # Re: C'est bien dommage

                                    Posté par . Évalué à 2.

                                    Pour le passage par valeur/référence, les paramètres des fonctions lisp sont passés par valeur, l'essentiel des valeurs étant des références (comme en java, si ça te dit quelque chose : dans les variables on a en général des références à des objets).

                                    Les macros reçoivent en paramètre des expressions Lisp (du code Lisp), sous forme non de texte mais d'informations structurées (l'AST, qui est un truc tout simple constitué de listes imbriquées).

                                    Je vais prendre un exemple et le dérouler, ce sera plus simple…

                                    En Lisp, if est un opérateur spécial. Il s'agit d'un des quelques opérateurs existant dans le langage, qui ne peut pas être représenté par une fonction mais pas non plus par une macro parce qu'une macro ne fait que générer du code Lisp de plus bas niveau. Donc il faut bien une conditionnelle "élémentaire" pour construire les autres. Et par exemple cond, l'équivalent d'un if/elseif/elseif/else dans d'autres langages, est une macro qui se base sur if (qui génère du code contenant des if, en remplacement du cond).

                                    Les if s'utilisent comme ça en général (condition puis l'expression à évaluer si la condition est réalisée puis celle à évaluer sinon) :

                                    (if (= x 33)
                                      (print "réussi")
                                      (print "raté"))

                                    Les cond s'utilisent comme ça (t signifie "vrai" : la troisième condition est toujours vérifiée ; sachant que seule la première expression vérifiée conduit à évaluer l'expression correspondante) :

                                    (cond
                                      ((= x 33)
                                         (print "réussi"))
                                      ((= x 32)
                                         (print "presque"))
                                      (t
                                         (print "raté")))

                                    Il se trouve que if est un opérateur spécial et cond une macro, qui se base sur if. Pour simplifier mon exemple je vais imaginer l'inverse : cond serait un opérateur spécial compris par le compilateur et qui ne peut pas être écrit sous forme de fonction ou de macro, il n'existerait pas de macro if et je veux créer cette dernière.

                                    Le but est donc d'écrire une macro if qui permettra ensuite d'écrire ce code (1) :

                                    (if (= x 33)
                                      (print "réussi")
                                      (print "raté"))

                                    et qui le transforme en ce code, qui ne contient plus que cond, supposé être connu par le compilateur (2) :

                                    (cond
                                      ((= x 33)
                                         (print "réussi"))
                                      (t
                                         (print "raté")))

                                    On veut donc écrire une macro qui prend en paramètre 3 expressions Lisp : dans mon exemple ce serait (= x 33) (une liste contenant le symbole =, le symbole x et le nombre 33), (print "réussi") (une liste contenant le symbole print et la chaîne "réussi"), (print "raté") (une liste contenant le symbole print et la chaîne "raté").

                                    Les paramètres sont des listes représentant l'AST de ces 3 expressions. L'analyse syntaxique est déjà passée par là, la macro ne reçoit pas en paramètre le texte des expressions mais bien les expressions structurées en mémoire. C'est à dire que la macro peut vérifier si une des expressions reçues en paramètre est une liste, un nombre, … et si c'est une liste demander le nombre d'éléments, le premier élément, … : elle peut manipuler les expressions simplement, comme n'importe quelle liste contenant des données (=> le code est passé sous forme de données).

                                    Le but de la macro est de renvoyer une expression Lisp sous forme d'AST (des listes imbriquées) représentant le code généré. Dans ce code (mon exemple (2) plus haut), on voit bien les listes imbriquées, chaque paire de parenthèses délimitant une liste : le code résultat est constitué de listes, il va être simple de le construire en imbriquant des listes.

                                    Un dernier mot sur la fonction list : il s'agit simplement d'une fonction permettant de créer une liste. (list 1 2 "coucou") crée une liste contenant 3 éléments (honteusement pas du même type ;) ).

                                    La version moche de la macro, qui laisse bien apparaître la manipulation des listes et montre que les macros sont des bouts de code Lisp qui génèrent du code représenté par des listes imbriquées :

                                    (defmacro if (condition expr1 expr2)
                                      (list 'cond
                                            (list condition
                                                  expr1)
                                            (list t
                                                  expr2)))

                                    Le code crée une liste contenant le symbole cond et deux sous-listes. La première sous-liste contient deux éléments : la condition (dans mon exemple (= x 33)) et la première expression (dans mon exemple (print "réussi")).

                                    Une version plus simple de la macro (l'opérateur "`" permet de construire une liste, l'opérateur "," d'insérer dedans quelque chose à évaluer ; ici simplement la valeur des paramètres : la liste ainsi construite contient le symbole cond mais la valeur du paramètre condition) :

                                    (defmacro if (condition expr1 expr2)
                                      `(cond
                                         (,condition
                                           ,expr1)
                                         (t
                                           ,expr2)))

                                    Est-ce que tu vois comment les macros fonctionnent ? Elles prennent en paramètre des expressions Lisp qui sont des AST sous forme de listes imbriquées et renvoient un AST sous forme de listes imbriquées, le tout au moment de la compilation. L'AST généré remplaçant le code dont la macro est la racine (dans mon exemple le code (1) est remplacé par le code (2) : le compilateur voit une expression commençant par if, appelle la macro if, remplace cette expression par le résultat de l'exécution de la macro).

                                    Il n'y a pas de notion d'évaluation paresseuse ou non : la macro génère simplement un bout de code Lisp destiné à remplacer l'ancien. Et ce bout de code n'est rien d'autre que du code Lisp normal. De manière générale il n'y a pas d'évaluation paresseuse en Common Lisp. Par contre effectivement il faut faire attention en écrivant les macros : ici c'est très simple mais il faut éviter d'insérer expr1 à plusieurs endroits dans le code généré, sinon l'expression sera exécutée plusieurs fois. Dans ce cas il faut générer du code qui évalue cette expression et place le résultat dans une variable puis utilise cette variable. C'est un problème plus sournois dans les macros parce qu'on a vite fait d'écrire deux fois expr1 sans s'en rendre compte et d'insérer deux fois une expression dans le code généré, ce qui peut mener à des surprises au moment de l'exécution, mais ce n'est rien d'autre que l'évaluation normale de Common Lisp d'un code dans lequel on aurait recopié deux fois la même expression.

                                    Par contre la macro est évaluée au moment de la compilation, donc une seule fois.

                                    Il reste juste à noter que cette macro est assez simple mais qu'elle pourrait faire des calculs complexes, parcourir les expressions qu'on lui passe en paramètre (il s'agit de simples listes), faire n'importe quoi pour générer le code Lisp destiné à la remplacer. Il s'agit simplement de code Lisp quelconque manipulant des listes (parcours, construction, …).

                                    Est-ce que c'est plus clair ?

                                    (Et comment vous faites pour la coloration syntaxique de Common Lisp ? J'ai mis clojure parce que je n'arrive pas à la faire marcher avec "Common Lisp" ; ça marche bizarrement d'ailleurs, parfois j'écris n'importe quoi comme langage, ou même je le supprime, et ça continue à fonctionner : j'ai supprimé le langage sur le premier extrait de code et il est coloré quand même).

                                    • [^] # Re: C'est bien dommage

                                      Posté par . Évalué à 1.

                                      Par contre la macro est évaluée au moment de la compilation, donc une seule fois.

                                      Il reste juste à noter que cette macro est assez simple mais qu'elle pourrait faire des calculs complexes, parcourir les expressions qu'on lui passe en paramètre (il s'agit de simples listes), faire n'importe quoi pour générer le code Lisp destiné à la remplacer. Il s'agit simplement de code Lisp quelconque manipulant des listes (parcours, construction, …).

                                      Est-ce que c'est plus clair ?

                                      Oui c'est plus clair, je n'avais pas fait attention à ta remarque qui expliquait que les macros étaient exécutées à la compilation (ce qui explique leur dénomination).

                                      En fait le compilateur OCaml fait la même chose avec certaines fonctions, et dans la nouvelle version à venir il le généralise via une nouvelle représentation intermédiaire, qui ne sera qu'optionnelle pour cette version, mais sera sans aucun doute la base pour les versions ultérieures. Par exemple le compilateur actuel remplace systématiquement les appels à ma fonction iff par if b then e1 else e2 (comme si c'était une macro en Common Lisp), mais le nouveau système pousse l'idée plus loin en l'étendant aux fonctions récursives et en « exécutant » au maximum le code de la fonction qu'il développe. Tu pourras en lire plus sur la partie que j'ai consacré, avec chicco, au système FLambda dans la dépêche en cours de rédaction.

                                      Sinon if est aussi une primitive en OCaml, comme en Common Lisp, mais n'est en fait qu'un cas particulier d'une autre structure primitive le pattern matching qui est une version généralisée de ta macro cond : if b then e1 else e2 n'est qu'un alias pour match b with true -> e1 | false -> e2, et on peut mettre autant de cas que l'on veut dans l'analyse et pas seulement en faisant des tests sur les booléens mais sur la forme et la structure du type que l'on étudie. Cela parce que, comme pour Common Lisp, toutes les valeurs sont essentiellement des références (sauf les types int, bool, char et les constructeurs constants comme la liste vide) et accessible uniquement via leur constructeur de type.

                                      Par exemple pour définir la fonction list_map qui applique une fonction à tous les éléments d'une liste, on l'écrira

                                      (* en OCaml le type des listes se définit comme *)
                                      type 'a list =
                                       | Nil (* la liste vide *)
                                       | Cons of 'a * 'a list (* le nom du constructeur est issu du nom de l'opérateur en Lisp ;-) *)
                                      
                                      (* [] est un alias pour la liste vide et hd :: tl un alias pour Cons(hd, tl) 
                                      la liste [1; 2; 3] se construit par Cons(1, Cons(2, Cons(3, Nil))) et on retrouve la structure de liste imbriquée à la Lisp *)
                                      
                                      let rec list_map f l =
                                        match l with
                                        (* soit la liste est vide et on renvoie la liste vide *)
                                        | [] -> []
                                        (* soit elle a une tête et une queue, alors on applique f à la tête et list_map à la queue *)
                                        | hd :: tl -> (f hd ) :: (list_map f tl)
                                      ;;
                                      
                                      (* on peut alors faire une fonction qui renvoie la liste des successeurs d'une liste d'entiers *)
                                      let succ_list l = list_map (fun n -> n + 1) l;;
                                      
                                      (* mais à la compilation il va développer list_map comme une macro et simplifier le tout en *)
                                      let succ_list l =
                                        let rec list_map' l =
                                          match l with
                                          | [] -> []
                                          | hd :: tl -> (hd + 1) :: (list_map' tl)
                                        in list_map' l
                                      ;;

                                      Sinon pour la coloration de code en Common Lisp, je mets lisp comme langage.

                                      (defmacro if (condition expr1 expr2)
                                        `(cond
                                           (,condition
                                             ,expr1)
                                           (t
                                             ,expr2)))

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

                                      • [^] # Re: C'est bien dommage

                                        Posté par (page perso) . Évalué à 2. Dernière modification le 21/03/16 à 18:42.

                                        Par exemple le compilateur actuel remplace systématiquement les appels à ma fonction iff par if b then e1 else e2 (comme si c'était une macro en Common Lisp)

                                        Je n'ai pas testé encore flambda, mais sauf surprise il inline juste la fonction, mais sans changer la sémantique d'évaluation, ce qui veut dire que les arguments sont évalués avant. Par exemple : let () = iff true (print_int 4) (print_int 2) va afficher 42. Le if fait à partir de cond en Common Lisp sera lui un vrai if (même si comme l'a remarqué ylsul en vrai c'est l'inverse, et if n'est pas une vraie macro, plutôt une macro built-in on pourrait dire).

                                        le pattern matching qui est une version généralisée de ta macro cond

                                        Mais en Lisp tu peux, uniquement à l'aide de macros, définir un pattern matching à la OCaml (il y a plusieurs packages qui font ça dans différents dialectes). En fait, c'est même possible de créer un système de types statique uniquement à l'aide de macros (comme c'est fait dans une extension de Racket). Ou créer un langage de documentation comme scribble pour Racket, qui est en fait un programme Racket valide (pas un langage spécial à part avec des commentaires ou quelque chose comme ça, je trouve ça plutôt sympa). Bref, les possibilités offertes par les macros Lisp sont assez difficiles à égaler, et OCaml va difficilement gagner sur ce terrain. Après, c'est loin d'être un critère ultime, la plupart des cas où les macros permettent une solution élégante ont des alternatives tout à fait acceptables, parfois intégrées au langage de façon ad hoc ou à l'aide d'un outil externe (souvent plus léger).

                                        • [^] # Re: C'est bien dommage

                                          Posté par . Évalué à 1.

                                          Je n'ai pas testé encore flambda, mais sauf surprise il inline juste la fonction, mais sans changer la sémantique d'évaluation, ce qui veut dire que les arguments sont évalués avant. Par exemple : let () = iff true (print_int 4) (print_int 2) va afficher 42.

                                          Effectivement j'ai parlé trop vite, flambda fait de même pour ne pas changer la sémantique d'évaluation, même si les deux compilateurs (le classique et le flambda) comprennent que l'expression finira dans la première branche. Ils inlinent et suppriment même le test, mais évaluent quand même les expressions à cause des effets de bord qu'elles contiennent. En revanche en l'absence d'effet de bord, c'est compilé comme pour les macros en Common Lisp.

                                          Après que les macros offrent toutes les possibilités que tu décris, je ne le nie pas : cela permet de gérer deux stratégies différentes de beta-réduction sur les \lambda-termes. À l'exécution en commençant par les paramètres sur les fonctions, ou à la compilation sur le corps de la macro. Là où pour l'instant OCaml ne dispose que de la première stratégie, même si l'inlining a fortement à voir avec la seconde. Ce sont deux approches du \lambda-calcul qu'il est intéressant de comparer, même si je ne lâcherai pour rien au monde la puissance du système de types d'OCaml (statique et très grande expressivité). Le système de \lambda-calcul typé le plus puissant étant Coq comme je l'avais expliqué ici : un programme Coq qui compile est garanti sans bug, comme le compilateur CompCert (le seul à ma connaissance) développé par Xavier Leroy, le BDFL OCaml.

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

                                          • [^] # Re: C'est bien dommage

                                            Posté par (page perso) . Évalué à 3. Dernière modification le 22/03/16 à 08:51.

                                            un programme Coq qui compile est garanti sans bug

                                            Encore faut-il que les propriétés écrites qui sont prouvées soient les bonnes :) Donc dans le cas d'un compilo, principalement que la sémantique du langage initial soit écrite correctement (les rares bugs trouvés l'ont été à ce niveau pour CompCert). Ceci dit, un programme Coq qui compile, même sans preuves, apporte au moins la terminaison.

                                            comme le compilateur CompCert (le seul à ma connaissance)

                                            Le seul (que je sache) qui ait une chaîne complète (à quelques petits bouts près au début et à la fin), oui. Sinon il y a aussi eu quelques efforts pour formaliser l'IR de LLVM et quelques optims, par exemple.

                                      • [^] # Re: C'est bien dommage

                                        Posté par . Évalué à 1.

                                        Ok.

                                        Et je lirai la dépêche sur OCaml :) .

                                        • [^] # Re: C'est bien dommage

                                          Posté par . Évalué à 1.

                                          Au cours des discussions qui entourent la rédaction de la dépêche, j'ai appris qu'il y avait le projet d'ajouter le principe des macros à la Lisp au langage OCaml : modular macros.

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

                                          • [^] # Re: C'est bien dommage

                                            Posté par . Évalué à 1.

                                            Si je comprends bien, manipuler l'arbre explicitement est toujours difficile (ou pas possible dans ce modèle ?) mais ça fournit une syntaxe pour écrire des macros simples (un truc qui existe dans scheme, Racket est d'ailleurs cité dans l'article), c'est ça ? Contrairement à ppx qui fournit un outil plus complet pour manipuler l'AST mais au prix d'une plus grande complexité ?

                            • [^] # Re: C'est bien dommage

                              Posté par (page perso) . Évalué à 2. Dernière modification le 18/03/16 à 19:52.

                              Pour ce qui est de "défigurer" la syntaxe : en Lisp c'est facile, il n'y a "pas de syntaxe", juste les AST. C'est-à-dire qu'il n'y a rien à défigurer.

                              Je suis d'accord que « défigurer » n'est pas du tout l'image qui correspond, et qu'au contraire, les extensions au langage en Lisp sont naturelles (peut-être même trop pour l'œil humain). Ceci dit, par contre, j'appelle syntaxe tout élément dont l'évaluation n'est pas prévisible, et les macros en font partie (contrairement aux fonctions dont la logique d'évaluation est connue). Le fait que l'apparence soit homogène en Lisp est utile pour faire des macros de façon intuitive et naturelle, mais ne change rien au fait que chaque macros apporte de la syntaxe. On peut avoir une syntaxe très folklorique (genre Perl), et pourtant que la syntaxe en soi ne soit pas très complexe (c'est plus le lexeur qui est compliqué), comme avoir une syntaxe plus homogène mais plus complexe. Et d'ailleurs, si pas au niveau de Lisp encore, OCaml n'a pas une syntaxe particulièrement simple non plus, il y a pas mal de choses dans ce langage, et de plus en plus. Et autant cela peut apporter du pouvoir d'expression et plein de trucs utiles, autant ça rend le langage moins accessible aussi (et pas qu'aux débutants, je pense).

      • [^] # Re: C'est bien dommage

        Posté par . Évalué à 3.

        C'est le « pragmatique » qui fait la différence. Comme tu le dis ça peut être une bonne solution pour en faire plus avec un langage donné. Mais en soit si tu as besoin de ce genre d'artifices c'est que tu a besoin d'exprimer quelque chose que ton langage en est incapable ou incapable de la façon dont tu le souhaite. C'est bel et bien un manque de ton langage.

        Après c'est massivement utilisé notamment dans les technologies JS.

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

  • # Syntaxe d'appel uniforme

    Posté par . Évalué à 1.

    J'attends la syntaxe d'appel uniforme avec impatience.

    Ce que j’apprécie pas beaucoup dans le C++ c'est la non séparation structure/fonction.

    Très souvent on a envie d'une structure préexistante (string_view, vector) mais pas les 36 000 fonctions templates et headers qui vont avec qui ralentissent le temps de compilation et font grossir l’exécutable.

    Pouvoir appeler indifféremment "my_string.size();" ou "size(my_string);" changerai ma vie. :D

    • [^] # Re: Syntaxe d'appel uniforme

      Posté par . Évalué à 3.

      Cette syntaxe a fait couler beaucoup d'encre. Elle est soutenue depuis le départ par Herb Sutter et Bjarne Stroustrup, mais elle ne fait pas l'unanimité. En particulier parce qu'elle complexifierait beaucoup les règles déjà très complexe de recherche de nom. Et aussi parce qu'un des deux sens a un souci.

      Le sens qui va bien, c'est de chercher my_string.size() quand on écrit size(my_string). Actuellement, il y a beaucoup de fonctions qui sont définies pour ces cas là (genre begin/end). Le sens qui ne va pas bien, c'est de chercher size(my_string) quand on écrit my_string.size(). Parce que là, si la fonction size a un problème, on donne l'impression que c'est une méthode de my_string qui a un problème. En fait, c'est comme si on ajoutait des méthodes à une classe et ça peut poser des problèmes de définitions. Si un jour, l'auteur ajoute une méthode size() à sa classe, l'appel my_string.size() n'appellera plus size(my_string) et ça passera totalement inaperçu.

      • [^] # Re: Syntaxe d'appel uniforme

        Posté par . Évalué à 1.

        Pourquoi il ne pourrait pas y avoir une simple erreur de compilo informant qu'il y a ambigüité ? Et qu'une fonction a été "redéfinie" ?

        En considérant:

            struct Point {
                int x, y;
        
                void add(int x, int y);
        
                void square_length() const;
            };

        et

            void add(Point& p, int x, int y);      // ou void add(Point* p, int x, int y);
        
            void square_length(const Point& p);    // ou void square_length(Point* const p);

        La fonction add de la structure Point devrait définir la fonction add d'après quelque part à l'étape de la compilation et général une erreur.

        Si le niveau d'accès public/privé est un problème suffit d'appliquer la possibilité d'appel uniforme aux structures (et faire en sortent qu'elles ne puissent plus contenir de membres privés), au contraire des classes.

        Ajouter des méthodes à une structure, ça peut poser beaucoup de solution. :D

        • [^] # Re: Syntaxe d'appel uniforme

          Posté par . Évalué à 2.

          Pourquoi il ne pourrait pas y avoir une simple erreur de compilo informant qu'il y a ambigüité ? Et qu'une fonction a été "redéfinie" ?

          Actuellement lorsqu'on change de version de C++, à part des truc comme des mots clé en plus, ou des depreciated, on peut recompiler un ancien projet sans problème; on peut aussi mettre à jour ses bibliothèques tierces, et si l'interface n'a pas changé, juste ajouté des truc, ça compile sans problèmes.

          Il m'arrive aussi pas mal de créer des fonctions utilitaires, non publiés dans les .h, pour simplifier des traitements, vu leur nom et leur fréquences, je crains d'avoir quelques collisions le jour où elles commencent à exister dans les objets en question.

          J'ajouterai que l’ambiguïté peut aussi venir du const/non const, et qu'il y a un risque de passer complètement a coté (pour le compilo, y'a pas d'ambiguïté).

          Bref c'est un beau merdier, et je ne vois pas l'intérêt sauf peut être pour les template, mais je passe peut être à coté de quelque chose d'important.

          Il ne faut pas décorner les boeufs avant d'avoir semé le vent

          • [^] # Re: Syntaxe d'appel uniforme

            Posté par . Évalué à 2.

            Actuellement lorsqu'on change de version de C++, à part des truc comme des mots clé en plus, ou des depreciated, on peut recompiler un ancien projet sans problème

            En pratique, pas vraiment. En général, ce qui se passe, c'est que quand on change de version de C++, on change aussi de version de compilateur. Et les compilos s'améliorent, en particulier, ils ne laissent plus passer un certain nombre de constructions illicites mais pourtant tolérées par les anciennes versions.

            • [^] # Re: Syntaxe d'appel uniforme

              Posté par . Évalué à 2.

              En pratique sur le projet où je suis l'activation de c++14 ne casse pas la compile, c'est tout juste si j'ai des warning sur l'utilisation des auto_ptr, alors certes c'est un petit projet de 100000 lignes (un peu plus mais bon…); (je ne compte pas la partie java, qui n'est pas le sujet)

              mais surtout dans le cas où ça planterai la compile, c'est du code invalide qui ne passe pas, et non du code qui devient invalide du fait de la migration. Bref dans un cas c'est ce qui était déjà cassé qui nous pète à la gueule et qui allait fatalement arriver, tôt ou tard. (le passage de gcc 2.96 vers au dessus a été assez ravageur ;) )

              Il ne faut pas décorner les boeufs avant d'avoir semé le vent

              • [^] # Re: Syntaxe d'appel uniforme

                Posté par . Évalué à 3.

                Bref dans un cas c'est ce qui était déjà cassé qui nous pète à la gueule et qui allait fatalement arriver, tôt ou tard.

                Le truc, c'est quand même que les normes C++ sont tellement complexes que la plupart du temps, on est déja bien contents quand ça compile et que ça donne le résultat prévu. La plupart du temps, le code pseudo-cassé passerait sur n'importe quel compilateur (disons qu'en pratique, ça n'a jamais posé de problème).

                La réalité du terrain, c'est quand même qu'avec le temps, les vieux codes en C++ ne compilent plus du tout. Quand il ne s'agit que de passer de C++11 à C++14, oui, certes, il n'y a probablement pas grand chose à faire. Mais quand on se farcit 4 versions d'un coup sur un truc qu'on n'a pas codé, on est bon pour jeter le code.

                • [^] # Re: Syntaxe d'appel uniforme

                  Posté par . Évalué à 3.

                  Le truc, c'est quand même que les normes C++ sont tellement complexes que la plupart du temps, on est déja bien contents quand ça compile et que ça donne le résultat prévu

                  Encore heureux que ça marche mieux que ça!

                  Le projet date de 2001 (et encore il repart de code existant précédemment), et on heureusement qu'on a pas tout recodé au changement de compilo.

                  J'ai aussi fait des migration Solaris CC => GNU/Linux GCC, l'immense majorité du code compilait, juste 2/3 soucis sur les template, et des bug liés à la gestion de la mémoire plus permissive dans solaris (mais pas lié au compilo, plus à la logique du codeur )

                  typiquement :

                  char* a = b; 
                  free(b); 
                  return a;
                  
                  // ou plus marrant car plus subtile :)
                  
                  int f(int a )
                  {
                     static int* tab = malloc(...);
                     [...]
                     if( tailleInsuffisante )
                     {
                         tab = realloc();
                     }
                     [...]
                     tab[i]=f(z);
                     return tab[i];
                  }

                  Il ne faut pas décorner les boeufs avant d'avoir semé le vent

                  • [^] # Re: Syntaxe d'appel uniforme

                    Posté par . Évalué à 2.

                    J'ai aussi fait des migration Solaris CC => GNU/Linux GCC

                    Je pense qu'on ne peut pas vraiment comparer l'effet de changer de compilo sur du C ou sur du C++…

                    Le problème, c'est que je ne peux pas vraiment me rappeler du genre de choses sur lesquelles je me suis déja arraché les cheveux en changeant de version de compilo C++. Il y a évidemment des trucs qui sont assez triviaux à corriger, il suffit de prendre les erreurs une par une et de changer toujours la même chose. Par contre, je me rappelle avoir eu des erreurs sur des cast, des templates ou sur des fonctions virtuelles qui ont nécessité un refactoring du code et de la hiérarchie des classes, parfois d'ailleurs sans que je ne comprenne vraiment pourquoi ça ne passait plus. Typiquement le genre d'erreurs impossibles à corriger sur du code qui n'est pas le sien…

                    Évidemment, dans une équipe bien structurée avec des pros expérimentés, la détection des instructions illicites selon la norme est beaucoup plus facile. Dans des projets solo ou des trucs libres codés par des amateurs, il est assez facile de faire des trucs au comportement indéfini sans le savoir. Bien que ça ne soit pas recommandé, quand j'ai un doute, je fais confiance au compilo, je ne vais pas me taper la norme!

                    • [^] # Re: Syntaxe d'appel uniforme

                      Posté par . Évalué à 2.

                      c'est juste des sous partie de code que j'ai montré (très C il faut bien l'avouer), mais à la base c'est du bon vieux c++ des familles, avec des morceaux interfacé avec du fortran pour des calculs (d'autre sont en C ou C++), et le java pour l'interfaRce.

                      Il ne faut pas décorner les boeufs avant d'avoir semé le vent

                      • [^] # Re: Syntaxe d'appel uniforme

                        Posté par . Évalué à 3. Dernière modification le 15/03/16 à 17:52.

                        OK, donc si pour toi le bout de code que tu m'as montré c'est du C++, alors je comprends pourquoi tu n'as pas de problème de migration :-)

                        Au passage, ton code sert à savoir en combien de temps tu satures la RAM, ou il y a une condition de sortie quelque part? :-)

                        • [^] # Re: Syntaxe d'appel uniforme

                          Posté par . Évalué à 3.

                          Non ce code c'est du C, mais il est intégré dans du C++, oui le code a une condition de sortie, mais j'ai coupé pour montrer les point intéressant (notamment ceux qui font qu'on au mieux on a un joli segmentation fault, ou au pire un comportement aléatoire.) Ces codes ont tourné sans problème sur DEC et Solaris pendant des années, les soucis se sont révélés lors du passage à linux :D, et ils ont posé bien plus de difficulté que les soucis de syntaxe des template (notamment parce que les soucis de syntaxe sont vus à la compilation)

                          Il ne faut pas décorner les boeufs avant d'avoir semé le vent

                          • [^] # Re: Syntaxe d'appel uniforme

                            Posté par . Évalué à 2.

                            OK, je comprends (c'est juste qu'on a un peu dérivé par rapport à la discussion initiale). Après, c'est aussi la conséquence de jouer avec un langage bas niveau, on est près du système et on peut faire ce genre de choses.

Suivre le flux des commentaires

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