Du monitoring à la matière noire

J’ai participé au Breizhcamp cette année avec une présentation sur le monitoring.

Au passage merci aux organisateurs pour cette édition 2014. Les conférences sont toujours l’occasion de discussions passionnées sur les sujets les plus chaud du moment et puis c’est cool finalement de pouvoir assister à des conférences sans rien avoir à faire. Bravo aussi à l’initiative de faire un repas ouvert à tous le jeudi soir. On a ainsi pu partager les discussions passionnées des speaker diners avec d’autres passionnés.

Le but ce cette présentation est de revoir un peu nos idées reçues sur le monitoring. Même si le sujet n’est pas forcément très hype, Lean et Devops changent notre vision des mesures et les outils BigData viennent à notre secours.

La présentation se trouve .

Sébastien Brousse a twitté sur une partie de la présentation où je fais un détour par la matière noire. Je pense que le rapport avec le monitoring est peu évident sans les explications que j’ai donné de vive voix et le slide ne vous sera pas forcément d’une grande aide.

Pour ceux qui n’étaient là en attendant la vidéo voici les explications et même quelques infos supplémentaires.

Mais pourquoi la matière noire ?

Non, je n’ai pas changé de métier, la présentation parlait bien de monitoring.

Tout d’abord il y a peu de femmes dans l’informatique et donc dans les conférences, et c’était l’occasion d’avoir deux grandes dames avec nous, Margaret Burbidge et Vera Rubin, deux astrophysiciennes.

La première a imaginé le concept de matière noire, la seconde a poursuivi les travaux et mis au point une méthode pour prouver son existence.

Nous on est n’est pas des astrophysiciens, je vais rester à un niveau assez général. Pour les détails, sur la matière noire je vous renvoie à Wikipedia.

Des galaxies prises en excès de vitesse

Cette découverte a un rapport avec le monitoring parce que tout ça découle de problèmes de mesure.

Les astronomes avait constaté depuis longtemps que les galaxies spirales tournaient trop vite par rapport à la masse qu’elles sont supposées avoir d’après les mesures.

Plusieurs hypothèses ont été avancées pour expliquer cette bizarrerie en particulier l’imprécision des mesures.

Dans les années 1970, Margaret Burbidge a refait les mesures avec toute la précision des moyens de l’époque. Et là, ça ne collait toujours pas.

Plutôt que de remettre en cause les mesures, Margaret Burbidge a remis en cause le raisonnenment. Si la vitesse ne colle pas avec la masse, c’est que la masse n’est pas celle que l’on pense.

A cette époque on évalue la masse à partir des objets que l’on peut observer au téléscope, donc ceux qu’on peut voir parce qu’ils émettent de la lumière. Par conséquent, elle en a déduit qu’il y a dans les galaxies quelque chose qui a une masse mais que l’on ne voit pas parce que ça n’émet pas de lumière. C’est le concept de matière noire.

Vera Rubin qui travaillait avec Margaret Burbidge a apporté des éléments de preuve en mettant au point une méthode de calcul de la masse basée sur l’influence gravitationnelle de la galaxie sur son environnement, ce qu’on appelle la masse dynamique. A la différence de la masse lumineuse, la masse dynamique est compatible avec la vitesse de rotation des galaxie spirales. Et la masse dynamique basée directement sur l’effet de la masse n’a pas de raison d’être fausse, ce qui justifie qu’il y a un élément non visible ayant une masse.

Bon, d’accord mais moi je mesure des requêtes HTTP

Ok, nous on ne mesure pas des galaxies, mais même si on peut parfois regarder le serveur, enfin la boîte, finalement ce qu’on mesure est tout aussi peu visualisable.

Cette histoire montre que c’est important de mesurer de la bonne manière.

Si on mesure des effets indirects, on est dépendant d’un modèle. Ce modèle c’est l’idée qu’on se fait du fonctionnement du système et il peut être faux.

Dans un certain nombre de cas, cette mesure indirecte marche, par exemple la masse lumineuse convient pour les objets très lumineux comme les étoiles. Mais de temps en temps, ça ne marche pas.

Bien sûr, on n’a pas toujours la possibilité de mesurer l’effet direct, et de temps en temps on doit se baser sur une mesure indirecte parce que c’est plus abordable. Dans ce cas il faut rester vigilant, et savoir remettre en cause le modèle qu’on se fait du système.

Notre matière noire

Qu’est ce que c’est notre matière noire à nous les informaticiens ?

Ce sont les caches, les buffers, les load balancers, les heuristiques sur les files d’attente, les optimisations de JVM, tout un tas de mécanismes internes au système qui le rendent plus performant, mais qui font aussi que certaines de nos mesures ont un comportement erratique ou n’ont plus de sens dans certaines situations.

Au final, mesurer correctement les performances d’un système ou d’une application informatiques est une activité qui réserve toujours des surprises. On ne peut pas se contenter de poser des sondes au petit bonheur la chance sans comprendre ce qu’on fait. Il faut comprendre le système et comment il fonctionne pour le mesurer correctement.

On a un peu plus de chances, en cas de doute on peut souvent se reporter à la documentation (quoique à la réflexion pas toujours) ou demander au développeur (enfin des fois … ).

Mais par contre, un cache c’est beaucoup moins beau sur les photos qu’une galaxie spirale.

Les images proviennent toutes de la galerie d’images de la Nasa.

Publicités

Présentation Devops au Women Techmakers Nantes

