Sortie de Clojure 1.6

Posté par  (site web personnel) . Édité par BAud, ZeroHeure, Davy Defaud, palm123, patrick_g, Bruno Michel et Jiehong. Modéré par patrick_g. Licence CC By‑SA.
Étiquettes :
31
1
avr.
2014
Programmation fonctionnelle

Le 25 mars, Clojure est sorti en version 1.6, l'occasion de se pencher un peu sur ce langage.

Clojure est un langage de programmation fonctionnel dérivé de Lisp tournant au-dessus de la Machine Virtuelle Java, des ports existant également pour Javascript et pour le Common Language Runtime de .NET.

Sommaire

Présentation du langage

Conçu par Rich Hickey, et présenté pour la première fois en 2007, Clojure est un langage dont l'objectif est d'être une variante moderne de Lisp. S'il garde certaines des spécificités de cette famille de langages (considérer le code comme des données, ce qui permet notamment un usage sophistiqué de macros), il a comme particularité d'avoir une approche plus orientée vers la programmation fonctionnelle (décourageant l'utilisation de variables muables) et vers la concurrence. Par ailleurs, Clojure est conçu pour être en « symbiose » avec son hôte (dans la plupart des cas, la machine virtuelle Java) et permet d'utiliser facilement le code existant et de mêler au sein d'un même projet du code écrit en Java et en Clojure.

Installation

La manière la plus simple d'utiliser Clojure est d'utiliser Leiningen. Cet outil (en ligne de commande) permettra entre autres choses de récupérer les dépendances nécessaires (dont Clojure lui-même, qui prend la forme d'une bibliothèque Java), de créer l'arborescence pour un nouveau projet et de compiler ce projet.

Cet outil permet également (via lein repl) de lancer un REPL (Read-Eval-Print-Loop) qui permet de manipuler interactivement du code. Cf. le tutorial Clojurer des regexps avec Java, en Lisp :) qui présente un peu plus cet aspect-là.

Pour tester Clojure sans avoir à installer quoi que ce soit sur sa machine, il est également possible d'utiliser le site Try Clojure, qui fournit un REPL accessible par le web.

Un peu de syntaxe

La syntaxe de Clojure ne dépaysera pas les habitués de Lisp, ou de ses variantes.

(println "Hello, world !")
;; affiche "Hello, world !"

(+ 2 2)
;; renvoie 4

(* 3 2)
;; renvoie 6

Clojure est dynamiquement typé : c'est au moment de l'exécution qu'on vérifie qu'une valeur est bien du type attendu. Comme le langage tourne sur la machine virtuelle Java, un certain nombre de types de base, comme les entiers ou les chaînes de caractères, viennent directement de Java, comme on peut le constater avec la fonction type :

(type 2)
;; renvoie java.lang.Long
(type "Hello, world !")
;; renvoie java.lang.String

Un petit mot sur la syntaxe, directement héritée de Lisp. Dans tous les exemples ci-dessus, on construit, grâce aux parenthèses, une liste contenant un certain nombre d'éléments. Cette liste est ensuite évaluée, et le premier élément est interprété comme la fonction à appeler : dans le premier cas, println, dans les suivants + et * (qui ne sont pas considérés comme des caractères spéciaux). Les autres éléments de la liste correspondent aux arguments donnés à cette fonction.

Pour créer une liste, il suffit de demander à ce qu'elle ne soit pas évaluée, via quote, ou le caractère spécial ' :

(quote (1 2 3 4 5))
;; renvoie (1 2 3 4 5)

'(1 2 3 4 5)
;; identique

C'est le principe fondateur de Lisp (le nom Lisp vient de LISt Processing) : le code est constitué de données, c'est à dire que tout le code qu'on écrit est en réalité constitué de listes qui vont être évaluées. Il s'agit d'une structure assez simple, celle de liste simplement chaînée, sur laquelle on peut effectuer un certain nombre d'opérations basiques :

  • (cons x une-liste) ajoute l'élément x au début de la liste une-liste ;
  • (first une-liste) retourne le premier élément d'une-liste (correspond à car dans Lisp) ;
  • (rest une-liste) retourne une liste comprenant tous les éléments d'une-liste, sauf le premier (correspond à cdr dans Lisp).

Quelques exemples :

(def une-liste '(1 2 3 4 5))
;; définit une variable, une-liste, contenant (1 2 3 4 5)

(first une-liste)
;; renvoie 1

(rest une-liste)
;; renvoie (2 3 4 5)

(cons 0 une-liste)
;; renvoie (0 1 2 3 4 5)

Particularités de Clojure

Structures de données

En Lisp, la liste est donc la structure hégémonique que l'on retrouve partout et qui est l'élément de syntaxe principal du langage (les fameuses parenthèses). Clojure garde le même principe, mais avec quelques nuances. Ainsi, pour déclarer une fonction :

(defn doubler [x]
  (* 2 x))

(doubler 2)
;; renvoie 4

Les crochets autour des paramètres de la fonction (en l'occurrence, x) ont une signification bien particulière : ils indiquent qu'il ne s'agit pas d'une liste ordinaire, mais d'un vecteur. Clojure se différencie en effet de son aîné en proposant « en dur » d'autres structures de données, comme les vecteurs : 

[1 2 3 4]

À première vue, un vecteur peut ne pas sembler très différent d'une liste, mais accéder à un élément ou en ajouter un n'ont pas les mêmes impacts en termes de performances :

  • avec une liste, il est possible d'accéder au premier élément ou d'ajouter un élément en début de liste en temps constant, mais pour accéder au n-ième élément il faudra parcourir la liste depuis le début ;
  • à l'inverse, avec un vecteur, il est possible d'accéder à n'importe quel élément avec un temps constant ; avec l'implémentation de Clojure, il en est de même pour ajouter un élément à la fin du vecteur.

Clojure fournit également une syntaxe pour les ensembles :

#{"bleu" "rouge" "vert"}

ainsi que pour les dictionnaires :

{1 "bleu"
 2 "rouge"
 3 "vert"}

Un accent sur l'immuabilité

À première vue, ces ajouts peuvent être vus comme du simple sucre syntaxique, puisque ces structures de données peuvent également être créées par l'appel à des fonctions, repectivement :

(vector 1 2 3 4)
;; crée un vecteur

(set '("bleu" "rouge" "vert"))
;; crée un ensemble

(array-map 1 "bleu" 2 "rouge" 3 "vert")
;; crée un dictionnaire

Cependant, l'intérêt de ces structures n'est pas uniquement d'avoir du code comportant un peu moins de parenthèses et plus de caractères différents. Leur particularité est qu'à l'instar des listes, ces structures de données sont persistantes. Clojure met en effet un fort accent sur l'immuabilité. Par exemple, pour ajouter une valeur à un vecteur, plutôt que de modifier le vecteur lui-même, on va créer un nouveau vecteur, via la fonction conj :

(def mon-vecteur [1 2 3 4])
(conj mon-vecteur 5)
;; renvoie [1 2 3 4 5]
mon-vecteur
;; contient toujours [1 2 3 4]

Si garantir cette immuabilité pour toutes les structures de données du langage a un coût (en termes de performances), cela présente un certain nombre d'avantages, notamment en programmation concurrente : en n'utilisant que des variables immuables, on n'a pas à se soucier de ce qui peut arriver si un autre thread modifie les données sur lesquelles on est en train de travailler.

Bien entendu, il est parfois indispensable qu’une variable puisse être modifiée. Clojure fait le choix de proposer plusieurs solutions pour ce cas, en fonction des besoins (en termes de concurrence et de parallélisme). Ainsi, si modifier une variable par le biais du mot‐clé def n’entraînera un changement qui ne sera visible qu’à l’intérieur du même thread, il existe également atom, agent et ref qui permettent de partager des données entre plusieurs threads, avec des mécanismes un peu différents :

  • atom s'assure simplement, comme son nom l'indique, d'une modification atomique de la variable, garantissant qu'elle soit toujours dans un état cohérent. C'est notamment utile lorsqu'une modification de cette variable n'a pas à être coordonnée avec la modification d'autres variables ;
  • ref permet une modification coordonnée de plusieurs variables, de manière synchrone ;
  • agent permet une modification coordonnée de plusieurs variables, de manière asynchrone.

Du sucre syntaxique

Clojure fournit également du sucre syntaxique pour permettre une utilisation plus facile de ces structures de données. Ainsi, vecteurs comme dictionnaires peuvent être utilisés comme des fonctions, renvoyant la valeur correspondant au paramètre qui leur est passé :

(def v [1 2 3 4])
(def d {1 "bleu"
        2 "rouge"
        3 "vert"})

(v 2)
;; renvoie 3
(d 1)
;; renvoie "bleu"

En ce qui concerne les dictionnaires, leurs clés correspondent souvent à des mots-clés, des identifiants commençant par le caractère :. Il est également possible d'utiliser ces mots-clés en guise de fonction, afin d'obtenir la valeur correspondante dans le dictionnaire passé en argument :

(def point {:x 3
            :y 4})
(:x point)
;; renvoie 3

Un autre élément de syntaxe intéressant pour accéder au contenu d'une structure de données est la déstructuration, qui permet de lier des variables à un élément d'une structure plutôt qu'à son ensemble. Par exemple, si l'on désire créer une fonction affiche-point qui prend en paramètre un vecteur contenant l'abscisse et l'ordonnée, plutôt que d'accéder manuellement au premier et au second élément du vecteur, on peut écrire le code suivant :

(defn  affiche-point [[x y]]
  (println "Coordonnées :" x ";" y))

(affiche-point [4 2])
;; affiche "Coordonnées : 4 , 2"

Cette déstructuration fonctionne pour les listes et les vecteur, mais également pour les dictionnaires.

Interopérabilité avec Java

Un atout de Clojure est son interopérabilité avec Java, qui lui permet d'utiliser les multiples bibliothèques existantes. Cela se fait très simplement : (classe. parametres) crée une nouvelle instance de la classe, tandis qu'on accède à une méthode ou à un attribut public en faisant (.methode objet). Un exemple concret, pour afficher une fenêtre via la biblièthèque Swing :

(import '(javax.swing JFrame JLabel)) ;; importe JFrame et JLabel

(def frame (JFrame. "Hello !"))
;; JFrame frame = new JFrame ("Hello !");
(def label (JLabel. "Hello, world !"))
;; JLabel label = new JLabel ("Hello, world !");
(.add (.getContentPane frame) label)
;; frame.getContentPane().add(label)
(.pack frame)
;; frame.pack ()
(.setVisible frame true)
;; frame.setVisible (true)

La macro proxy permet également de créer des objets étendant des classes ou implémentant des interfaces. Pour rester dans l'interface graphique, si l'on veut que le programme affiche "Plop !" lorsqu'on clique sur un bouton :

(def button (javax.swing.JButton. "Plop"))
;; crée le bouton
(.addActionListener 
 button
 (proxy [java.awt.event.ActionListener] []
        (actionPerformed [e]
         (println "Plop !"))))
;; on implémente la méthode actionPerformed de l'interface ActionListener

Pas (vraiment) de modèle objet

Hormis les mécanismes d’interopérabilité évoqués ci‐dessus, Clojure ne met pas en avant de mécanisme pour faire de la programmation objet au sens strict du terme. Cependant, il existe un certain nombre d’outils permettant d’obtenir sensiblement le même type de fonctionnalités, notamment en termes de polymorphisme.

L’un de ces outils est la notion de protocoles, similaires aux interfaces en programmation objet. Un protocole va en effet définir un certain nombre de fonctions s’appliquant aux éléments d’un type donné. Par exemple, pour définir un protocole contenant une seule fonction, doubler :

(defprotocol Doubler
  (doubler [this]))

Il est ensuite possible d'implémenter ce protocole, soit pour de nouveaux types (voir ci-dessous), soit pour des types existants. Par exemple, si on veut implémenter ce protocole pour les nombres et les chaînes de caractères :

(extend-protocol Doubler
  java.lang.Number
   (doubler [x] (* 2 x))
  java.lang.String
   (doubler [x] (str x x)))

(doubler 21)
;; renvoie 42
(doubler "coin")
;; renvoie "coincoin"

En complément des protocoles, Clojure fournit également un outil pour créer de nouveaux types : defrecord (il existe également deftype, un peu plus bas niveau). Cela crée concrètement une classe Java contenant les attributs passés en paramètres, tout en permettant une utilisation similaire à celle des dictionnaires :

(defrecord Surface [longueur largeur])

(def s (Surface. 2 2))
;; On crée un nouvel objet de la classe Surface

(.longueur s)
;; On peut accéder aux attributs en utilisant les méthodes d'interopérabilité Java...
(:largeur 2)
;; ... ou comme s'il s'agissait de clés pour un dictionnaire

Il est possible, lors de la création d'un nouveau record, d'implémenter directement certaines interfaces :

(defrecord Surface [longueur largeur])
  Doubler
  (doubler [this] (Surface. (* 1.41 longueur)
                            (* 1.41 largeur))))
(def s (Surface. 2 2))
(:longueur (doubler s))
;; -> renvoie 2.82

Les protocoles permettent donc une forme de polymorphisme assez similaire aux interfaces ou à l'héritage dans la programmation orientée objet. Le choix de la méthode à appeler en fonction du type de l'objet (single dispatch) n'est pas toujours optimal, et il est parfois utile d'avoir un choix de la méthode à appeler qui puisse être arbitraire (multiple dispatch). Pour cela, Clojure fournit également un mécanisme, les multiméthodes.

Changements apportés par la version 1.6

La version 1.6 de Clojure apporte peu de réelles nouveautés, le langage ayant maintenant acquis une certaine maturité. Un certain nombre de fonctionnalités qui étaient auparavant considérées comme alpha ont été « promues » et sont donc maintenant considérées comme stables, notamment :

  • la création de types via defrecord ou deftype ;
  • les transients (permettant de considérer une variable immuable comme muable tant que c'est au sein de la même fonction) ;
  • les watches, permettant d'appeler automatiquement une fonction lorsqu'une variable est modifiée ;
  • les promises, qui permettent de ne pas bloquer le thread à la création de la variable, mais uniquement lorsqu'elle est lue.

Au rang des nouveautés, cette version fournit des interfaces minimales pour permettre l'accès à Clojure depuis d'autres langages tournant sur la machine virtuelle Java. Elle propose également quelques fonctions supplémentaires (some?, if-some et when-some) destinées à simplifier l'écriture d'expressions conditionnelles courantes. Sinon, la majorité des changements concerne des améliorations de mécanismes existants (comme la déstructuration, ou les mécanismes de hachage utilisés par un certain nombre de structures de données), sans compter de nombreuses corrections de bugs.

On notera également que Clojure requiert dorénavant Java 6 (ou supérieur), tandis que la version précédente (mais pas toutes les bibliothèques) pouvait encore tourner sur la version 5 de Java.

Un certain nombre de développements intéressants concernant Clojure ont lieu en dehors du cœur même de Clojure. Il existe notamment un certain nombre de projets pour compiler du code Clojure vers d'autres plate-formes. Parmi ceux-ci, ClojureScript, le compilateur Clojure pour le langage javascript, a acquis une certaine stabilité et une relative popularité. Dans un autre registre, le projet Typed Clojure vise à ajouter un typage statique optionnel, à l'instar de ce qui se fait pour d'autres langages typés dynamiquement.

Aller plus loin

  • # Tuto

    Posté par  (site web personnel) . Évalué à 4.

    Merci pour cette présentation, je trouve Clojure vraiment intéressant pour voir à quoi le LISP peut ressembler, vu les éloges qu'on en fait. Comme prévu, ça change de la programmation impérative.

    Pour ceux qui veulent aller plus loin, il y a ce tutorial qui permet d'aller assez loin dans la découverte de Clojure.

  • # Intéressant les transients

    Posté par  . Évalué à 3.

    L'initialisation des constantes (*) est un problème sur lequel j'ai vu bien des débats, les transients ont l'air d'une solution intéressantes, ne nécessitant peut-être nécessairement une copie des données..

    *:je n'aime pas le mot immutable qui est verbeux et utilisé uniquement parce que le C++ a dénaturé le mot-clef const qui aurait du être read_only_view(view) au lieu de const.

  • # 4Clojure

    Posté par  (site web personnel) . Évalué à 3.

    Pour ceux qui veulent s'essayer à Clojure, il y a aussi https://www.4clojure.com/ qui est plutôt très sympa :-)
    C'est une liste de problèmes à résoudre en clojure. Pour ma part un mélange de doc, de 4Clojure et du livre "Clojure Programming" m'ont pas mal aidé au début (moins d'une semaine avant de commencer à vraiment coder avec)

    C'est un langage plutôt sympa, très agréable et le côté fonctionnel c'est cool (c'est le premier langage fonctionnel que j'utilise, j'avais juste fait un peu de emacs lisp mais vraiment pas grand chose et surtout sans comprendre vraiment)

    Et le côté jvm est plutôt cool aussi, ça permet d'utiliser pas mal de briques existantes mais avec un langage beaucoup plus concis et expressif.

    • [^] # Re: 4Clojure

      Posté par  . Évalué à 1.

      Je me rappelle avoir appris le LISP à la fac, sous différentes variantes : scheme, emacs-lisp, théorie du lisp.

      J'avais beaucoup aimé le scheme en DEUG, pour son côté "pur". On avait implémenté pas mal d'algorithmes classiques avec.

      J'aimerais vraiment m'y remettre, mais comme j'ai fait depuis uniquement du procédural et objet, j'ai du mal à remettre mes neurones dans le sens fonctionnel. Quels sont les exemples d'applications concrètes dans lesquelles il est le plus intuitif d'utiliser du fonctionnel, et du Clojure en particulier ?

      • [^] # Re: 4Clojure

        Posté par  (site web personnel) . Évalué à 2.

        Quels sont les exemples d'applications concrètes dans lesquelles il est le plus intuitif d'utiliser du fonctionnel, et du Clojure en particulier ?

        Heu… toutes ?
        Mais disons que toute application où tu dois gérer des ensembles de données (où tu peux représenter les données par des listes, des listes de listes, des maps, etc) s'y prête plutôt bien.
        Par exemple l'application pour laquelle j'ai appris clojure est une application de comptabilité donc essentiellement un ensemble de lignes d'écritures.
        Mais de manière générale, plus je code en fonctionnel plus j'ai envie de rajouter du fonctionnel dans mes autres codes (que ce soit en js, en ruby ou en c++ par exemple).

        • [^] # Re: 4Clojure

          Posté par  . Évalué à 1.

          Bon ben justement je suis en plein dévs pour transférer des lignes de compta d'une base à une autre. Je crois que c'est l'occasion d'essayer tout ça…

          Merci !

        • [^] # Re: 4Clojure

          Posté par  . Évalué à 1.

          salut,
          justement puisque tu connais déjà la bête, tu la situerais où par rapport à un lisp disons plus classic (sans jvm) du point de vues de l'utilisabilité ?
          Parce-qu’autant je trouve le langage pas mal du tout, autant la jvm est un peut trop pour moi :)

          Bien sur cette question n'a de sens que si tu a déjà utilisé un lisp dans le cas contraire j'aimerais connaître ton ressenti, concernant clojure par rapport aux autres langages de la jvm (fonctionnel ou pas).
          merci.

          KISS

          • [^] # Re: 4Clojure

            Posté par  (site web personnel) . Évalué à 2.

            tu la situerais où par rapport à un lisp disons plus classic (sans jvm) du point de vues de l'utilisabilité ?

            Aucune idée je ne fais pas de lisp.

            autant la jvm est un peut trop pour moi :)

            oO

            Autant java est au mauvais langage, autant la jvm s'en sort plutôt bien, est efficace et plutôt rapide. Franchement aujourd'hui (ça fait bien longtemps qu'on est plus sur de l'interprété) pas vraiment de raison de refuser la jvm (enfin sauf si on parle de contraintes genre du déploiement, de l'embarqué, etc)

            j'aimerais connaître ton ressenti, concernant clojure par rapport aux autres langages de la jvm (fonctionnel ou pas)

            Mais en fait je m'en fiche un peu que ce soit sur jvm ou non.
            Après je ne fais pas de scala, je vais faire du ruby et non du groovy ou du jruby et je trouve que (même si ça s'améliore) java reste un langage d'une pauvreté attristante et d'un intérêt plus que limité.
            Clojure c'est bien, j'ai adoré coder avec, mais ça aurait été sans la jvm c'était pareil.

            Mais pour revenir à la question, si c'est pour coder sur la jvm je pense que je considèrerais beaucoup plus clojure comme une vrai alternative avant de me lancer par défaut sur du java (ce n'est pas moi qui avait fait le choix clojure dans ce projet) et sauf contrainte spécifique j'éviterais simplement java.

            • [^] # Re: 4Clojure

              Posté par  (site web personnel) . Évalué à 1.

              À propos de la JVM : il y a quand même un petit truc spécifique (enfin, il y a peut-être le même problème pour la machine virtuelle Microsoft) concernant la récursion terminale (Tail Call Optimization). Je maîtrise pas les détails techniques, mais en gros si j'ai bien compris c'est pas possible de faire ça sur la la JVM. Concrètement ça a rien de dramatique à mon avis (en gros il faut juste utiliser la forme spéciale, recur pour faire une récursion terminale si on veut éviter de surcharger la pile). Ça me semble pas une différence vitale et j'avoue que je comprends pas forcément tous les enjeux, mais j'ai vu pas mal de discussions dessus :)

              Sinon, c'est pas uniquement la faute à la JVM (cela dit je crois que le problème se pose moins pour la version Javascript), mais Clojure est quand même vraiment long à démarrer, ce qui est pas très gênant pour une appli qui tourne lontemps (serveur web, GUI, …) mais est plus embêtant pour un usage en mode ligne de commande où on n'a pas forcément envie d'attende ~1s pour une commande simple. Je crois qu'il y a des réflexions pour améliorer ça, donc des chances pour que ça s'améliore dans pas trop longtemps, à voir.

              Après je trouve que la JVM a aussi des avantages. Pour des petits projets (où t'as pas forcément les moyens d'être intégré dans des distributions ou de fournir des paquets pour chaque distrib), je trouve qu'avoir la possiblité (certes pas optimale, j'en conviens) de fournir un gros fichier .jar avec tout ce qu'il fait, ça rend quand même l'utilisation plus facile.

              • [^] # Re: 4Clojure

                Posté par  . Évalué à 3.

                À propos de la JVM : il y a quand même un petit truc spécifique (enfin, il y a peut-être le même problème pour la machine virtuelle Microsoft) concernant la récursion terminale (Tail Call Optimization)

                La récursion terminale ne pose aucun problème sur la JVM. C'est un choix du langage ou non de l'implémenter. Scala le fait pas exemple.

                Pour le TCO par contre ce n'est actuellement pas possible sur la JVM. Ca n'a jamais été une priorité et ca n'a donc pas été fait. En gros les problèmes à résoudre sont:
                - La spec actuelle dit qu'il ne faut pas jouer avec la stack frame courante, c'est pourtant ce que le TCO fait
                - On perd les stack trace. Ce n'est pas un problème en soit mais ca change un peu de ce qui existe actuellement
                - Actuellement certains mécanisme repose sur les stack frame notament le bytecode verifier et les security manager. Ca peut se résoudre mais ca demande du travail.

                Donc en gros oui la JVM à clairement des limites pour le TCO. Maintenant en pratique ce n'est pas non plus extrêment bloquant il y a souvent des alternatives.

                John rose avait bossé sur un patch et Oracle en parlait dans sa roadmap à long terme en 2012 mais je ne sais pas si c'est toujours vivant ou pas:
                https://wiki.openjdk.java.net/display/mlvm/TailCalls
                http://wiki.jvmlangsummit.com/Why_Tailcalls

Suivre le flux des commentaires

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