Bien commencer avec Clojure - Setup & prise en main

Bien commencer avec Clojure et IntelliJ - Setup & prise en main.

Avant-propos

Clojure et son écosystème est assez incroyable. Il y a selon moi 4 raisons principales qui rendent clojure si intéressant:

La JVM

L'avantage d'être construit sur la JVM permet à clojure d'être déployable facilement et sur beaucoup de matériel. Aussi Clojure bénéficie d'une interopérabilité avec JAVA

La programmation fonctionnelle (FP)

Clojure est parfait pour apprendre la programmation fonctionnelle.

La concurrence

En utilisant la programmation fonctionnelle (donc en faisant le choix de l'immutabilité) clojure rend la programmation sur plusieurs thread relativement simple.

Les macros

Globalement l'idée derrière les macros est de pouvoir changer sois même le code qu'on écrit en clojure, en gros il est possible d'introduire des bouts de langages supplémentaires et propre à son code.

macros transform code into different code

En tant que débutant, j'ai trouvé que la prise en main de clojure était plutôt rapide et agréable. Bien que l'écriture des s-expressions (parenthèses et compagnie) soit un peu déroutante au départ, j'ai trouvé une simplicité dans l'écriture et la compréhension du code qui me plait bien.

À la lecture de cet article, vous serez en mesure de faire le setup de votre environnement de développement et d'expérimenter avec clojure.

Prérequis

SDK 8 - 11

Clojure est un langage basé sur la JVM, il vous faudra avoir installé Java 8 ou Java 11 sur votre machine pour pourvoir utiliser clojure.

Pour ubuntu:

sudo apt install openjdk-11-jdk

Pour OS X : Je vous invite vivement a vous référer a ce tutoriel https://mkyong.com/java/how-to-install-java-on-mac-osx/

Java 15

Clojure semble très bien tourner avec Java 15. Ma config est composée de OpenJDK 15 et Open J9

Pour ubuntu:

sudo apt install openjdk-15-jdk
# installe le JDK dans répertoire /usr/lib/jvm/openjdk-15-jdk

Pour OS X j'ai utilisé ce tutoriel qui est très exhaustif https://mkyong.com/java/how-to-install-java-on-mac-osx/

Clojure

Pour installer clojure sur OSX

brew install clojure/tools/clojure

Pour installer clojure sur une distribution Linux

curl -O <https://download.clojure.org/install/linux-install-1.10.1.754.sh>
chmod +x linux-install-1.10.1.754.sh
sudo ./linux-install-1.10.1.754.sh

Entrez clojure en ligne de commande vous devriez voir apparaitre ce genre d'output :

Clojure 1.10.1
user=>

Setup de l'environnement de Développement.

IntelliJ IDEA

Installer cursive

Cursive est le plugin recommandé pour développer en Clojure sur IntelliJ IDEA.

Cursive nécessite une licence. Cette licence est gratuite pour un usage personnel, rendez-vous [ici] pour inscrire.

L'installation est assez directe, rendez-vous dans les préférences d'IntelliJ ⌘, → plugins (file → settings sur ubuntu) puis entrez cursive dans la barre de recherche. Cliquez sur install suivez les instructions d'IntelliJ IDEA.

Premiers pas avec Clojure

Créer un projet Leiningen avec IntelliJ IDEA

Maintenant que votre IDE est prêt pour écrire du Clojure nous allons pouvoir créer votre premier projet Clojure en utilisant Leiningen.

Leiningen, en plus d'être une petite bourgade de l'est de l'Allemagne, est un build tool qui se veut plus simple que Maven et conçu pour Clojure.

🗣️
Automate Clojure projects without setting your hair on fire - Leiningen moto

Ce qui va nous intéresser dans un premier temps sera la résolution de dépendance ainsi que les configurations du REPL

Pour créer votre projet, dans IntelliJ IDEA :

  • File → new project
  • Sélectionnez Clojure dans le menu de gauche.
  • Sélectionnez Leiningen puis cliquez sur next
  • Définissez votre nom de projet, la localisation de votre projet sur votre disque et le JDK que vous souhaitez utiliser.
  • HIT FINISH !

Domptez le REPL

Désormais vous devriez vous retrouver avec un projet qui ressemble à ça:

.
├── CHANGELOG.md
├── LICENSE
├── README.md
├── doc
│   └── intro.md
├── project.clj
├── resources
├── src
│   └── this_is_the_project_name
│       └── core.clj
├── target
│   └── classes
├── test
│   └── this_is_the_project_name
│       └── core_test.clj
└── this-is-the-project-name.iml

Nous allons maintenant lancer une session REPL afin d'évaluer et d’expérimenter avec notre code à mesure que nous l'écrivons.

En haut de votre IDE cliquez sur add configuration puis dans la fenêtre qui vient de s'ouvrir cliquez sur + Clojure REPL → local

Laissez la configuration tel quel (vous pouvez éventuellement lui donner un nom) cliquez sur ok et enfin lancez le ▶️

Ouvrez votre fichier src/<project_name/core.clj (Cf. l'arbre précédent)

Entrez du code clojure valide tel que : (+ 1 2) puis charger cette expression dans le REPL à l'aide du raccourcis shift cmd p

(+ 1 2)
=> 3

TADA !! Vous avez évalué du code Clojure dans votre session REPL.

Jouons un peu

Avant d'aller chercher plus de détails (détails qui seront surement discutés lors d'un autre post - vous pouvez aussi vous référer à la section ressources pour aller plus loin) commençons avec quelques bases.

Les expressions

Si vous avez comme moi vous avez plutôt utilisé des langages C-like le premier contact avec Clojure peut être un peu déroutant.

Clojure à une structure d'écriture extrêmement simple : parenthèse fonction arguments parenthèse (ou disons que c'est tout ce dont vous avez besoin pour commencer)

