#+Title: Comment organiser du code fonctionnel #+Author: Yann Esposito #+Date: Mercredi 27 mars 2019 * Design Pattern en fonctionnel - Programmation Orienté Objet => design pattern - Fonctionnel => fonctions * Fonctionnel - LISP - *ML - Haskell * Clojure *** Fonctions / Fonctions partout ! - Utiliser des fonctions initialiser: #+BEGIN_SRC clojure (defn main- [& args] (do-thing-1) (do-thing-2) (do-thing-3)) #+END_SRC *** 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. #+BEGIN_QUOTE 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 #+END_QUOTE *** Clojure Protocols #+BEGIN_SRC clojure (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 #+END_SRC *** Protocoles utiles ? Création d'une API standardisé pour plusieurs types différents. Par exemple, notion de CRUD pour différentes DB. #+BEGIN_SRC clojure (defprotocol CRUD (create [this id obj]) (read [this id]) (update [this id new-obj]) (delete [this id])) #+END_SRC #+BEGIN_SRC clojure (deftype Redis [redis-conf] ...) (deftype Elasticsearch [es-conf] ...) (deftype Postgres [pg-conf] ...) #+END_SRC *** État en paramètre Problème central en fonctionnel. Gestion des états internes. Functions: #+BEGIN_SRC clojure (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)) #+END_SRC 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) #+BEGIN_SRC clojure (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 ...)) #+END_SRC *** Exemple (2/2) #+BEGIN_SRC clojure (ns main (:require [my.redis] [my.http-server])) (defn main- [] (init-redis) (init-http-server) (do-stuff)) #+END_SRC ** État central explicite *** État central explicite ; exemple #+BEGIN_SRC clojure (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)) #+END_SRC *** É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 #+BEGIN_SRC clojure (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))) #+END_SRC *** Graphes d'États ; Abstraction - component / trapperkeeper #+BEGIN_SRC clojure (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] ...)) #+END_SRC *** Graphes d'États ; Usage fichier =bootstrap.cfg= #+BEGIN_SRC crud-service db-service http-server-service #+END_SRC fichier =config.edn= (ou =.json=, =.properties=, etc...) #+BEGIN_SRC clojure {: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"}} #+END_SRC *** 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 #+BEGIN_SRC clojure (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 ...)))) #+END_SRC Dans le fichier =myservice/core.clj= les fn sont pures. #+BEGIN_SRC clojure (defn do-stuff [handlers obj] (let [create (:redis-create handlers)] (create object))) #+END_SRC *** Testing Les fonctions de cores sont pures donc *reproductibles*. Test de semi-intégration; les services déjà lancés et l'état initialisé. #+BEGIN_SRC clojure (with-app-with-config app services conf (test-with-initiliazed-services ...)) #+END_SRC * Haskell ** Règles *pragmatiques* *** Organisation en fonction de la complexité #+BEGIN_QUOTE Make it work, make it right, make it fast #+END_QUOTE - 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 #+BEGIN_SRC haskell main = do redisSvc <- newRedisHandler serverSvc <- newServerHandler doStuff redisSvc serverSvc #+END_SRC *** 3 couches - *Imperatif*: ReaderT IO + Insérer l'état dans une ~TVar~, ~MVar~ ou ~IORef~ (concurrence) - *Orienté Objet*: + Handle / MTL / Free... + donner des access ~UserDB~, ~AccessTime~, ~APIHTTP~... - *Fonctionnel*: Business Logic ~f : Handlers -> Inputs -> Command~