Il y a 6 années · 6 minutes · Java / JEE

Devoxx – Cracking clojure

Clojure est un dialecte de Lisp fonctionnant sur la JVM. Dans cette présentation, Alex Miller s’adresse aux développeurs sans expérience préalable du langage. Son but est d’en présenter les concepts de base et de montrer la puissance et l’expressivité qui lui ont fait choisir Clojure comme langage principal chez son employeur actuel.

Alex revient sur les paradigmes de base du langage :

  • fonctionnel
  • dynamique
  • compilé
  • favorisant l’immutabilité et l’évaluation paresseuse.

Il ouvre le capot du langage en nous expliquant le fonctionnement du compilateur :

En particulier, Clojure dispose d’un système de macros, qui seront détectées et transformées en un bloc de code par le reader.

Types de base

On retrouve sans surprise les équivalents des types Java : booléens, entiers et flottants. Parmi les particularités du langage, on peut noter que Clojure gère de manière transparente la conversion vers des types de précision arbitraire quand cela est nécessaire. Comme en Javascript, n’importe quel type peut être utilisé en tant qu’expression booléenne. Mais seuls nil et false sont considérés comme valeurs falsy, toutes les autres valeurs sont évaluées à true (y compris 0 et la liste vide ()).

Collections

Clojure fournit un ensemble de structures de base, que le développeur est encouragé à réutiliser au maximum :