Vous pouvez dès lors vous retrouver avec des expressions un peu étranges, surtout quand il s'agit d’arithmétique ex : (+ 1 2 3) => 6

En imbriquant les expressions vous pouvez vous retrouver avec des expressions à l'allure un peu plus complexe

(+ (* 2 3) (* 4 2) 3) => 17

Les structures de contrôle

Avant de manipuler les structures de contrôle il est important de bien comprendre la truthyness des expressions Clojure.

En Clojure, est considéré comme falsy false & nil (respectivement le booléen et l'absence de valeur).

Est considéré comme truthy, à peu près tout le reste.

(if true 
  :it-is-truthy 
  :it-is-fasley) => :it-is-truthy 

(if "hello" 
  :it-is-truthy 
  :it-is-falsey) => :it-is-truthy 

(if (+ 1 2) 
  :it-is-truthy 
  :it-is-falsey) => :it-is-truthy 

(if nil 
  :it-is-truthy 
  :it-is-falsey) => :it-is-falsy 

Les structures de contrôle principales sont décrites en commentaires :

(def there-are-fish true)
(def im-hungry true)
(def there-are-pirates false)

;; if
(if there-are-fish
  (println "We're lucky")
  (println "We're not lucky"))

;; do
;; `do` permet d'executer un ensemble d'evaluations de maniere synchrone
;; `do` allow you to execute many evaluations in a synchronous way
(do
  (println "print this")
  (println "then print that"))

;; when 
;; `when` est globalement un if sans cas falsy
;; `when` is essentially a if without a "falsy case"
(when there-are-fish
  (println "blob blob"))

;; or
;; `or` renvoie la premiere valeur truthy
;; `or` returns the first truthy value
(or there-are-pirates there-are-fish) ;; => true
(or :truthy true) ;; => :truthy 

;; and
;;   Evalue chaque expression et renvoie false à la premiere expression 
;; falsy ou renvoie l'évaluation de la derniere expression
;;   Evaluates each expression at once and returns false if bumps across 
;; a falsy evaluation. 
;; Else it'll return the evaluation result of the last expression
(and there-are-pirates there-are-fish)

;; (prérequis pour les prochaines structures) let
;;   Let permet de faire des local bindings (lire variable locales) 
;; avant une expression
;;   Let allow you to declare local bindings (local variables) 
;; before an expression
(let [trois (+ 1 2) 
      deux (+ 1 1)]
  (+ trois deux)) => 5

;; if-let
;;   Si le test dans les bindings est true, cette structure evalue l'expression 
;; en prenant compte de ce binding. Sinon elle évalue l'expression du else.
;;   If the structure is provided with a truthy local binding, if-let will evaluate the following expression 
;; while taking into account the tested local binding. Else it will evaluate the else expression on its own.
(defn should-i-go-swimming [pirate-presence]
  (if-let [x pirate-presence]
    "Hell no !"
    "No pirates, all good"))

(should-i-go-swimming there-are-pirates)

;; when-let
(defn speak-loudly [pirate-presence]
  (when-let [presence (not pirate-presence)]
    (println "Blablabla")
    (println presence)))

(speak-loudly there-are-pirates)

Les structures de données

Clojure permet de manipuler un ensemble restreint de structures de données :

  • number
  • string
  • map
  • keyword
  • vectors
  • lists
  • sets

Voici quelques exemples de manipulation ainsi que leurs explications en commentaire.

strings

;; =====================STRINGS======================
;; str
(require '[clojure.string :as str])
(str "I can eat " 10 " sushi")
;; blank?
(str/capitalize "hello")
(str/ends-with? "hello" "lo")
(str/escape "test" {\\e "oa"})
(str/includes? "hello" "hell")
(str/index-of "hello" "e")
(str/join ", " ["hello" "world"])
(str/last-index-of "hello" "l")
(str/lower-case "HELLO")
(str/replace "I like spinach because spinach are healthy" #"spinach" "sushi")
(str/replace-first "I like spinach because spinach are healthy" #"spinach" "sushi")
(str/reverse "hello")
(str/split "hello world test" #" ")
(str/split-lines "hello world \\n test")
(str/starts-with? "hello" "hel")
(str/trim "       hola  ")
(str/trim-newline "test\\n\\r")
(str/triml "       hola  ")
(str/trimr "       hola  ")
(str/upper-case "hello")

maps

Les maps en Clojure sont des tableaux associatifs, équivalent à ce que l'on trouve en JSON/JS avec {"a": 1, "b": "text"}. On va pouvoir manipuler ces structures avec les clés.

(def personal-taste {:california-roll true
                     :raw-squid       false
                     :lower-case-fish "fish"})

;; get
;; get something from a map
(get personal-taste :california-roll)
;; with the key directly if it's a keyword
(:california-roll personal-taste)

;; seq
;; seq retourne false en présence d'une sequence (list map etc) vide
;; seq will return false in in presence of an empty sequence (list map etc)
(seq personal-taste)

;; nested maps
(def nested {:memes  true
             :nested {:ok    "boomer"
                      :bi*ch "please"}})
;; get-in
;;   Retourne une valeur nichée dans une map à plusieurs niveaux 
;; en donnant le chemin de la valeur
;;   Brings value in nested map given a path
(get-in nested (list :nested :ok))

;; assoc 
;; Ajoute un couple clé-valeur a une map (tableau associatif)
;; add key-value pair to a map
(assoc personal-taste :sushi true)

;; assoc-in
(assoc-in nested [:nested :new-key] "value")

;; update
(update personal-taste :lower-case-fish #(clojure.string/capitalize %))
(update personal-taste :lower-case-fish clojure.string/capitalize)
(= (update {:a 1} :a - 2 3)
   (update {:a 1} :a #(- % 2 3)))

;; update-in
(update-in nested [:nested :ok] #(clojure.string/capitalize %))

;; merge
(merge {:a "blowfish" :b "rat"} {:b "clown-fish"}) 
;;{:a "blowfish", :b "clown-fish"}

;; zipmap
(zipmap [:a :b :c] ["blowfish" "catfish" "tuna"]) 
;; {:a "blowfish", :b "catfish", :c "tuna"}

;; keys
;; retourne les clé d'une map
;; return keys of map
(keys (zipmap [:a :b :c] ["blowfish" "catfish" "tuna"]))

;; vals
;; retourne les valeurs d'une map
;; return values of a map
(vals (zipmap [:a :b :c] ["blowfish" "catfish" "tuna"]))

;; conj (conjoin)
;; ajoute des valeurs à la fin d'une sequence
;; adds a value at the end 
(conj {:a "blowfish" :b "tuna"} {:c "catfish"})
(conj {1 2 3 4} [5 6])

;; into
(into (hash-map) [[:a "blowfish"] [:c "blowfish"] [:b "blowfish"]])

Vectors

Les vecteurs sont des séquences en Clojure équivalentes à des tableaux. L’accès aux valeurs se fait en temps constant.

;; =====================VECTORS=======================
(def fish-vector ["blowfish" "lowfish" "owfish"])
;; get
;; retourne la valeur de fish vector a l'index 1
;; return value at position 1 in our vector 
(get fish-vector 1)

;; map
;;   Applique une fonction a chaque elt du tableau et renvoie les resultats 
;; sous forme d'une nouvelle liste
;;   Apply function to each element of the array and returns a new list.
(map clojure.string/upper-case fish-vector)

;; assoc
;; ajoute un element au tableau
;; add an  element to a vector
(assoc [:blob :bloub] 2 :blib)

;; update
;; Met a jour un element d'un vecteur en lui passant un index et une fonction
;; Update element of a vector given an index and a function
(update ["bloub" "blib"] 0 str/upper-case)

;; assoc-in
;; assoc dans un vecteur de vecteurs
;; assoc in vector of vectors
(assoc-in [[1 0 0]
           [0 1 0]
           [0 0 1]] [1 1] 2)

;; update-in
;; update dans un vecteur de vecteurs
;; update in vector of vectors
(update-in [[1 0 0]
            [0 1 0]
            [0 0 1]] [1 1] inc)

;; conj
;; conj permet de joindre un element à la fin d'un vecteur
;; Add an element to the end of a vector
(conj [:blob :blib] :bloub)

;; into
(into [:blob :blib] (list :bloub :blips))

;; get-in
(get-in [[1 0 0]
         [0 :blob 0]
         [0 0 1]] [1 1])

;; peek
(time (peek [1 2 3 4]))
(time (last [1 2 3 4]))

;; pop
(pop [1 2 3 4])

Lists

Les listes Clojure sont plus ou moins des listes chainées.

Bien qu'omniprésentes dans la composition du code Clojure (Clojure est un dialect LISP -> LISt Processing), on peut se poser la question de ses avantages par rapport aux tableaux.

Les listes sont à privilégier dans 2 cas :

  • Lorsque l'on cherche à générer du code (notamment dans les macros)
  • Lorsque l'on souhaite ajouter du contenu en tête d'une séquence facilement
;; =====================LISTS=====================
;; cons
(cons 1 (cons 2 (list 3 4)))

;; concat
(concat (list 1 2) (list 3 4))

;; first
(first (range 10))

;; rest
(rest (range 10))
(rest ())

;; next
(next (range 10))
(next ())

;; nth
(nth (list 1 7 9) 1)

;; conj
(conj (list 1 2 3) 0)

Sets

;; =====================SETS=====================

(require '[clojure.set :as set])

;; litteral
#{1 2}
;; ops
(hash-set 1 1 1 2)
(conj #{:fish :pirate} :fish)
;; contains
(contains? #{:fish :pirate} :fish)

;; difference
(set/difference #{:fish :pirate} #{:blowfish :pirate})
;; index
(def weights #{{:name "fish" :weight 1000}
               {:name "catfish" :weight 800}
               {:name "blowfish" :weight 1000}})

(set/index weights [:weight])
;; intersection
(set/intersection #{:fish :pirate} #{:blowfish :pirate})
;; join
(set/join #{{:a "tuna"} {:a "dorade"}} #{{:b 'salt} {:b 'pepper}})
;; map-invert
(set/map-invert {:a 1, :b 2})
;; project // projection select in sql
(set/project #{{:name "tuna" :id 33} {:name "blowfish" :id 34}} [:name])
;; rename
(set/rename #{{:name "test", :b 1} {:name "chose", :b 2}} {:name :NaMe})
;; rename-keys
(set/rename-keys {:a 1, :b 2} {:a :new-a, :b :new-b})
;; select
(set/select odd? (into #{} (range 10)))
;; subset?
(set/subset? #{2 3} #{1 2 3 4})
;; superset?
(set/superset? #{1 2 3 4} #{2 3})
;; union
(set/union #{1 2} #{2 3} #{3 4})

Les fonctions

Définir une fonction

(defn print-arg1-arg2 [arg1 arg2] 
  (println (str arg1 arg2)))

(print-arg1-arg2 "hello " "world")

Créer une fonction anonyme

(fn [arg1 arg2] (println (str arg1 arg2)))

Le mot de la fin…

Si vous êtes arrivé jusque-là, bien joué ! Ça y est, vous êtes bien installé et vous savez quel article consulter/recommander pour le setup de Clojure + IntelliJ 😉.

Vous pouvez désormais commencer à résoudre des problèmes de manière élégante et simple avec Clojure. La route n'est pas finie, mais les premiers 100 m sont déjà derrière vous.

J'anticipe le fait que vous allez commencer à ne plus pouvoir vous passer de clojure et la philosophie qui entoure cette technologie. Je vous invite alors à aller consulter le lien suivant pour vous instruire davantage et aller plus loin.

Ressources