Wednesday, June 12 2013

Découverte de Clojure (partie 2 sur 2)

La première partie de cette article est ici. Par ailleurs, je me rends compte que la façon d'afficher les extraits de code n'est pas terrible ; voici doncla version PDF de l'article pour une mise en page peut-être un peu plus lisible.


Structures de données

La syntaxe de Clojure est donc un peu différente de celle des autres Lisp : en particulier, il y a autre chose que des parenthèses. Reprenons l'exemple de la fonction factorielle :

(defn factorial [n]
    (if (= n 0)
        1
        (* n (factorial (- n 1)))))
        
(factorial 6)
;; renvoie 720

Comme vous pouvez le constater, il y a quelques différences par rapport au Scheme dans la déclaration de la fonction et de ses paramètres (le code de la fonction lui-même étant ici identique).

D'abord, defn est le mot-clé permettant de définir une fonction et de lui assigner un nom. Il est également possible de créer une fonction anonyme avec fn, et d'utiliser def pour assigner une valeur à un nom.

Ensuite, les paramètres sont passés entre crochets. Pour comprendre les raisons de cette différence, il est utile de présenter très sommairement les principales structures de données de Clojure.

Liste

La liste est la structure de base des langages Lisp, même si c'est un peu moins le cas dans Clojure. Créer une liste se fait grâce à des parenthèses, et c'est en fait ce que vous faites lorsque vous programmez : l'instruction (+ 2 (* 2 2), soit 2 + (2 * 2) revient en effet à créer une liste contenant '+', '2', et une liste interne contenant '*', '2' et '2'.

Si vous tapez cela, cette liste est interprétée et la fonction '+' est appliquée avec comme paramètres '2' et le résultat de la fonction '*' appliquée avec comme paramètres '2' et '2'.

Pour créer une liste de données, vous devez donc faire en sorte que cette liste ne soit pas interprétée : (1 2 3) essaierait en effet d'appeler la fonction '1' — qui n'est pas un nom de symbole valide — avec comme paramètres '2' et '3'. Pour qu'une liste ne soit pas interprétée, il faut utiliser la fonction quote ou le raccourci : '.

(quote (1 2 3))
;; => (1 2 3)
'(1 2 3)
;; => (1 2 3)

Comme dans Scheme et Lisp, les trois fonctions qui permettent de manipuler les listes sont cons, qui ajoute un élément à une liste, first, qui renvoie le premier élément d'une liste (équivalent de car) et rest qui renvoie le reste de la liste (équivalent de cdr).

Il existe beaucoup d'autres fonctions utiles, mais l'objet de cet article étant de rester succinct, je ne rentrerai pas dans les détails. Ce qu'il est plus important de comprendre, c'est que les données sont représentées sous forme de liste chaînée : la liste (1 2 3) contient en fait le premier élément, 1, et un pointeur vers la liste (2 3), qui est elle-même constituée de l'élément 2 et d'un pointeur vers la liste (3), elle-même constituée de l'élément 3 et d'un pointeur vers la liste vide.

Il est ainsi possible de créer la liste (1 2 3) comme suit :

(cons 1 (cons 2 (cons 3 nil)))

Cela implique que les fonctions cons, first et rest sont très rapides (O(1)) ; en revanche, il va être plus long d'obtenir le n-ième élément d'une liste (O(n)).

Vecteurs

Les vecteurs permettent, de leur côté, d'accéder rapidement au n-ième élément d'une liste et peuvent, pour cette raison, être plus pratiques pour un certain nombre d'utilisations.

La création d'un vecteur peut se faire soit en utilisant la fonction vector, soit avec les crochets [ ]. Il est ensuite possible d'obtenir le n-ième élément (en comptant à partir de zéro) avec la fonction nth :

(def x [1 2 3])
(nth x 1)
;; => 2

Clojure ajoute encore un peu de sucre syntaxique en permettant d'utiliser directement le vecteur comme fonction, prenant alors comme paramètre l'index de la valeur à renvoyer :

(x 2)
;; => 3

Ainsi, si l'on utilise les crochets lorsque l'on définit les paramètres d'une fonction, c'est parce que defn attend la liste des paramètres sous la forme d'un vecteur.

(En pratique, c'est peut-être un peu plus compliqué que cela, car il n'est pas possible de construire le vecteur de paramètres avec la fonction vector.)

Dictionnaires

