Cet article est en cours de rédaction, son contenu peut évoluer sans préavis et les informations qu'il contient peuvent manquer de précisions.
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.
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.