(def myList '(1 2 3 4))        ;; liste chaînée, l'apostrophe est expliquée plus loin

(def myVector [1 2 3 4])
(get myVector 1)               ;; accès par index (renvoie 2)

(def mySet #{1 2 3 4})

(def myMap {:a 1, :b 2, :c 3}) ;; les clés sont des symboles (préfixés par ':' comme en Ruby)
(get myMap :a)                 ;; récupération par clé (renvoie 1)
(myMap :a)                     ;; notation plus concise
(:a myMap)                     ;; les symboles peuvent également se comporter comme des fonctions d'extraction

Au-dessus de tous ces types, on trouve la notion de séquence. Une séquence correspond à une liste logique composée d’un premier élément et d’une séquence contenant les éléments restants :

(first '(1 2 3 4)) ;; renvoie 1
(rest  '(1 2 3 4)) ;; renvoie (2 3 4)

Cette abstraction est très utilisée dans Clojure et ne se limite pas aux collections : ainsi, un fichier est une séquence de lignes, un répertoire une séquence de fichiers, un ResultSet une séquence d’enregistrements dans une base de données, etc.

La puissance des séquences réside dans les fonctions d’ordre supérieur qu’elles fournissent, comme nous allons le voir ci-dessous.

Fonction

Nous avons déjà vu quelques exemples d’applications de fonctions :

(+ 41 1) ;; renvoie 42

Un point intéressant à noter est qu’une expression est elle-même une liste : par défaut, Clojure invoque le premier élément en lui passant les valeurs issues de l’évaluation des éléments restants (il est possible de désactiver cette évaluation en précédant l’expression d’une apostrophe, comme nous l’avons fait pour écrire les listes littérales dans les exemples précédents).

Le code est donc écrit comme des données et les données comme du code ; on dit que le language est homoiconique (un mot à retenir pour briller lors de votre prochain dîner geek).

La définition de fonctions se fait avec le mot-clé fn :

(def square (fn [x] (* x x)))

;; ou sous forme raccourcie :
(defn square [x] (* x x))

Les séquences fournissent des fonctions d’ordre supérieur, c’est-à-dire auxquelles on peut passer d’autres fonctions (généralement des fonctions anonymes définies à la volée) :

;; Applique une fonction à tous les éléments d'une séquence
(map (fn [x] (+ x 1)) '(1 2 3 4))    ;; retourne (2 3 4 5)

;; Filtre les éléments d'une séquence selon un prédicat
(filter (fn [x] (> x 2)) '(1 2 3 4)) ;; retourne (3 4)

Au passage, les structures de données sont généralement immuables (les expressions ci-dessus renvoient une nouvelle liste). Clojure favorise également l’évaluation paresseuse et est capable de travailler avec des séquences infinies.

Alex dispose maintenant de suffisamment d’éléments pour nous présenter un exemple un peu plus concret :

;; Compte les lignes d'un fichier:
(defn line-count [file]
  (count (line-seq  (reader file))))

;; Compte les lignes de chaque fichier d'un répertoire:
(defn file-counts [dir]
  (map line-count
    (filter #(. % isFile)
      (file-seq (file dir)))))

;; Compte le nombre de lignes total dans le répertoire
(defn total-count [dir]
  (reduce + (file-counts dir)))

Sans expliquer en détail le code, on retrouve la concision et l’expressivité communément présentées comme des avantages clés des langages fonctionnels.

Objet

Dans les cas les plus simples, un objet peut être une simple map, mais Clojure permet également de créer un type d’enregistrement spécifique :

;; Un exemple de circonstance à Devoxx:
(defrecord Beer [name brewery])

;; Instanciation et affectation à une variable
(def beer (->Beer "Tremens" "Delirium"))
;; Accès à une propriété (comme pour les maps, plusieurs syntaxes)
(get beer :name)
(:name beer)
(beer :name)

Alex présente trois méthodes pour faire du polymorphisme :

  1. dans le cas le plus simple, donner le même nom aux propriétés communes à plusieurs types, elles seront ainsi accédées de manière transparente quel que soit le type ;
  2. les multimethods permettent de définir une fonction et de spécifier des implémentations différentes selon le type sur lesquels elle est invoquée ;
  3. enfin, les protocoles définissent un contrat, qu’il faut implémenter pour chaque type participant :
(defprotocol Beverage
  (source [beverage]))

;; Pour une bière, la fonction source est implémentée en invoquant le getter de la propriété 'brewery'
(extend-protocol Beverage Beer
  (source [beer] (:brewery beer)))

;; Pour un vin, il s'agit de la propriété 'winery'
(extend-protocol Beverage Wine
  (source [wine] (:winery wine)))

On remarque ici une différence importante avec la notion d’objet en Java : la déclaration des données est séparée de celle des comportements. Bien que cette approche réduise l’encapsulation, elle est aussi plus flexible : ainsi, le développeur peut créer un protocole et le faire implémenter par des types qui ne sont pas forcément sous son contrôle (y compris les types de base de Clojure). Les amateurs de Haskell reconnaîtront ce mécanisme : les protocoles sont les équivalents des typeclasses.

Concurrence

Alex aborde rapidement les fonctionnalités offertes pour la programmation concurrente :

  • les atoms sont similaires aux AtomicReferences de Java ;
  • les refs sont utilisées uniquement à l’intérieur de transactions (Clojure dispose d’une mémoire transactionnelle logicielle) ;
  • les agents sont des composants isolés communiquant à travers des files de messages, comme les acteurs d’Erlang ou Scala.

Démo

La session se termine par une session de coding pré-enregistrée, montrant l’implémentation du jeu de la vie de Conway. Les résultats intermédiaires sont affichés après chaque fonction développée, ce qui explicite bien comment l’application est assemblée en combinant des blocs de base.

L’affichage des résultats est un peu austère, mais Alex nous révèle qu’il a codé une interface graphique basée sur Processing pendant la keynote du matin (comment ça, la keynote n’était pas intéressante ?) et nous la démontre sous les applaudissements de la foule.

Pourquoi Clojure ?

Au final, Alex a réussi son pari en offrant un tour d’horizon de Clojure sans perdre son audience. La présentation donne envie d’aller plus loin dans l’expérimentation du langage.

Cependant, beaucoup des aspects présentés rappellent d’autres langages fonctionnels, en particulier Scala. Interrogé après la présentation, Alex explique que son équipe avait effectivement préselectionné Scala et Clojure sur la base des critères suivants :

  • un langage sur la JVM ;
  • permettant un niveau d’abstraction élevé ;
  • avec un bon support pour la programmation concurrente.

Après quelques tests, c’est finalement Clojure qui l’a emporté, principalement car c’est le langage avec lequel l’équipe a ressenti le plus d’affinité.

Nicolas Jozwiak

Nicolas est un ingénieur d’études confirmé disposant de 10 ans d’expérience de conception et développement sur technologies Java/JEE. Son parcours chez un éditeur avant son entrée chez Xebia lui a notamment permis de développer de solides compétences dans le domaine de la qualité et de l’industrialisation (tests, intégration continue, gestion de configuration, contrôle qualité). Nicolas est Directeur de projet chez Xebia Studio et bénéficie d’une expérience très solide de mise en place des méthodes agiles et d’accompagnement d’équipe sur le terrain. Il est reconnu pour son approche pragmatique, proactive et pédagogique.

4 thoughts on “Devoxx – Cracking clojure”

  1. Publié par Christophe Grand, Il y a 6 années

    Waouh, mon premier pingback depuis un site français ! #nul-nest-prophete-en-son-pays

    Pour info il y a un groupe naissant d’utilisateurs Clojure sur Paris, un sur Genève, et un à venir sur Lyon (si des lyonnais intéressés qu’ils me contactent).

  2. Publié par Teryk, Il y a 6 années

    Bonjour Christophe,

    Existe-t-il bien un Clojure User Group sur Genève ?
    Je serais intéressé.

    Comment vous contacter ?

  3. Publié par Teryk, Il y a 6 années

    Merci Christophe

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *