Intéressé par des cours d'informatique en ligne ?
Visitez mon nouveau site https://www.yesik.it !

Pile Java originale.png

Pile Java originale — La pile Java présentée par Sun en 1995: Le programmeur code en utilisant le langage de haut niveau Java. Un compilateur est chargé de convertir le code source en bytecode Java. Celui-ci peut ensuite être exécuté sur différentes architectures grâce à un environnement d'exécution spécifique, composé notamment de la machine virtuelle Java.

Pile Java moderne.png

Pile Java moderne — Si la plate-forme Java est un succès indéniable, l'idée du langage Java comme un langage universel pour tous types d'applications a fait long feu. Aujourd'hui, le choix pour le développeur est beaucoup plus large.

Ainsi, non seulement on trouve des compilateurs pour des langages alternatifs. Mais également toute une variété de langages interprétés dont l'exécution ne passe pas par la case bytecode. A tel point que quelque soit votre langage de prédilection, il y a de grandes chances qu'il en existe (au moins) une variante dans le monde Java!

Face à ce constat, Sun a décidé depuis quelques années d'officialiser cette évolution de la plate-forme Java. Tout d'abord en incorporant le support dans Java 6 pour les langages de scripts via JSR 233 — Scripting for the Javat™ Platform. Puis, à partir de Java 7, avec le support des langages dynamiques compilés au niveau de la JVM (JSR 292 — Supporting Dynamically Typed Languages on the Java™ Platform). Autant dire que le mouvement n'est pas prêt de s'inverser...

Il est de bon ton aujourd'hui de distinguer la plate-forme Java du langage Java. En effet, si les deux étaient intimement liés dans le modèle initial proposé par Sun en 1995, il faut bien avouer que les choses ont bien changé depuis! De nos jours, on peut développer pour la plate-forme Java avec des dizaines – voire des centaines – de langages. Certains interprétés, d'autres compilés. Certains génériques, d'autres spécialisés. Bref, il y en a pour tous les goûts!

A titre personnel, je trouve cette situation rafraîchissante. Surtout après des années de concentration (de crispation?) autour de quelques langages aux concepts et à la syntaxe presque identiques... Et puis, c'est toujours un exercice intellectuel profitable – et dans mon cas agréable – que de découvrir un nouveau langage. D'autant plus quand c'est l'occasion de (re-)mettre en lumière un paradigme de programmation occulté par le bulldozer de l'orienté objets impératif à la C++.

D'un point de vue plus pragmatique, disposer de tous ces langages pour la plate-forme Java offre la possibilité de pouvoir choisir le langage le plus adapté à un domaine ou une pratique. Sans pour autant oblitérer la compatibilité avec le code existant ou d'autres parties du projet écrites dans un autre langage, puisque l'interropérabilité est garantie par la plate-forme Java.

Ici, nous allons nous intéresser à un langage qui monte en cette année 2010. A savoir Clojure.

Clojure

En quelques mots, Clojure est un dialecte du langage LISP pour la plate-forme Java. Comme LISP, Clojure encourage un modèle de programmation fonctionnel. Mais ne l'impose pas.

Toujours au chapitre des vocations de ce langage, une fonctionnalité importante voulue par Rich Hickey – l'auteur original de Clojure – est de fournir un langage permettant d'écrire facilement et avec beaucoup moins de risques d'erreurs des programmes concurrents.

Pour terminer cette introduction théorique, une propriété importante de LISP héritée par Clojure est d'être un langage homoiconique. Autrement dit, un langage dans lequel un programme est représenté comme une donnée. Ce qui rend naturelles des opérations comme la génération automatique de code, la réflexion ou la métaprogrammation.

Pour illustrer ce dernier point – et vous donner un aperçu de Clojure – considérez l'exemple de code suivant:

(println "Hello, world!")

En Clojure, cette S-expression est une séquence – dont l'évaluation a pour effet d'afficher un message. Pour vous convaincre qu'une expression est bel et bien une séquence, voici une manière plus alambiquée d'obtenir le même résultat, en manipulant une liste avant de l'évaluer:

(eval (reverse (list "Hello, world!" println)))

Oui d'accord, les choses sont peut-être un peu mystérieuses pour l'instant! Mais dès que vous aurez commencé à jouer avec Clojure, cela devrait changer. Alors procédons sans plus attendre à l'installation.

Installation

Note:

Avant toute chose, vous devez vous assurer d'avoir Java ≥ 5 installé sur votre système. Pour ma part, je disposais pour cet article d'une machine dotée du JDK6 de Sun:

sh$ java -version
java version "1.6.0_20"
Java(TM) SE Runtime Environment (build 1.6.0_20-b02)
Java HotSpot(TM) Client VM (build 16.3-b01, mixed mode, sharing)

Nous allons installer Clojure à partir de la distribution disponible sur http://code.google.com/p/clojure/. Au moment où je rédige, la dernière version stable est Clojure-1.1.0:

sh$ wget http://clojure.googlecode.com/files/clojure-1.1.0.zip
sh$ unzip clojure-1.1.0.zip

Le zip contient le JAR (Java ARchive – Un format d'archive utilisé dans le monde Java pour regrouper des fichiers (classes compilées, méta-données, etc.).) de Clojure compilé, ainsi que les sources. Par ailleurs, Clojure requiert la bibliothèque de manipulation de bytecode ASM 3.0. À télécharger également:

sh$ wget http://download.forge.objectweb.org/asm/asm-3.2-bin.zip
sh$ unzip asm-3.2-bin.zip

À ce stade, vous devez disposer dans votre répertoire courant des deux archives téléchargées et des dossiers extraits. Dossiers qui à leur tour contiennent un certain nombre de JARs:

sh ls -F
asm-3.2/  asm-3.2-bin.zip  clojure-1.1.0/  clojure-1.1.0.zip
sh$ find asm-*/ clojure-*/ -name '*.jar'
asm-3.2/lib/asm-3.2.jar
asm-3.2/lib/asm-util-3.2.jar
asm-3.2/lib/asm-xml-3.2.jar
asm-3.2/lib/asm-analysis-3.2.jar
asm-3.2/lib/asm-commons-3.2.jar
asm-3.2/lib/asm-tree-3.2.jar
asm-3.2/lib/all/asm-all-3.2.jar
asm-3.2/lib/all/asm-debug-all-3.2.jar
asm-3.2/examples/jasmin/test/jasmin.jar
clojure-1.1.0/clojure.jar

Les bibliothèques indispensables à l'utilisation de Clojure sont clojure.jar et asm-3.x:

sh$ java -cp clojure-1.1.0/clojure.jar:asm-3.2/lib/asm-3.2.jar \
            clojure.main
Clojure 1.1.0
user=> 
REPL.png

Read Eval Print Loop — L'environnement interactif effectue toujours les trois mêmes opérations: Tout d'abord il lit une expression saisie par l'utilisateur, puis il l'évalue et enfin il affiche le résultat. Une fois ces opérations effectuées, l'exécution se poursuit avec l'expression suivante. C'est la boucle REPLRead-Eval-Print Loop.

Et voilà: l'environnement interactif de Clojure est prêt à accepter vos premières expressions. Dans ce mode, Clojure évalue chaque expression que vous saisissez: c'est la boucle REPL. Celle-ci est chargée de lire une S-expression, de la compiler à la volée et de l'évaluer. Et enfin d'afficher le résultat de cette évaluation. Ce que nous pouvons vérifier sur quelques expression simples (les détails seront expliqués dans la suite de cet article):

user=> (+ 1 4)
5
user=> (reverse [2 4 8])
(8 4 2)
user=> (concat [1 2 3] [4 5 6])
(1 2 3 4 5 6)
user=> (subs "Hello Clojure" 3 8)
"lo Cl"

Et enfin, vous vous souvenez des exemples donnés en introduction?

user=> (println "Hello, world!")
Hello, world!
nil
user=> (list "Hello, world!" println)
("Hello, world!" #<core$println__5422 clojure.core$println__5422@1c5f743>)
user=> (reverse (list "Hello, world!" println))
(#<core$println__5422 clojure.core$println__5422@1c5f743> "Hello, world!")
user=> (eval (reverse (list "Hello, world!" println)))
Hello, world!
nil

Un mot sur la syntaxe

Si vous êtes habitués à programmer en Java ou C et que vous n'avez jamais utilisé de langage de la famille de LISP, la syntaxe peut vous surprendre au premier abord. En fait, en Clojure, chaque expression est écrite entre parenthèses. Et les sous-expressions sont traduites par autant de blocs entre parenthèses imbriqués.

Note:

Cette profusion de parenthèses fait qu'on traduit parfois l'acronyme LISP par Lots of Insane and Stupid Parenthesis. Mais en réalité, le nom vient de LISt Processing. Autrement dit, la couleur est annoncée: les listes vont être un élément fondamental du langage...

Une autre particularité des langages issus de LISP est l'utilisation de la notation préfixe. Autrement dit, dans une expression, la fonction est toujours le premier membre de la liste. Et les arguments viennent après. Cela vous semblait sans doute naturel pour println dans le Hello, world ci-dessus. Mais ça le sera peut-être moins quand il s'agira des opérateurs mathématiques usuels:

user=> (+ 1 3)
4
user=> (+ 1 3 5)
9
user=> (println (+ 1 3))
4
nil

Pourquoi 2 résultats?

Observez cet exemple:

user=> (println (+ 1 3))
4
nil

On peut se demander pourquoi deux résultats sont affichés. Et d'où vient ce nil. En fait, les expressions dans clojure sont sensées renvoyer un résultat. Et la boucle REPL (Read Evaluate Print Loop) affiche systématiquement le résultat de l'évaluation d'une expression. Or println comme son nom l'indique, sert aussi à afficher. Pour être précis, l'affichage est un effet de bord de l'évaluation de la fonction println. Le 4 est donc affiché par println. Et le nil est affiché par la boucle REPL. Pourquoi nil? Parce que println ne renvoie rien. Et rien en LISP, ça se nomme nil.

S-expression.png

S-expression — Une expression Clojure est définie par une séquence entre parenthèses dont le premier terme est la fonction à appliquer et les termes suivants sont des arguments. N'importe quel terme peut être une sous-expression.

Notez aussi qu'utiliser des expressions complètement parenthésées a pour conséquence qu'il n'y pas de règles de précédence en Clojure: en effet, celle-ci n'ont aucune raison d'être, puisque chaque sous-expression doit être explicitement définie par une liste entre parenthèse:

user=> ;; équivalent en notation infixe: 1 + 2 + 3*4
user=> (+ 1 2 (* 3 4))
15
user=> ;; équivalent en notation infixe: (1 + 2 + 3)*4
user=> (* (+ 1 2 3) 4)
24

Nommer les choses

Approche fonctionnelle

La première manière de donner un nom à une valeur dans Clojure est via un bloc let:

user=> (let [a 1
             b 2]
         (+ a b)
       )
3
user=> a
java.lang.Exception: Unable to resolve symbol: a in this context (NO_SOURCE_FILE:0)

Dans cet exemple, le bloc let lie le symbole a à la valeur 1, et le symbole b à la valeur 2. Cette liaison n'est effective que dans le corps de ce bloc. Cela s'approche de la notion de variable locale – à ceci près que conformément à l'approche purement fonctionnelle, les variables liées ainsi sont immuables (oui, ok: pensez-y plutôt comme à des constantes)...

Approche impérative

Clojure se veut aussi pragmatique. Et en tant que tel, le langage reconnaît qu'il est parfois pratique d'avoir de véritables variables. C'est à dire susceptibles d'être modifiées. Les variables mutables sont introduites par la forme spéciale def:

user=> (def a 1)
#'user/a
user=> (def b 2)
#'user/b
user=> (+ a b)
3

Pour changer la valeur d'une variable, vous pouvez la (re-)lier par un nouvelle utilisation de def:

user=> (def a 10)
#'user/a
user=> (+ a b)
12

Fonctions

Vous vous souvenez de vos cours de maths? En particulier, vous vous souvenez comment on définissait une fonction à l'époque? "Soit f une fonction qui à x associe 2 fois x...". Remarquez comme cette phrase est composée de deux parties: une proposition principale, ("soit f une fonction") et une subordonnée ("qui ..."). Mis à part vous rappeler des souvenirs émus de l'école, cela traduit bien que la fonction et son nom sont deux choses distinctes. LISP vient du monde des mathématiques. Et distingue la définition de la fonction du fait de lui donner un nom:

user=> (fn [x] (* x 2))
#<user$eval__42$fn__44 user$eval__42$fn__44@1b60280>

La forme spéciale fn permet de définir une fonction – avec ses arguments entre crochet, et sa définition sous la forme d'une S-expression. Cette fonction n'a pas de nom. Et est appelée une fonction anonyme. Mais dans ce cas, comment l'utiliser?

Souvenez-vous que le premier terme d'une liste est toujours la fonction à appliquer. Mais rien n'empêche ce premier terme – comme n'importe quel autre terme – d'être une sous-expression. Ce qui me permet d'écrire ce genre de code:

user=> ((fn [x] (* x 2)) 36)
72

Dans cet exemple, le premier terme est bien la fonction "qui multiplie par deux", et c'est bien elle qui est appliquée au second terme de la liste.

Ceci dit, dans la pratique, il est souvent nécessaire de nommer une fonction. Ne serait-ce que pour pouvoir la réutiliser dans devoir la redéfinir à chaque fois. Alors, comment donner un nom à une fonction? Je l'ai dit en introduction, Clojure est un langage homoiconic. C'est à dire que le code est représenté par des structures de données du langage. Autrement dit, les fonctions sont aussi des données. Et nous avons vu que def (entre autres) permet de donner un nom à une valeur. En toute logique, cela s'applique aussi aux fonctions:

user=> (def fois2 (fn [x] (* x 2)))
#'user/fois2

Ce qui peut ensuite être testé:

user=> (fois2 34)
68
user=> (fois2 12)
24

Ceci dit, pour le travail quotidien, Clojure offre aussi une macro pour simplifier la syntaxe:

user=> (defn fois2 [x] (* x 2))
#'user/fois2
user=> (fois2 5)
10

Clojure, Closure

Une utilisation des fonctions anonymes serait par exemple pour associer une action à un bouton dans une interface graphique. Dans ce contexte, les fonctions anonymes remplacent élégamment les inner classes de Java – et puisqu'elles permettent de capturer les variables liées, elles permettent de réaliser des fermetures (closures):

user=> (def f (let [m 2] (fn [x] (* m x))))
#'user/f
user=> (f 5)
10

Ce qui s'avère bien pratique pour définir des familles de fonctions:

user=> (def fois (fn [m] (fn [x] (* m x))))
#'user/fois
user=> (def fois2 (fois 2))
#'user/fois2
user=> (def fois3 (fois 3))
#'user/fois3
user=> (fois2 23)
46
user=> (fois3 23)
69

Closure, Clojure. Est-ce que ça peut vraiment être une coïncidence...

Intégration avec Java

Closure est un langage pour la JVM. Un aspect important est donc l'intégration avec les bibliothèques Java existantes. Pour illustrer ce point, je vais prendre l'exemple bateau de l'utilisation de Swing. Quelque-chose de simple pour cette introduction. Disons un jeu de 421.

Imports

Comme pour un code source Java, commençons par quelques imports:

(import '(javax.swing JLabel JButton JPanel JFrame))

Point de syntaxe:

Par défaut, Clojure interprète toute liste entre parenthèse comme une expression à évaluer dans laquelle le premier terme est la fonction à appliquer:

user=> (+ 1 2 3)
6

Ce qui pose un problème quand on veut réellement créer une liste sans l'évaluer:

user=> (1 2 3)
java.lang.ClassCastException: java.lang.Integer cannot be cast to clojure.lang.IFn (NO_SOURCE_FILE:0)

Le message d'erreur observé est causé par le fait que Clojure tente d'évaluer l'expression en appliquant le premier terme. Qui se trouve être ici un entier – et pas une fonction. C'est exactement ce que dit le message d'erreur...

Alors comment créer une liste? Une solution est d'utiliser l'apostrophe ('). Ce symbole signifie "N'évalue pas cette expression":

user=> '(1 2 3)
(1 2 3)

L'apostrophe est un macro caractère (macro character). C'est à dire qu'il a une signification pour le lecteur (le R de REPL) qui en l'occurrence lui substitue la forme spéciale quote. En clair, les deux notations suivantes sont rigoureusement identiques:

user=> '(1 2 3)
(1 2 3)
user=> (quote (1 2 3))
(1 2 3)
Mais à l'usage, le choix se porte le plus souvent sur l'apostrophe. Sans doute parce que c'est plus court...

Constructeurs

Construisons maintenant nos widgets:

(def button (JButton. "Lancer"))
;; Pour les dés, j'utilise un "vecteur":
(def dice  [(JLabel. "--") (JLabel. "--") (JLabel. "--")])
 
(def panel  (JPanel.))
(def frame  (JFrame. "Faites vos jeux"))

Point de syntaxe:

En Clojure, une liste est écrite entre parenthèses. Un vecteur entre crochets.

user=> (type '(1 2 3))
clojure.lang.PersistentList
user=> (type '[1 2 3])
clojure.lang.PersistentVector

Bien entendu, vous devinez la différence conceptuelle entre ces deux structures de données: la liste est à privilégier pour les accès séquentiels. Alors que le vecteur permet un accès direct (par index) efficace.

Voir http://clojure.org/data_structures pour les autres structures de données disponibles en Clojure.

Le fragment de code pour créer nos widgets a aussi été l'occasion de voir comment invoquer des constructeurs pour créer des objets Java. Notez bien l'utilisation d'un point accolé au nom de la classe:

user=> (java.math.BigInteger. "10010000" 2)
144

A la place de cette notation, on peut aussi obtenir le même résultat avec la forme spéciale new:

user=> (new java.math.BigInteger "10010000" 2)
144

Appels de méthodes

Voyons maintenant comment invoquer une méthode sur un objet Java en ajoutant nos widgets à leur panneau:

;; équivalent Java: panel.add(button)
(.add panel button)
 
;; La version Clojure du "for each". Peut se lire:
;;   pour chaque ''d'' dans ''dice'' faire (.add panel d)
(doseq [d dice] (.add panel d))
 
(.setContentPane frame panel)
(.setSize frame 400 75)
(.setDefaultCloseOperation frame (JFrame/EXIT_ON_CLOSE))
(.setVisible frame true)

Point de syntaxe:

La syntaxe des appels de méthode est inversée par rapport à Java. Dans Clojure, la méthode apparaît d'abord, suivie du receveur puis des éventuels arguments:

user=> (.toUpperCase "Hello")
"HELLO"

N'oubliez pas non plus le point (.) devant le nom de la méthode à invoquer.

Clojure utilise une syntaxe différente pour l'accès aux membres statiques:

user=> (java.lang.System/getProperty "user.name")
"sylvain"

Et voilà: la fenêtre apparaît, avec ses labels et son bouton. Mais rien n'est actif pour l'instant. Ajoutons donc de quoi lancer les dés quand l'utilisateur clique:

(import java.awt.event.ActionListener)
(.addActionListener button 
  (proxy [ActionListener] []
    (actionPerformed [event]
        (.setText (dice 0) (str 1))
        (.setText (dice 1) (str 2))
        (.setText (dice 2) (str 3))
    )
  )
)

Point de syntaxe:

La macro proxy permet de créer dynamiquement un objet qui met en œuvre une interface et/ou qui étend une classe Java.

user=> (def my_task 
         (proxy [Runnable] [] 
           (run [] (Thread/sleep 2000)(println "Hello"))
         )
        )
#'user/my_task
user=> (.start (Thread. my_task))
nil
user=> Hello

Désormais quand l'utilisateur clique, les dés sont lancés et le résultat affiché. Hey! Attendez une minute... C'est toujours le même résultat 1, 2, 3. Par très amusant ça. Pour avoir quelque chose de plus excitant, il faudrait utiliser un générateur pseudo-aléatoire. Une instance de la classe Random devrait faire l'affaire. Mais avant, faisons le ménage en supprimant l'action déjà associée à notre bouton:

(defn clearListeners [widget]
  (loop [listeners (.getActionListeners widget)]
    (if (seq listeners)
      (let []
        (.removeActionListener button (first listeners))
        (recur (rest listeners))
      )
    )
  )
)
(clearListeners button)

Point de syntaxe: Loop et recur

Celui là est un peu compliqué! La forme spéciale loop est exactement comme let dont nous avons déjà parlé. A ceci près qu'elle introduit un point de recursion. Celui-ci permet de revenir au début de la boucle en utilisant recur. Lors du retour, les variables liées par loop voient leur valeur reliée aux arguments de recur:

user=> (loop [a 0]
           (println a)
           (if (< a 5) (recur (+ a 1)))
       )
0
1
2
3
4
5
nil
Pensez à cette notation comme à une sorte de goto ou de continue. Notez que comme il n'y a pas d'optimisation des appels récursifs terminaux en Clojure, les boucles utilisant recur sont le seul moyen d'effectuer des récursions sans consommer d'espace sur la pile.
(.addActionListener button 
  (let [random (java.util.Random.)]
    (proxy [ActionListener] []
      (actionPerformed [event]
          (.setText (dice 0) (str (+ 1 (.nextInt random 6))))
          (.setText (dice 1) (str (+ 1 (.nextInt random 6))))
          (.setText (dice 2) (str (+ 1 (.nextInt random 6))))
      )
    )
  )
)

Le code est un peu redondant. Rien qu'une boucle à base de doseq ne saurait résoudre:

(clearListeners button)
(.addActionListener button 
  (let [random (java.util.Random.)]
    (proxy [ActionListener] []
      (actionPerformed [event]
          (doseq [d dice] (.setText d (str (+ 1 (.nextInt random 6)))))
      )
    )
  )
)

Un script autonome

Jusqu'à présent, vous avez sans doute copié les exemples de code dans le shell Clojure pour une utilisation interactive. Mais il est aussi possible d'enregistrer les différentes instructions dans un fichier pour en faire un script autonome. C'est ce que nous allons faire pour terminer. Remarquez que j'en profite au passage pour refactoriser un poil le code – en particulier pour éliminer les def inutile – et les remplacer par un let:

sh$ cat > quatre-cent-vingt-et-un.clj << EOF
(import '(javax.swing JLabel JButton JPanel JFrame))
(import java.awt.event.ActionListener)
 
(let [button (JButton. "Lancer")
      dice   [(JLabel. "--") (JLabel. "--") (JLabel. "--")]
      panel  (JPanel.)
      frame  (JFrame. "Faites vos jeux")]
 
  (.add panel button)
  (doseq [d dice] (.add panel d))
  (.setContentPane frame panel)
  (.setSize frame 400 75)
  (.setDefaultCloseOperation frame (JFrame/EXIT_ON_CLOSE))
  (.setVisible frame true)
 
  (.addActionListener button 
    (let [random (java.util.Random.)
          rollDice (fn[d] (.setText d (str (+ 1 (.nextInt random 6)))))]
      (proxy [ActionListener] []
        (actionPerformed [event]
            (doseq [d dice] (rollDice d))
        )
      )
    )
  )
)
EOF

Et voilà. Ce script peut maintenant être exécuté ainsi:

sh$ java -cp clojure-1.1.0/clojure.jar:asm-3.2/lib/asm-3.2.jar \
            clojure.main \
            quatre-cent-vingt-et-un.clj

Conclusion

Ouf! Nous avons fait un sacré tour dans Clojure. Et nous avons vu pas mal de notations différentes. Et pourtant nous avons passé sous silence certains aspects importants de la programmation Clojure. Comme la compilation. Ou la programmation concurrente. Mais laissons ça pour une autre fois.

En attendant, je vous encourage à manipuler ce langage. Tout exotique que puisse vous apparaître la syntaxe au premier abord, vous vous y familiariserez relativement vite. Et vous constaterez que dès que les différents concepts commenceront à se mettre en place dans votre esprit, tout un monde de possibilité s'ouvrira à vous.

Ressources