6.8 KiB
Comment organiser du code fonctionnel
- Design Pattern en fonctionnel
- Fonctionnel
- Clojure
- Haskell
Design Pattern en fonctionnel
- Programmation Orienté Objet => design pattern
- Fonctionnel => fonctions
Fonctionnel
- LISP
- *ML
- Haskell
Clojure
Fonctions / Fonctions partout !
- Utiliser des fonctions
initialiser:
(defn main- [& args]
(do-thing-1)
(do-thing-2)
(do-thing-3))
Libs vs Applications
- Libs, un seul but précis.
- Applications, gestion de plusieurs sous fonctionalités.
Libs
Les libs presentent une liste de fonctions qui devraient être proche des fonctions au sens mathématiques.
La valeur de retour de (f x)
depend seulement de x
. Pas de modification
d'état. Evaluer (f x)
ne change pas l'état du système:
- pas de creation de fichier
- pas d'affichage sur l'écran
Applications
Gestions d'états internes, importations de plusieurs libs pour combiner leur capacités.
- Serveur
- DB
- Logs
- Authentication
Combiner ; Expression Problem
The expression problem is a term used in discussing strengths and weaknesses of various programming paradigms and programming languages.
The expression problem is a new name for an old problem. The goal is to define a datatype by cases, where one can add new cases to the datatype and new functions over the datatype, without recompiling existing code, and while retaining static type safety (e.g., no casts).
Philip Wadler
Clojure Protocols
(defprotocol P
(foo [x])
(bar-me [x] [x y]))
(deftype Foo [a b c]
P
(foo [x] a)
(bar-me [x] b)
(bar-me [x y] (+ c y)))
(bar-me (Foo. 1 2 3) 42)
= > 45
Protocoles utiles ?
Création d'une API standardisé pour plusieurs types différents. Par exemple, notion de CRUD pour différentes DB.
(defprotocol CRUD
(create [this id obj])
(read [this id])
(update [this id new-obj])
(delete [this id]))
(deftype Redis [redis-conf] ...)
(deftype Elasticsearch [es-conf] ...)
(deftype Postgres [pg-conf] ...)
État en paramètre
Problème central en fonctionnel. Gestion des états internes.
Functions:
(ns main
(:require
[my.redis]
[my.http-server]))
(defn main- []
(let [redis-state (init-redis)
http-server-state (init-http-server)]
(do-stuff redis-state http-server-state))
La différence fondamentale, l'état est explicite.
Supérieur en pratique à l'organisation orienté objet pour du code de taille moyenne à relativement grande.
Typiquement organisation en micro-services avec peu d'inter-dépendances.
État en RAM dans un atom
- Rend l'état implicite
- Changement d'état thread safe
Exemple (1/2)
(ns my.redis ...)
(def redis-state (atom {})
(defn init-redis []
(reset! redis-state
(new-redis-connections {:host "..." :port ...})))
(defn do-stuf [...]
(redis/do-stuff @redis-state ...))
Exemple (2/2)
(ns main
(:require
[my.redis]
[my.http-server]))
(defn main- []
(init-redis)
(init-http-server)
(do-stuff))
État central explicite
État central explicite ; exemple
(def state (atom {}))
(def init []
(let [svc1-state (init-svc1)
svc2-state (init-svc1)
svc3-state (init-svc1)]
(reset! state {:svc1-st svc1-state
:svc2-st svc2-state
:svc3-st svc3-state})))
(def main- []
(init)
(do-things))
État central explicite ; tests
- Facile à comprendre
- Possibilité de logger les changements d'états (voir elm architecture)
- undo presque gratuit
Mais:
- Difficile d'organiser l'init avec beaucoup de sous-composants/services
- Difficile de tester en parallèle, il faut "dupliquer" les states.
Graphes d'États
Graphes d'États ; Exemple
(def main- []
(let [svc1 (new-svc1)
svc2 (new-svc2 svc1)
...
svcN (new-svcN svc1 svc5)
services {:svc1 svc1
:svc2 svc2
...
:svcN svcN}]
(do-things services)))
Graphes d'États ; Abstraction
- component / trapperkeeper
(defprotocol CRUDService
(create [this id obj])
(read [this id])
(update [this id new-obj])
(delete [this id]))
(defservice crud-service
(init [this] ...)
(stop [this] ...)
(create [this id obj] ...)
(read [this id] ...)
(update [this id new-obj] ...)
(delete [this id] ...))
Graphes d'États ; Usage
fichier bootstrap.cfg
crud-service
db-service
http-server-service
fichier config.edn
(ou .json
, .properties
, etc…)
{:logging "dev-resources/logging.xml"
:server
{:user/user-web-service "/users"
:pizza/pizza-web-service "/pizzas"}
:jwt {:public-key "resources/cert.pub"
:private-key "resources/cert.key"}}
Usage réel
Choix du déploiement:
- plusieurs micro-services
- un seul gros super-service qui merge tous les micro-services
- un sous-ensemble de services
Avantages:
- scalabilité sur mesure
- Meilleur usage de la RAM
Purity
(defservice MyService
[ConfigService
SubService]
(init [this context]
(let [more-context (core/my-init ConfigService)]
(merge context more-context)))
(stop [this] (core/my-stop (service-context this)))
(do-stuff [this]
(let [handlers (:handlers (service-context this))]
(core/do-stuff handlers ...))))
Dans le fichier myservice/core.clj
les fn sont pures.
(defn do-stuff [handlers obj]
(let [create (:redis-create handlers)]
(create object)))
Testing
Les fonctions de cores sont pures donc reproductibles.
Test de semi-intégration; les services déjà lancés et l'état initialisé.
(with-app-with-config app services conf
(test-with-initiliazed-services ...))
Haskell
Règles pragmatiques
Organisation en fonction de la complexité
Make it work, make it right, make it fast
- Simple: directement IO (YOLO!)
- Medium: Haskell Design Patterns: The Handle Pattern: https://jaspervdj.be/posts/2018-03-08-handle-pattern.html
- Gros: MTL / RIO / Free / Freeer / Effects…
Handler Pattern
main = do
redisSvc <- newRedisHandler
serverSvc <- newServerHandler
doStuff redisSvc serverSvc
3 couches
-
Imperatif: ReaderT IO
- Insérer l'état dans une
TVar
,MVar
ouIORef
(concurrence)
- Insérer l'état dans une
-
Orienté Objet:
- Handle / MTL / Free…
- donner des access
UserDB
,AccessTime
,APIHTTP
…
- Fonctionnel: Business Logic
f : Handlers -> Inputs -> Command