Clojure ajoute une autre syntaxe (les accolades { et }) pour créer facilement des dictionnaires (maps) (il est également possible d'utiliser les fonctions hash-map ou array-map), qui permettent d'associer un élément à un autre. Je passerai très vite sur leur utilisation, et vais me contenter de donner un exemple d'utilisation :

(def m {:chien "waf", :chat "miaou"})
(m :chien)
;; => "waf"
(m :chat)
;; => "miaou"

On crée ici un dictionnaire m associant chien et chat à leurs « cris » respectifs. :chien et :chat sont les deux clés du tableau ; le : permet de créer des mots-clés, ce qui permet d'effectuer des comparaison plus rapidement que sur les chaînes de caractère (les clés d'un dictionnaire peuvent cependant être de n'importe quel type : nombre, chaîne, mot-clé...).

La virgule n'a, en Clojure, aucune valeur syntaxique et est considérée comme un espace blanc ; elle est simplement utilisée pour que le code soit plus lisible.

Enfin, comme pour les vecteurs, un dictionnaire peut également être utilisé comme fonction prenant en paramètre une clé. (m :chat) renvoie donc la valeur associée à la clé :chat (en l'occurrence, "miaou").

Déstructuration

Pour terminer un peu sur le sucre syntaxique qu'apporte Clojure, imaginons le code suivant, qui permet de définir et afficher un point en deux dimensions :

(def point [4 2]])
(defn affiche-point [point]
        (println "x : " (point 0))
        (println "y : " (point 1)))
;; affiche :
;; x : 4
;; y : 2

Ce code marche, mais n'est pas très élégant : on doit manuellement accéder au premier élément de point, puis au second. Clojure permet cependant la déstructuration de variables, et affiche-point peut être écrit plus simplement de la façon suivante :

(defn affiche-point [[x y]]
        (println "x : " x)
        (println "y : " y))

Le mécanisme de déstructuration de Clojure est assez puissant, et fonctionne également pour les structures emboîtées, pour les dictionnaires, etc.

Tout cela fait que Clojure est moins « épuré » que peut l'être Scheme, et rend peut-être ce langage un peu plus compliqué à maîtriser ; cependant je trouve que cette « perte_» est nettement compensée par un gain de temps, de facilité et de lisibilité à l'utilisation.

Utilisation de bibliothèques Java

Comme Clojure est construit au-dessus de la JVM, il est possible d'utiliser n'importe quelle classe Java ; ce qui permet non seulement d'utiliser des bibliothèques Java dans un code écrit en Clojure, mais également de mélanger, au sein du même projet, Java et Clojure.

Pour créer un nouvel objet à partir d'une classe, deux syntaxes sont possibles :

  • (new classe paramètres)
  • (classe. paramètres)

De la même manière, il existe deux syntaxes pour appeler la méthode d'un objet :

  • (. objet méthode paramètres)
  • (.méthode objet paramètres)

En pratique, ce sont le plus souvent les deux dernières versions qui sont le plus utilisées, mais il s'agit essentiellement d'une question de goût.

L'exemple suivant utilise la bibliothèque Swing pour afficher une fenêtre intitulée « Hello ! » contenant « Hello, world ! » :

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

(def frame (JFrame. "Hello !"))
(def label (JLabel. "Hello, world !"))
(.add (.getContentPane frame) label)
;; équivaut à frame.getContentPane().add(label)
(.pack frame)
(.setVisible frame true)

Quelques autres fonctionnalités

J'ai présenté ici de façon très sommaire quelques unes des fonctionnalités de Clojure qui rendent, à mon avis, ce langage intéressant. Il y en a un certain nombre d'autres. Je me contenterais d'en évoquer rapidement certaines :

  • les protocoles, qui correspondent grosso modo à des interfaces en Java, et qui permettent par exemple que l'appel (foo bar) invoque une implémentation différente en fonction du type de bar ;
  • les multiméthodes, qui ont quelques similarités avec les protocoles, mais qui permettent un dynamic dispatch (c'est-à-dire, de choisir une implémentation différente lors de l'exécution — je ne sais pas trop la traduction recommandée de ce terme en français ?) qui n'est pas limité au type du premier argument.

Ces deux fonctionnalités (avec defrecord, qui permet de créer un nouveau type de données) sont ce qui permet d'avoir des fonctionnalités se rapprochant de la programmation orientée objet.

Pour terminer sur les fonctionnalités qu'apporte Clojure, il est intéressant de noter que ce langage prend le parti de décourager l'utilisation de variables muables (ou mutable en anglais), ce qui permet notamment d'éviter un certain nombre de problème liés à la parallélisation et à la concurrence (autrement dit, à l'utilisation d'une même zone de mémoire par deux threads s'exécutant en parallèle). Bien entendu, il est parfois indispensable d'utiliser des variables muables, auquel cas Clojure fournit un certain nombre de fonctionnalités pour faciliter l'accès partagé de ces variables ou la parallélisation du code, sans avoir à manipuler directement des threads ou à poser explicitement des verrous sur des variables.

