deft/archives/pres-fp-architect-big-app.org
Yann Esposito (Yogsototh) 5138e54776
updated files
2019-04-24 21:27:19 +02:00

6.8 KiB

Comment organiser du code fonctionnel

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

Handler Pattern

main = do
  redisSvc <- newRedisHandler
  serverSvc <- newServerHandler
  doStuff redisSvc serverSvc

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