Dans le cadre du Women Techmakers Event de ce mois de mars, le Women in Technology et le GDG de Nantes organisent une soirée spéciale.

Au programme, la présentation de Women in Technology, un retour sur l’entrepreneuriat par Anaïs Vivion, et une présentation plus technique sur DevOps que j’assurerai.

La présentation se trouve en ligne ici.

Alohomora Incanter-4

Incanter logo

Quand on ne sait plus trop où trouver des formules magiques, il reste Harry Potter, Alohomora l’incantation qui ouvre les portes, les portes de la voie des statistiques.

Allez au boulot, on a du rangement à faire.

Cet article fait partie d’une suite qui commence avec cet article épisode 1.

Recharger les données

First things first, charger nos librairies et nos données et on reprend où on en était dans l’épisode 3.

user=> (use '(incanter core io datasets stats))
nil
user=> (def data (read-dataset
"/Users/cfalguiere/Workspaces/Diapason/report-data/20111217-ETALON_WSW_TPS2011.csv" :header true))

#'user/data

Il est grandement temps de faire une fonction qui charge les résultats. On peut être sorcier et fainéant.

user=> (defn read-results []
  (read-dataset "/Users/cfalguiere/Documents/2012-01/RD-Clojure/Workspace/incanter/resources/sample-results.csv" :header true))

#'user/read-results

N’oubliez pas les parenthèses, sinon au lieu du prince charmant vous auriez une jolie mangouste, jolie mais bon ça n’est pas ce que vous vouliez. En l’occurrence, vous ne voulez pas que data représente la fonction mais son résultat.

user=>(def data read-results)
#'user/data
user=> data
#<user$read_results user$read_results@6bd46c20>

Donc voilà, le tour exécuté correctement et quelques donnnées présentes.

user=>(def data (read-results))
#'user/data

Pour le moment j’expérimente interactivement, donc peu importe l’IDE. Lançons un éditeur de texte pour copier ce code et chargeons le dans le REPL.

(use '(incanter core io datasets stats))

(def result-file-name "/Users/cfalguiere/Documents/2012-01/RD-Clojure/Workspace/incanter/resources/sample-results.csv")

(defn read-results []
  (read-dataset result-file-name :header true))

(def data (read-results))

J’aimerais bien indiquer le répertoire courant pour éviter tout ces chemins mais à la différence de R on ne peut pas changer le répertoire courant, parce que la JVM ne permet pas de le faire. Mais au fait quel serait mon répertoire courant ? Une rapide recherche sur Google indique ça :

user=> (. (java.io.File. ".") getCanonicalPath)
"/Applications"

Un peu ésotérique. C’est plus clair avec un exemple plus évident comme la méthode toUpperCase de la classe String. Le . applique la méthode à l’objet.

user=>(. "aaa" toUpperCase)
"AAA"

Il ne reste plus qu’à charger le programme et data est disponibles pour d’autres tours.

user=>(load-file "/Users/cfalguiere/Documents/2012-01/RD-Clojure/workspace/incanter/tuto1.clj")
#'user/data
user=> (nrow data)
209

Agrégeons, agrégeons, ils en restera toujours quelque chose

En général, j’aime bien avoir une vue d’ensemble de mes données : temps max, temps moyen, taux d’erreur …

Qu’est ce que j’ai là dedans ?

Le dataset est une map qui contient un vecteur de labels et une liste de lignes de données. La fonction col-names retourne les labels des colonnes (c’est équivalent (:column-names data))

user=> (col-names data)
[:lb :t :lt :ts :s :rc :rm :by :na]
user=> (:column-names data)
[:lb :t :lt :ts :s :rc :rm :by :na]

Les labels JMeter ne sont pas des plus parlants pour des raisons de compacité du résultat.

Pour chaque relevé nous disposons des informations suivantes :

  • :t le temps de réponse,
  • :lt le ‘latency time’ c’est à dire le temps qui s’est écoulé jusqu’à la réception du premier octet de la réponse,
  • :by le nombre d’octets de la réponse,
  • :na : le nombre d’utilisateurs simulés au moment de cette requête,
  • :rc le code retour de la requête HTTP, habituellement 200 ou 302,
  • :s le résultat des assertions sur les contenus reçus (true ou false),
  • :rm : un message,
  • :lb le label soit le nom donné au sample dans le script JMeter soit une url,
  • :ts  le timestamp du relevé.

Les 4 premières lignes sont des séries numériques. Les autres sont des catégories (l’équivalent des factors pour R). Elles vont servir à filtrer (pour les statuts) ou à regrouper les données.

Le module stats fourni les statistiques habituelles. Pour faire quelques expériementations, on va générer une série de nombres dont la moyenne est connue. sample-normal construit une liste de 1000 nombres distribués selon une loi normale centrée sur 10 avec un écart-type de 1. Sans surprise, la moyenne est autour de 10 et l’écart type est autour de 1.

user=> (def mysample (sample-normal 1000 :mean 10 :std 1))
(10.099828740537856 9.903470850168006 9.52228847634955 9.003518490739022 9.171579274811812 ...
user=> (mean mysample)
9.941436842660105
user=> (sd mysample)
1.0007997747452984

Et min et max ?

présentation des 4 quartiles

Un magicien ne voit pas les choses aussi simplement. Le min est la valeur qui est inférieure à tous les autres relevés. Et symétriquement le max est la valeur qui est supérieure à tous les autres relevés.

Mais les magiciens aiment bien découper les données en tranches. Donc ils veulent aussi savoir quelle est la valeur qui sépare les relevés en deux groupes, inférieurs et supérieurs à une valeur qui s’appelle la médiane.

Pour améliorer le spectacle, les données sont découpées en quatre tranches par pas de 25% comme le montre le schéma de droite, ce sont des quartiles. On voit sur le schéma que la surface rouge qui représente les 25% les plus bas se termine vers 9, la surface bleu qui représente les 25% suivants se termine vers 10 etc.

Les valeurs de 25% sont arbitraires. La forme générale de cet agrégat est le quantile. La fonction quantile retourne le min, la valeur maximale pour 25% des relevés, 50% (la médiane) et 75%, et le max (le quantile 100%).

user=> (quantile mysample)
(6.078738192126153 9.284660278736457 9.9269193454763 10.609659921191085 12.899443382831647)

Le monde étant ce qu’il est les sorciers qui font du test de charge ne s’intéressent pas aux quantiles des magiciens. En fait la seule chose qui les intéressent, c’est l’impact des 5% ou 10% de temps les plus longs. Donc ils calculent la valeurs maximale pour 90% ou 95% des relevés.

user=> (quantile mysample :probs 0.95)
11.591767882673627
user=> (quantile mysample :probs [0.9 0.95])
(11.278966306153395 11.591767882673627)

Qu’est ce que ça donne sur nos données de test ?

user=> (mean ($ :t data))
1429.579831932773
user=> (sd ($ :t data))
2237.25502018713

On peut faire toutes ces opérations sur la même série de données en utilisant la fonction associée au dataset with-data. Elle  factorise le dataset et permet de définir l’expression à utiliser, ici construire un tableau contenant les différentes métriques. $data représente la série à l’intérieur du with-data.

user=> (with-data ($ :t data)
  [(mean $data)(sd $data)])

[1429.579831932773 2237.25502018713]

Si on rajoute le quantile ça nous donne ce qui suit. Un bon début, mais on a un tableau des différents indicateurs plus une liste avec les quantiles.

user=> (with-data ($ :t data)
  [(count $data)(mean $data)(sd $data)(quantile $data :probs[0 0.5 0.9 0.95 1])])

[119 1429.579831932773 2237.25502018713 (10.0 708.0 3393.4 4130.999999999982 13007.0)]

Mmm … un coup de baguette magique flatten et les données se retrouvent bien alignées dans le tableau.

user=> (with-data ($ :t data)
  (flatten [(count $data)(mean $data)(sd $data)(quantile $data :probs[0 0.5 0.9 0.95 1])]) )

(119 1429.579831932773 2237.25502018713 10.0 708.0 3393.4 4130.999999999982 13007.0)

Une variable pour stocker le résultat et on sauve

user=> (def stats
  (with-data ($ :t data)
    (flatten [(count $data)(mean $data)(sd $data)(quantile $data :probs[0 0.5 0.9 0.95 1])]) ) )

#'user/stats
user=> stats
(119 1429.579831932773 2237.25502018713 10.0 708.0 3393.4 4130.999999999982 13007.0)

Quoique, pas tout de suite apparemment.

user=> (save 'stats "stats.csv")
java.lang.IllegalArgumentException: No method in multimethod 'save' for dispatch value: class clojure.lang.Symbol (NO_SOURCE_FILE:0)

En quête d’une multiméthode …

Que peut bien être une multiméthode ?

Vous trouverez une réponse complète dans cet article sur le polymorphisme en Clojure. En quelques mots, c’est le mécanisme qui permet à une fonction de se comporter différement en fonction des paremètres qu’on lui passe (des nombres ou des chaînes par exemple).

Vous avez probablement déjà noté que les fonctions en clojure sont souvent définies pour plusieurs nombres d’arguments (l’arité de la fonction si vous voulez briller la nuit au prochain ParisJUG). Clojure permet qu’une fonction soit surchargée (overloaded en anglais) et elle peut ainsi avoir une définition différente selon le nombre d’arguments.

Ci-après un exemple très simple de surcharge qui crée une map rectangle avec 1 ou 2 paramètres. La forme a un seul paramètre utilise la forme a 2 paramètres pour construire un carré.

user=> (defn make-rectangle
  ([width] (make-rectangle width width))
  ([width height] {:width width :height height}))

#'user/make-rectangle
user=> (make-rectangle 20)
{:width 20, :height 20}
user=> (make-rectangle 10 20)
{:width 10, :height 20}

Autre exemple plus compliqué, la multiplication implémentée de manière récursive en utilisant 4 formes :

(def mult
  (fn this
    ([] 1)
    ([x] x)
    ([x y] (* x y))
    ([x y & more]
      (apply this (this x y) more))))

La fonction est définie pour 0, 1, 2 arguments et un nombre quelconque d’arguments. Le & indique que ce qui se trouve à droite contient une liste d’arguments, et c’est par ce moyen qu’on définie une fonction variadique (pour quand vous ne brillerez plus assez avec arité).

Décryptons un peu cette multiplication. Tout d’abord, cet exemple n’utilise pas la macro defn, on a donc deux les étapes, décrire la fonction (fn) et la nommer (def). La multiplication utilise 4 variantes. Dans le cas général (4) je prend les 2 paramètres les plus à gauche, je leur applique « moi-même » (this x y) et j’applique ensuite « moi-même » en utilisant ce résultat et tous les paramètres qui restent à droite (more). Si je n’ai que 2 paramètres (3), je les multiple. Si je n’en ai plus qu’un (2), je le renvoie. Si je n’en ai plus (1) je renvoie 1.

Et pourquoi apply ? apply est utilisé dans les contextes où le nombre d’élements n’est pas connu à la compilation et donc typiquement lorsque l’on déconstruit une séquence. Par exemple

user=> (+ 1 2 3 4 5)
15
user=> (+ [1 2 3 4 5])
java.lang.ClassCastException (NO_SOURCE_FILE:0)
user=> (apply + [1 2 3 4 5])
15

L’addition du contenu de la séquence n’est pas possible directement. L’utilisation d’apply permet de déconstruire le tableau pour en lister les éléments et d’effectuer l’opération. Dans le cas de la fonction mult, apply permet de traiter more qui est une séquence.

Et ma multiméthode ?

La multiméthode est un mécanisme plus puissant qui permet de gérer le polymorphisme. La syntaxe est un peu différente et basée sur les verbes defmulti et defmethod.

L’exemple suivant montre une multiméthode « rencontre » entre différentes espèces. Si le paramètre 1 est un lapin et le paramètre 2 un lion, le comportement de « rencontre » est équivalent à la fonction « s’enfuit ». Dans le cas inverse, elle est équivalente à « mange »

(defmulti encounter (fn [x y] [(:Species x) (:Species y)]))
(defmethod encounter [:Bunny :Lion] [b l] :run-away)
(defmethod encounter [:Lion :Bunny] [l b] :eat)

L’exemple complet se trouve dans cet article sur le polymorphisme en Clojure.

Les multimethodes peuvent dispatcher en fonction des types des paramètres mais également sur des valeurs des arguments, leur nombre ou des méta-données selon la syntaxe.

Bref,  « No method in multimethod 'save' for dispatch value: class clojure.lang.Symbol (NO_SOURCE_FILE:0)" signifie seulement « je n’ai pas d’implémentation de save pour le type que tu m’envoie ». Oui, je sais …, mais les magiciens ne vont pas dévoiler tous leurs tours si facilement.

Enfin sauvé

stats est une liste et  save est une fonction Incanter qui fonctionnera que sur une matrice ou un dataset.
Qu’à celà ne tienne, on va construire un dataset avec ces données. Il faut lui indiquer les noms de colonnes et les valeurs.

user=> stats
(119 1429.579831932773 2237.25502018713 10.0 708.0 3393.4
4130.999999999982 13007.0)
user=> (def statsds
 (dataset ["count", "mean", "sd", "min", "median", "q90", "q95", "max"] stats) )

#'user/statsds
user=> statsds
#:incanter.core.Dataset{:column-names ["count" "mean" "sd" "min" "median" "q90" "q95" "max"],
:rows ({"count" 119} {"count" 1429.579831932773} {"count" 2237.25502018713} {"count" 10.0} {"count" 708.0} {"count" 3393.4} {"count" 4130.99999
9999982} {"count" 13007.0})}
user=> (view statsds)

Mmm, est ce bien ce que l’on veut ? Non, en fait le dataset n’est pas dans le bon sens. Il devrait avoir 1 ligne avec une colonne par indicateur.

Si vous allez sur la fonction dataset vous pouvez voir le source sous github. La fonction dataset attend une séquence de séquences ou une séquence de maps. Avec une simple liste, dataset a considéré que l’on voulait une liste de plusieurs lignes de 1 colonne.

La façon la plus simple de résoudre le problème est de transformer la liste en dataset en la transposant pour qu’elle constitue 1 ligne de plusieurs colonnes. Ensuite on lui ajoute des noms de colonnes.

user=> (def statsds (col-names
  (to-dataset stats :transpose true) ["count", "mean", "sd", "min", "median", "q90", "q95", "max"]) )

#'user/statsds
user=> statsds
#:incanter.core.Dataset{:column-names ["count" "mean" "sd" "min" "median" "q90" "q95" "max"],
:rows ({"max" 13007.0, "q95" 4130.999999999982, "q90" 3393.4, "median" 708.0, "min" 10.0, "sd" 2237.25502018713, "mean" 1429.579831932773, "count" 119})}
user=> (view statsds)
Le dataset est bien dans le bon sens et on peut le sauver
user=> (save statsds "/Users/cfalguiere/Documents/stats.csv")
nil

Il manque juste une dernière info utile le taux d’erreurs et le top n.
Pour le taux d »erreurs, on verra plus tard (surtout qu’il n’y en a pas dans le jeu de données) mais pour le top 5 des pires temps voici l’incantation trier-les-lignes-renverser-l-ordre-et-prendre-les-5-premiers

user=> (take 5 (reverse (sort-by :t (:rows data))))
({:na 1, :by 172777, :rm "OK", :rc 200, :s "true", :ts 1.324109405557E12, :lt 1398, :t 13007, :lb "/CategoryDisplay"}
{:na 1, :by 33235, :rm "OK", :rc 200, :s "true", :ts 1.324109282891E12, :lt 8967, :t 8981, :lb "/--product--.html"} ...

Et voilà d’autres tours de sorciers inscrits dans le grimoire. Dans l’épisode 5 on regroupera ces données pour avoir des résultats plus détaillés, par label par exemple et on fera des graphes de ces données.

Présentation Performances à Toulouse et Bordeaux

Voici les slides de la présentation que j’ai faite aux JUGs de Toulouse et Bordeaux le 7 et 8 décembre.

 

Je voudrais remercier ces JUGs ainsi que les deux genigraph.fr qui a publié un compte rendu sur son blog. Le JUG de Toulouse a également fait un compte-rendu de la soirée.

Une session similaire a été enregistrée par le JUG de Lausanne.

2012

ma photo en puzzleEt voilà ! Encore une nouvelle année, pleine de bonnes choses espérons le.

La fin d’année est aussi une occasion de faire le point sur l’année précédente. En 2011, j’ai appris Objective-C, utilisé 3 IDE différents (Eclipse, XCode, IDEA Intellij), dompté MacOSX, apprivoisé Git, fait mon premier projet Grails et MongoDB. Une année riche en nouveautés.

Mais un regret, je n’ai pas vraiment eu le temps de capitaliser sur ces sujets et 2012 m’amènera probablement sur d’autres thèmes. Et plein d’autres choses que j’aurai voulu expérimenter qui encombrent une très longue liste de choses à faire un jour.

Donc (comme chaque année je pense) je prend la bonne résolution de ne pas trop m’éparpiller.

Pas simple quand on est curieuse de tout dans un univers qui fourmille en nouveautés.

Le drame quand on a peu de temps disponible, c’est le « à quoi bon commencer quelque chose ». Donc on fini par utiliser ce temps de manière un peu aléatoire. On reste à la surface de tous les sujets. Mais j’apprend beaucoup plus en essayant de développer quelque chose en Clojure ou en préparant une présentation sur les performances, même si ça m’amène parfois sur d’autres sujets.

Donc voilà, en 2012, j’ai pris la résolution de me concentrer sur quelques sujets et de produire quelque chose sur ces sujets avant de zapper sur autre chose.

Rendez-vous début 2013 pour faire le point 😉

1, 2, 3 . . . Incanter

Incanter logoCet article est la suite de cet article abracadabra ! Incanter.

Pour traiter des données statistiques, les structures de données qui nous intéressent principalement sont les nombres et les tableaux de nombres.

Clojure utilise les types de données classiques pour représenter des textes String, Character, des Boolean (true/false), des nombres, plusieurs types de collections et deux types d’identifiants.

La documentation de l’interpréteur décrit de manière assez rapide les différents types de données http://clojure.org/reader.  Ce document http://clojure.org/data_structures décrit les structures de données disponibles de manière un peu plus approfondie. J’ai aussi trouvé ce document utile http://en.wikibooks.org/wiki/Learning_Clojure/Data_Types.

Les textes

Clojure repose sur les types Java. Donc java.lang.String et la manipulation des chaînes n’est pas très différente. Quelques opérations utiles :

user=> (type "A")
java.lang.String
user=> (str 144 " éléments") ; concatène les deux termes
144 éléments
nil
user=> (count "Incanter") ; compte le nombre de caractères de la chaîne
8

La quote ‘ a une signification particulière dans la syntaxe Clojure (on le verra plus loin). Il ne peut pas être utilisé pour les caractères. Les caractères sont définis par \. Il existe aussi des caractères spéciaux comme \tab \newline \space

user=> (type 'A')
java.lang.Exception: Unmatched delimiter: )
user=> (type \A)
java.lang.Character
user=> (println \A \tab \B)
A
        B
nil

Les identifiants

Clojure a 2 types d’identifiants, symbol et keyword.

Les  symbol sont des identifiants qui référencent quelque chose d’autre. Ce sont en général des noms de variable, de fonction, de classe.

Les keyword sont des identifiants qui référencent leur nom. Ce sont des sortes de chaînes constantes donc plus rapides à comparer. Elles sont principalement utilisées comme clés dans les hash-maps ou les méta-données des symbol. Un : est placé devant les keyword, mais il ne fait pas partie de son nom.

Ci-dessous la déclaration d’une variable avec un symbol x . On ne peut pas faire la même chose avec un keyword.

user=> (def y 1)
#'user/y
user=> y
1
user=> (def :y 1)
java.lang.Exception: First argument to def must be a Symbol (NO_SOURCE_FILE:221)
</code

Les keyword sont utilisés comme label dans les hash-maps. On voit que le keyword Alice retourne son nom.

user=> (def scores {:Alice 1, :Bob 2} )
#'user/scores
user=> (get scores :Alice)
1
user=> :Alice
"Alice"

Les  keyword sont aussi utilisés dans la définition des méta-données. Le nom de fonction hello est symbol. Un keyword :doc est utilisé pour ajouter une description. Les  keyword ne peuvent pas avoir de méta-données.

user=> (defn
  ^{:doc "pas très utile"}
  hello ([name] (println "Hello" name)))
#'user/hello
user=> (doc hello)
-------------------------
user/hello
([name])
  pas très utile
nil

Les symbol peuvent être utilisés là où des keyword seraient utilisés (même si c’est sans grand intérêt).
Essayons pour voir.
On ne peut pas utiliser le  symbol directement.

user=> (def scores {Alice 1, Bob 2})
java.lang.Exception: Unable to resolve symbol: Alice in this context (NO_SOURCE_FILE:375)

Contrairement aux keyword qui se résolvent sur eux même, les symbol tentent d’évaluer ce qu’ils référencent dès qu’on les utilise. Le keyword :Alice renvoie « Alice ». Le symbol Alice ne peut pas être évalué car il n’existe pas encore, et si on prend un symbol existant dans le contexte tel que y, il renvoie 1 dès qu’on utilise ce nom.

user=> :Alice
"Alice"
user=> (name y)
java.lang.ClassCastException: java.lang.Integer cannot be cast to clojure.lang.Named (NO_SOURCE_FILE:0)
user=> y
1
user=> (name :Alice)
"Alice"

Le ‘ permet d’empêcher l’évaluation de l’expression et d’utiliser le symbol non évalué comme label.

user=> 'y
y
user=> (name 'y)
"y"user=> (def scores {'Alice 1, 'Bob 2}) ; pour démo, on utiliserait normalement :Alice :Bob
#'user/scores
user=> (get scores 'Bob)
2

Les nombres

Les nombres sont toujours « boxés » c’est à dire contenus dans un objet comme Integer pour le type int. Ces types sont dérivés de java.lang.Number.

Quelques différences avec Java. Pour les entiers, Clojure gère automatiquement la conversion vers des BigNum et ajoute un type Ratio. Pour les décimaux, ce sont par défaut des Doubles et Clojure préserve la précision lors des opérations numériques qui impliquent un Double.

La représentation de la constante influence le type. M indique un BigNumber et le . un décimal comme en Java.

user=> (type 1)
java.lang.Integer
user=> (type 1.0)
java.lang.Double
user=> (type 1M)
java.math.BigDecimal

Quelque soit le type de départ, le type est adapté pour contenir la valeur sans la tronquer lors des opérations arithmétiques. Un souci de moins.

user=> (type 123456789)
java.lang.Integer
user=> (type (* 123456789 123456789))
java.lang.Long
user=> (type (* 123456789 123456789 123456789))
java.math.BigInteger

Quelque soit le type utilisé les opérations de comparaison entre des types différents restent cohérentes.

user=> (== 1 1.0)
true
user=> (== 1.0 1M)
true
Ratio et divisions

Une spécificité de Clojure, le type Ratio permet de représenter les fractions d’entiers sans perte de précision. Il ne s’applique qu’aux entiers. La même opération retourne un décimal dès qu’une des valeurs est décimale.

user=> (/ 2 3)
2/3
user=> (type (/ 2 3)) ;; opération sur entiers
clojure.lang.Ratio
user=> (/ 2 3.0)
0.6666666666666666
user=> (type (/ 2 3.0)) ;; opération sur un entier et un décimal
java.lang.Double
user=> (/ 4 2) ;; opération sur entiers
2
user=> (type (/ 4 2) )
java.lang.Integer

Si on veut faire une division sur entiers, il faut coercer une des valeurs en float ou en double.

user=> (type (float 2))
java.lang.Float
user=> ( / (float 2) 3 )
0.6666667
user=> (type ( / (float 2) 3 ))
java.lang.Float

Même si les nombre sont par défaut traités en Double, la conversion n’est pas systématique si elle n’est pas nécessaire. Une conversion en double aurait donné un Double.

user=> ( / (double 2) 3)
0.6666666666666666
user=> ( type ( / (double 2) 3) )
java.lang.Double
Les opérations

Maintenant qu’est ce que je peux faire avec mes nombres ?

user=> (pos? -2) ;; is positive ?
false
user=> (min 3 1 5) ;; min
1
user=> (max 4 8 2) ;; max
8
user=> (int 1.2) ;; coercion en entier
1
user=> (inc 2) ;; incrémente de 1
3
user=> (dec 5) ;; décrémente de 1
4

La plupart des fonctions acceptent n valeurs. Qu’est ce qui se passe si j’ai plusieurs paramètres ?

Le résultat est à assez évident sur min ou max. Moins sur d’autres opérateurs. Le résultat est spécifié dans la documentation. Par exemple pour /, le premier terme est divisé par tous les suivants.

user=> (/ 12 2 3)
2

Pour <, la documentation indique « Returns non-nil if nums are in monotonically increasing order, otherwise false. » La suite doit être strictement croissante ? Mmm? testons …

user=> (< 1 2)
true
user=> (< 1 2 4)
true
user=> (< 1 5 4)
false

Encore plus de nombres

En général, lorsque l’on fait des statistiques on a de longs tableaux de nombres. Il nous faut donc des collections.

A quoi ça ressemble en Clojure ?

Les maps

Les maps sont représentées par {}. On en a vu un exemple plus haut. Les , sont facultatives mais améliorent la lisibilité. On peut utiliser tout type de valeurs comme clé (tout type de form dans la terminologie Clojure).

user=> (def scores { "Alice" 1, "Bob" 2} )
#'user/scores
user=> (type scores)
clojure.lang.PersistentArrayMap
user=> (get scores "Bob")
2
user=> (def scores { 15 "A", 22 "B"} )
#'user/scores
user=> (get scores 15)
"A"
Les listes

Les listes sont représentées entre (). C’est ce qu’on manipule depuis le début.

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

Diantre ! La magie ne marche pas sur les listes ?
Si, mais l’interpréteur évalue toutes les listes comme si c’est c’était des programmes.

Mais si on veut que la liste ne soit pas évaluées et restent des listes ??? Mmm J’ai déjà vu ça quelque part …

user=> (def readings '(1 2 3))
#'user/readings
user=> (type readings)
clojure.lang.PersistentList
user=> readings
(1 2 3)

La fonction range est un moyen rapide de générer une liste de nombres.

user=> (range -2 6)
(-2 -1 0 1 2 3 4 5)
Les vecteurs

Le vecteur est représenté entre [].

user=> (def persons [ "Alice" "Bob" "Charles"] )
#'user/persons
user=> (type persons)
clojure.lang.PersistentVector
user=> (persons 1)
"Bob"
user=> ([ "Alice", 2, :name ] 2)
:name

La liste et le vecteur  conservent l’ordre. La liste est à privilégier pour les accès séquentiels. Le vecteur permet un accès direct (par index) efficace.

Les sets

Il reste un dernier type de collection dans Clojure, le set qui maintient l’unicité. Il est représenté par #{}.

user=> #{1 3 1 4}
java.lang.IllegalArgumentException: Duplicate key: 1
user=> #{1 3 5 4}
#{1 3 4 5}
user=> (type #{1 3 5 4})
clojure.lang.PersistentHashSet
Quelques opérations sur les collections

La fonction doseq permet de parcourir l’itérateur de la collection.

user=>(def persons '( "Alice" "Bob" "Charles") )
user=> (doseq [p persons] (println p))
Alice
Bob
Charles
nil
user=> (count persons)
3

Toutes les callections sont modifiables.  conj permet d’ajouter des éléments. Attention l’effet de conj depend du type de collection.

user=> (conj '(1 2 3) 5)
(5 1 2 3)
user=> (conj [1 2 3] 5)
[1 2 3 5]
user=> (pop [1 2 3])
[1 2]

Un peu d’Incanter

Ah ! Quand même !

Incanter rajoute un type de donnée, la matrice. Une matrice ne peut contenir que des données numériques et n’a pas de nom de colonnes (même définition que dans R).

user=> (def A (matrix [[1 2 3] [4 5 6] [7 8 9]]))
java.lang.Exception: Unable to resolve symbol: matrix in this context (NO_SOURCE_FILE:450)

Whoops ! Jusqu’à présent tous les types étaient du Clojure. Ce type de données étant spécifique à Incanter, il faut charger la librairie en utilisant use. Et voilà ! matrix génère une matrice. Comme dans R, la matrice est un Vector replié.

user=> (use 'incanter.core)
nil
user=> (def A (matrix [[1 2 3] [4 5 6] [7 8 9]]))
#'user/A
user=> A
[1,0000 2,0000 3,0000
4,0000 5,0000 6,0000
7,0000 8,0000 9,0000]

Il existe différentes syntaxes pour créer des matrices à partir de vectors ou de listes. La syntaxe précédente concaténait des vecteurs lignes. Celle-ci découpe un vecteur en fonction du nombre de colonnes souhaitées.

user=> (def A2 (matrix [1 2 3 4 5 6 7 8] 2))
#'user/A2
user=> A2
[1,0000 2,0000
3,0000 4,0000
5,0000 6,0000
7,0000 8,0000]

Incanter ajoute également le type Dataset, qui est l’équivalent d’un tableau Excel ou d’une table en base de données. Le dataset comporte des noms de colonnes. Les colonnes peuvent être de tout type.

user=> (use 'incanter.datasets)
nil
user=> (dataset ["x1" "x2" "x3"]
[[1 2 3]
[4 5 6]
[7 8 9]])
#:incanter.core.Dataset{:column-names ["x1" "x2" "x3"], :rows ({"x3" 3, "x2" 2, "x1" 1} {"x3" 6, "x2" 5, "x1" 4} {"x3" 9, "x2" 8, "x1" 7})}

Ces structures de données sont généralement lues à partir de fichiers CSV ou d’une base de données.

user=> (use 'incanter.io)
nil
user=> (def data (read-dataset "datafile.csv" :header true))
nil

Et quelques statistiques pour finir et vous montrer qu’on n’a pas manipulé toutes ces collections pour rien aujourd’hui :

user=> (use ‘(incanter core stats) )
nil
user=> (mean ‘(20 10 30) )  ; médiane
20.0
user=> (sum ‘(20 10 30) )  ; somme
60.0

On reviendra en détail sur tout ça dans l’épisode 3.

abracadabra ! Incanter

Incanter logo
Et voilà, j’ai a nouveau un paquet de résultats de test à analyser. Une bonne occasion d’évaluer Incanter.
Incanter se définit comme « a Clojure-based, R-like platform for statistical computing and graphics. ».

Un R-like ? R est une plate-forme et un langage dérivé de Scheme (lui même dérivé de LISP) qui permet de manipuler facilement des données statistiques (les lire depuis une source, les filtrer, calculer des moyennes, afficher ces valeurs sous forme de courbe ou d’histogramme, et bien d’autres fonctions pour statisticiens). Ok, mais ça fait à peu près ce que fait Excel. Oui, mais sous forme de langage ce qui permet de scripter bien plus facilement et surtout permet de traiter de grosses masses de données sans devoir les afficher.

Quel est le problème avec R ? C’est un outil génial mais qui existe dans un éco-système différent de Java et ne permet pas de réutiliser tout ce que l’on connait. Il est un peu lent et c’est un mix entre programmation fonctionnelle et C parfois un peu déroutant. Mais il bénéficie d’une communauté beaucoup plus grande et de beaucoup plus de fonctions que son challenger. Et puis quand même Incanter : Data Sorcery c’est indispensable d’aller voir.

Et c’est une bonne occasion de se mettre à Clojure aussi. Pour ceux et celles qui n’auraient pas fait de veille depuis 2 ans, Clojure est un dialecte de LISP (avec des (parenthèses) partout) qui tourne dans la JVM.

On s’y met

Assez tourné autour du sujet. Maintenant il faut regrouper quelques épices et mettre les mains dans le chaudron.

Pour commencer, il faut un environnement Clojure. Vu que le but c’est d’utiliser Incanter, autant utiliser la solution de fainéant avec un kit de magicien.

http://incanter.org/downloads/

Et une incantation ! (enfin un clic sur l’icône)
Mmm … non c’est bien ça.
Certes …
Effectivement, un R-like (encore que dans l’environnement R il y a deux ou trois trucs dans le menu).

incanter shell

J’ai bien réussi à faire une copie d’écran sur un Mac. Pas de soucis (encore que WordPress s’obstine a formater le code préformaté).

Le shell Clojure est un environnement interactif qui lit, evalue et affiche le résultat des expressions qu’on lui soumet. Il est souvent désigné  sous le nom REPL.

Tentons quelques trucs.

Clojure 1.2.0-master-SNAPSHOT
user=> (1+1)
java.lang.NumberFormatException: Invalid number: 1+1 java.lang.Exception: Unmatched delimiter: )
user=> ()
()
user=>

Soyons positifs, () marche … même si ça n’est pas très utile.

B-A-BA Clojure

Bon, commençons par le commencement, lire la doc. Vous trouverez des ressources sur http://data-sorcery.org/ qui est en fait le blog de incanter.org. Un peu trop compliqué de prime abord. J’ai trouvé là http://www.chicoree.fr/w/Premiers_pas_avec_Clojure un tutorial très progressif même si c’est appliqué à un sujet différent. J’en ai repris quelques éléments.

On veut manipuler des données. Donc on va faire des opérations sur des nombres. Comment on fait une opération ?

user=> (+ 1 2)
3

Et oui, en Clojure, les opérateurs sont préfixés.

Clojure est un langage homoiconique, il représente les programmes comme des structures de données. Le programme est donc représenté par une liste d’éléments. D’un point de vue plus technique, c’est une S-expression, c’est à dire une séquence de termes entre parenthèses. Le premier terme est la fonction à appliquer et les termes suivants sont des arguments. N’importe quel terme peut être une sous-expression. L’opérateur + est une fonction comme une autre.

Ce qui  permet d’écrire un suite d’additions en passant la liste des valeurs ou d’appliquer une méthode à un objet (vous noterez le . et le fait que la méthode précède la chaîne à laquelle elle s’applique).

user=> (+ 1 2 3)
6
user=> (.toUpperCase "Hello")
"HELLO"

Conséquence de ce principe, il n’y pas de règles de précédence des opérateurs en Clojure, l’opérateur ne peut se trouver qu’au début de la séquence.

user=> (+ 1 (* 2 3))
7
user=> ; 1 + 2*3

Le symbole qui marque le début d’un commentaire est ;

Il existe aussi des fonctions prédéfinies telles que print ou println similaires au méthodes Java. Vous noterez que les , entre les termes sont facultatives.

user=> (print "Hello")
Hellonil
user=> (println "Hello", "Claude")
Hello Claude
nil
user=> (println "Hello" "Claude")
Hello Claude
nil

Mais d’ou sort ce nil ?

Les fonctions print n’ont pas de valeur de retour à la différence de +. L’affichage du texte est un effet de bord de la fonction. nil sert à plusieurs choses en Clojure, mais en particulier à exprimer la fin de séquence et le rien.

Encore quelques détails et nous voilà prêts à écrire Hello World.

Hello World !

Une fonction se définit en utilisant la fonction fn suivi des paramètres entre crochets et d’une expression qui définit le résultat à produire. Et le nom ? Est ce bien nécessaire d’avoir un nom ?

user=> (fn [name] (println "Hello" name))
#<user$eval61$fn__62 user$eval61$fn__62@10e9df>
user=> ( (fn [name] (println "Hello" name)) "Claude" )
Hello Claude
nil

Cette fonction anonyme est utilisable (enfin, pour peu qu’on mettre les parenthèses au bon endroit). Finalement, un nom c’est pratique. On va associer cette fonction à un nom en utilisant la même syntaxe que pour déclarer toute variable, la fonction def.

user=> (def moi "Claude")
#'user/moi
user=> (type moi)
java.lang.String
user=> (println moi)
Claude
nil

Et maintenant la fonction hello :

user=> (def hello (fn [name] (println "Hello" name)) )
#'user/hello

La fonction doc retourne des informations sur la fonction. A noter aussi plus haut, la fonction type.

user=> (doc hello)
-------------------------
user/hello
nil
   nil
nil
user=> (doc println)
-------------------------
clojure.core/println
([& more])
   Same as print followed by (newline)
nil

En pratique, les fonctions sont plutôt déclarées de la manière suivante, grace à la macro defn qui regroupe les deux étapes.

Hello world en Clojure.

user=> (defn hello [name] (println "Hello", name))
user=> (hello "Claude")
Hello Claude
nil

Et Incanter ?
Houlà !!! vous êtes bien pressé jeune sorcier. Il nous reste à voir les collections et quelques concepts secrets. Mais à chaque jour suffit sa peine. C’est tout pour aujourd’hui. La suite dans l’épisode 2.