Conclusion

Clojure est un langage que je trouve vraiment intéressant, non seulement pour les fonctionnalités qu'il apporte mais, aussi parce qu'il (ou, plus exactement, ses concepteurs) assume une certaine vision de la programmation ce qui induit une certaine cohérence plutôt qu'un agglomérat de fonctionnalités.

Le fait qu'il tourne sur la JVM (ou, tout du moins, l'implémentation principale ; il y a également des implémentations de Clojure pour CLR et pour Javascript) permet d'utiliser les bibliothèques Java existantes et n'est sans doute pas étranger au relatif succès de ce langage, même si cela n'est pas sans présenter aussi quelques inconvénients (notamment un temps de lancement un peu long et quelques problèmes techniques d'optimisation).

Pour terminer sur une conclusion plus personnelle : j'aime vraiment beaucoup Clojure, mais au-delà, je pense qu'en apprendre plus sur la «programmation fonctionnelle» (sans prétendre être une gourou) m'a vraiment apporté beaucoup. C'est une façon d'aborder les choses assez différente et qui demande un certain investissement (plus, je trouve, que l'approche orientée objet), mais je trouve que le jeu en vaut la chandelle. Bref, tout ça pour dire que la programmation fonctionnelle n'est pas juste un truc abscons de chercheurs ou de spécialistes qui utilisent des mots compliqués, et je pense qu'un langage comme Clojure (ou, par exemple, Scala, pour citer un autre langage tournant au-dessus de la JVM qui facilite la programmation fonctionnelle, même si l'approche est assez différente) peut permettre d'utiliser cette approche dans des applications «de la vie réelle».

Tuesday, June 11 2013

Découvert de Clojure (partie 1 sur 2)

Voici la première moitié d'un petit article sans prétention de présentation du langage Clojure, un langage que j'ai découvert il n'y a pas très longtemps et que je trouve vraiment intéressant. Introduction Cet article a pour objectif de présenter un peu le langage de programmation Clojure, que j'ai  […]

Continue reading

Wednesday, October 17 2012

Introduction aux threads en Java

Cela fait un certain temps que je n'ai rien posté, notamment parce que je suis en train de suivre une formation Java/J2EE qui me prend pas mal de temps. Dans le cadre de cette formation, j'avais à faire un exposé de présentation sur l'utilisation des threads en Java. Si cela intéresse des gens,  […]

Continue reading

Tuesday, August 7 2012

My first attempt at a gnome-shell theme

Screenshot_from_2012-08-06_23_57_32.png

The real problem I had with the default configuration of Gnome-shell, was its theme. That might seem completely trivial for people who had *real* and serious user issues, like having to press "tab" to click on power-off, but I'm not a big fan of black and white. Now, there are a few  […]

Continue reading

Saturday, August 4 2012

I like Gtk's Application menu

gnome-shell.png

A new (I think it appeared in 3.4) feature of Gtk+ I like is the application menu. Basically, it allows to set a menu for your application whose display will depend on the way the window manager chooses to render it; in Gnome 3, it will be displayed in the top panel, whereas in most other WM it will  […]

Continue reading

Friday, August 3 2012

Tiny 'Nux Tarot: sortie de la version 0.3

gnome-shell.png

Je viens d'uploader la version 0.3 de Tiny 'Nux Tarot, que vous pouvez trouver sur la page de téléchargements du projet github. Les changements sont à peu près les suivants : La "Garde sans" et la "Garde contre" sont maintenant gérées. Il y a une table des scores au lieu d'un  […]

Continue reading

Tiny 'Nux Tarot: 0.3 released

gnome-shell.png

I just uploaded the 0.3 version of Tiny 'Nux Tarot, which you can find on the download page of the github project. Changes are the following: "Garde sans" and "Garde contre" are now handled. A table of score is now displayed instead of just a string. Game messages are now  […]

Continue reading

Thursday, August 2 2012

Gettext and me

So, I'm trying to add internationalization to my current little project, Tiny 'Nux Tarot (and to learn how it works), and really, it bores me. I mean, I was pretty sure I understood the base of gettext and it was quite simple: first, you add a gettext call to all transtlatable strings in the code,  […]

Continue reading

- page 1 of 2