deft/anoe.org
Yann Esposito (Yogsototh) 565a07a75d
update
2018-09-28 17:45:29 +02:00

14 KiB

Alexandre Delanöe

<2018-09-05 Wed>

  • John Rolls théorie de la justice. Se concentrer sur le plus mal loti.
  • democracie liquide

<2018-07-09 Mon>

Purescript thermite vs halogen

data Component state action eff props =
     Component { initialState :: State
               , update :: PerformAction eff State props Action -- performAction
               , view :: Spec eff State props Action -- spec
               }

<2018-07-10 Tue>

Notes de pre-lecture du code de purescript-gargantext

Main

Utilise Navigation pour avoir le layoutSpec (la vue) et initAppState. Utilise aussi unsafePartial.

Effects, seulement, dom, console et ajax.

détails: J'aurai ajouté un type à setRouting pour signifier qu'il s'agit d'une fn qui va appliquer une action à un routing.

le unsafePartial ne semble pas tellement utile, mais pas dangereux non plus.

Navigation

Semble centraliser toutes les pages du site. Elle centralise l'état de l'application qui est un produit de tous les états des sous-composants.

C'est une première grosse différence avec Halogen il me semble. Dans halogen, chaque composant à son état initial. Ici, c'est au composant root de s'occuper de l'initialisation comme avec elm.

initAppState :: AppState
initAppState =
  { currentRoute   : Just Home
  , landingState   : L.initialState
  , loginState     : LN.initialState
  , addCorpusState : AC.initialState
  , docViewState   : DV.tdata
  , searchState    : S.initialState
  , userPage       : U.initialState
  , docAnnotationView   : D.initialState
  , ntreeView : NT.exampleTree
  , tabview : TV.initialState
  , search : ""
  , corpusAnalysis : CA.initialState
  , showLogin : false
  , showCorpus : false
  , graphExplorer : GE.initialState
  , initialized : false
  , ngState : NG.initialState
  , dashboard : Dsh.initialState
  }

La composition des action se fait de la même manière. Avec un type produit de toutes les actions des sous-composants:

data Action
  = Initialize
  | LandingA   L.Action
  | LoginA     LN.Action
  | SetRoute   Routes
  | AddCorpusA AC.Action
  | DocViewA   DV.Action
  | SearchA    S.Action
  | UserPageA  U.Action
  | DocAnnotationViewA  D.Action
  | TreeViewA  NT.Action
  | TabViewA TV.Action
  | GraphExplorerA GE.Action
  | DashboardA Dsh.Action
  | Search String
  | Go
  | CorpusAnalysisA CA.Action
  | ShowLogin
  | ShowAddcorpus
  | NgramsA  NG.Action

Contrairement à halogen, il n'y a pas de distinction entre les actions interne aux sous-composants et aux messages envoyé du sous-composant au composant père. Il me semble cependant qu'il n'y a pas besoin de déclarer des actions des sous-sous-composants.

En tout cas, ici, la situation semble plus avancée et peut-être plus saine avec Halogen. Cependant je vois difficilement comment "simuler" la situation halogen avec thermite.

Celà étant dit, la complexité du composant s'en ressent, et aussi le "couplage" entre composant fils et père est donc plus importante avec thermite qu'avec Halogen. Cependant, tant que l'application frontend ne dépasse pas une complexité énorme. Je ne pense pas que celà constitue un réel problème avant longtemps.

La situation est déjà beaucoup plus clean que dans la plupart des autres systèmes qui font du frontend. On est au moins au niveau de clarté d'organisation de elm ici.

Digression sur la distinction des Action en Queries et Events

Une petite remarque il faudrait vérifier si possible que le nomage des actions corresponde dans la mesure du possible à du vocabulaire du Domaine. Un peu comme avec le DDD. C'est-à-dire que dans l'idéal les actions devrait être compréhensible par une personne non technique. Typiquement "bouton-nouveau-tab-cliqué" etc…

Il me semble aussi qu'il faut bien faire des distinctions entre action passées (ButtonClicked qui sous-entendent que l'action a été acceptée et devrait systématiquement provoquer un changement d'état) des "Queries" qui sont des action de demande de quelque chose par exemple "GetCorpora" qui n'est donc pas au passé mais au présent et qui n'engage aucun changement d'état mais va provoquer un effet de bord qui lui engendrera potentiellement un ou plusieurs Event qui eux provoqueront un changement d'état.

Ce n'est pas essentiel, mais je trouve que c'est une bonne distinction à avoir pour clarifier la situation. En particulier cela perme de splitter la fn performAction en deux sous-parties. Une partie "pure" qui s'occupe des actions passée et qui ne provoque pas d'effet de bord (ou à la limite juste console). Et une partie impure qui n'a pas accès à la state monade pour changer l'état interne. Mais qui doit pouvoir re-evenvoyer des events.

Il me semble qu'à ce stade il est un peu prématuré de s'occuper de ça. Ce n'est pas urgent. Malgré tout, celà permettrait de faire encore un peu plus propre. En particulier, celà peut-être très utile pour debugger. Parce qu'il suffit de s'occupter des Actions de type Event et d'oublier les actions de type Query pour reproduire l'état interne. Cela permet entre autre de "sauvegarder" les résulats des effets de bords qu'un utilisateur aurait rencontré. Et ainsi en regardant les Event on peut retrouver à la fois l'etat interne de l'app à un moment donné pour un utilisateur avec aussi le moyen de remonter le temps pour voir à quel moment un bug s'est produit pour un utilisateur dans ses conditions à lui (par exemple pb de connection difficile à reproduire)

Pour en revenir au cas particulier de l'app. On voit par exemple l'action ShowLogin, qui n'a pas d'effet de bord (à part du log) et je renommerai cette action en un event qui devrait être un verbe au passé. LoginPageSelected ou quelque chose de similaire.

Comme je l'ai dit c'est prématuré, mais peut-être penser à cette disctinction sera utile pour minimiser un tout petit peu la complixité de l'appli et gagner en "scalabilité de l'app". Par scalabilité j'entends la capacité d'ajouter facilement de nouveaux composant en minimisant le coût de dev.

Remarque importante: je n'ai pas fait cette distinction dans mon code avec Halogen parce je n'en ai pas encore ressenti la nécessité.

Lenses

J'imagine qu'il n'était pas possible d'avoir une fonction qui génère les lenses automatiquement comme c'est le cas avec generic-lenses en Haskell. Celà rend malheureusmeent la lecture des modules un peu pénible à cause de la grande répétition du code.

Digression sur l'isolation et l'organisation

Personnellement j'aurai peut-être isolé ces déclarations soit en fin de fichier après un commentaire qui marque bien la separation

-- ========== Lenses Declarations =============

L'autre possibliité étant de créer un répertoire différent par composant. Chaque répertoire contenant les parties qui le compose, notamment imaginer un module Lenses par composant:

Component/
  Actions.purs
  View.purs
  Lenses.purs

Ce qu'il ne faut surtout pas faire, c'est de créer un répertoire qui mixe les infos de tous les composants (c'est pourtant courant). Typiquement il faut éviter

!! DON'T DO THAT
Actions/
  Component1.purs
  Component2.purs
  ...
  ComponentN.purs
Lenses/
  Component1.purs
  Component2.purs
  ...
  ComponentN.purs
Views/
  Component1.purs
  Component2.purs
  ...
  ComponentN.purs

Vraiment c'est tout pourri de faire, ça. C'est angular qui fait ça et ça rend la vie des developper nulle. Mais ça ne se voit pas tout de suite. Donc vraiment il faut regrouper les composants, pas séparer les concerns techniques. De plus cette façon de découper est anti-modulaire.

Views (Spec)

L'idée d'utiliser des lenses pour les etats et les actions est très bonne en tout cas. Ça me semble la chose naturelle à faire. Il me semble qu'avec Halogen, ce genre de chose se fait plus naturellement. Typiquement voici le code pour sélectionner le composant visibile en fonction de la route:

pagesComponent :: forall props eff. AppState -> Spec (E eff) AppState props Action
pagesComponent s =
  case s.currentRoute of
    Just route ->
      selectSpec route
    Nothing ->
      selectSpec Home
  where
    selectSpec :: Routes -> Spec ( ajax    :: AJAX
                                 , console :: CONSOLE
                                 , dom     :: DOM
                                 | eff
                                 ) AppState props Action
    selectSpec CorpusAnalysis = layout0 $ focus _corpusState  _corpusAction CA.spec'
    selectSpec Login      = focus _loginState _loginAction LN.renderSpec
    selectSpec Home        = layout0 $ focus _landingState   _landingAction   (L.layoutLanding EN)
    selectSpec AddCorpus  = layout0 $ focus _addCorpusState _addCorpusAction AC.layoutAddcorpus
    selectSpec DocView    = layout0 $ focus _docViewState   _docViewAction   DV.layoutDocview
    selectSpec UserPage   = layout0 $ focus _userPageState  _userPageAction  U.layoutUser
    selectSpec (DocAnnotation i)   = layout0 $ focus _docAnnotationViewState  _docAnnotationViewAction  D.docview
    selectSpec Tabview   = layout0 $ focus _tabviewState  _tabviewAction  TV.tab1
    -- To be removed
    selectSpec SearchView = layout0 $ focus _searchState _searchAction  S.searchSpec
    selectSpec NGramsTable  = layout0 $ focus _ngState _ngAction  NG.ngramsTableSpec
    selectSpec PGraphExplorer = focus _graphExplorerState _graphExplorerAction  GE.specOld
    selectSpec Dashboard = layout0 $ focus _dashBoardSate _dashBoardAction Dsh.layoutDashboard

Ça rend le code correct. Mais on sent qu'il y a encore pas mal de boilerplate. Honnêtement je trouve ça très acceptable. Malgré tout je préfère encore la situation avec Halogen.

Je vais tenter d'expliquer pourquoi.

Avec halogen, le rendu se fait avec une fonction qui s'appelle slot ou la fonction plus générique slot'.

En voici un exemple d'utilisation:

type ChildQuery = Coproduct2 Alerts.Query Bot.Query
type ChildSlot = Either2 AlertsSlot BotSlot

HH.div_ [ HH.slot'
          CP.cp1
          (AlertsSlot unit)
          Alerts.alertsComponent st.newAlert absurd
        , HH.slot'
          CP.cp2
          (BotSlot b.name)
          (botComponent initInfo)
          unit
          (HE.input (HandleBotMessage b.name))]

le type de slot' est:

slot' :: forall f g g' p p' i o m.
          ChildPath g g' p p'
          -> p
          -> Component HTML g i o m
          -> i
          -> (o -> Maybe (f Unit))
          -> ParentHTML f g' p' m

Defines a slot for a child component when a parent has multiple types of child component. Takes:

  • the ChildPath for this particular child component type
  • the slot "address" value
  • the component for the slot
  • the input value to pass to the component
  • a function mapping outputs from the component to a query in the parent

Le ChildPath c'est l'équivalent d'un truc qui marche pour Lens et Prism ;). en gros le CP.cp1 ça dit (le premier element du product ou de la somme selon mes besoins). Et on voit que je ne file pas l'etat initial, mais seulement ce qu'Halogen appelle un Input.

Donc l'input c'est quelque chose qui sera utiliser pour engendrer l'état initial du sous composant si nécessaire.

Honnêtement, il y a des avantages et des inconvénient. L'inconvénient à mon avis c'est que c'est moins explicite.

L'avantage, c'est que ça parait plus modulaire.

dispatchAction

L'utilisation de dispatchAction est assez sympa. Elle permet de s'occuper de l'initializer qui manque cruellement à Halogen pour l'instant, mais qui ne va pas tarder à arriver.

Remarque sur les logs

Il me semblerai plus judicieux de créer une fichier d'util pour faire les logs qui soit facilement désactivable pour la mise en prod. C'est pas terrible de conserver les logs dans la console du front. D'autant plus qu'il n'est pas impossible que ça fasse bugger certain browser pourri comme IE. Dans tous les cas, c'est pas cool et ça fait pas pro d'ouvrir la console en prod et de voir plein de logs. Faut un mode où quand on build pour la prod, les logs soient absent. Par contre aussi dans l'idéal ça serait pas mal d'avoir un système qui centralise les call aux actions et qui enregistre ça dans le localStorage et d'ajouter un bouton pour soumettre un bug et quand on clique sur le bouton, ça récupère la liste des Events. Normalement à partir de ça, il y a moyen d'écrire une fonction qui "rejoue" les events pour son app.

J'ai dit localStorage, mais, ça peut être n'importe quoi, même ne pas sortir du purescript.

Routing

Le routing me semble très bien. rien à redire là dessus. L'usage direct du localStorage me semble aussi très facile à obtenir, j'imagine via l'effect Dom. Bon ma recommendation est à l'avenir d'éviter d'utiliser des JWT pour des sessions et de plutôt utiliser des Session serveur via les cookies. Mais bon. On peut survivre trèèèèèès longtemps sans ça. D'autant plus que je ne pense pas que les données soient très critiques à protéger.

Composants

Projects

à faire.

GraphExplorer

L'utilisation de js externe semble en réalité identique entre Halogen et Thermite. Et le code me semble clean.

Gargantext/Dashboard

Personnelement je ne connais pas les détails de react, j'ai toujours considéré l'utilisation des features "avancées" comme un anti-pattern. Du coups, je remarque immédiatement l'utilisation de Props. Mon intuition me dit que c'est pas beau, et non nécessaire avec du purescript.

Cependant, il s'agit peut-être de la bonne façon de s'intégrer avec React. Le fait qu'Halogen n'utilise pas react est pour moi un avantage. Mais j'imagine que ça peut-être un inconvénient si un des but est de pouvoir exporter et publier des composants react pour des 3rd party.

Donc je préfère ne pas m'étendre sur le sujet.

Je ne suis même pas certain que cette partie soit réellement utilisée.

Tab/TabView

Un poil difficile à lire. Mais c'est peut-être inévitable pour ce type de composant. Cependant, je ne suis pas certain de bien voir pourquoi Tab est isolé de TabView.

Avec l'esprit halogen, j'imagine qu'il ne serait pas beaucoup plus facile à écrire. Mais j'imagine ce serait plus lisible, puisqu'ici il faut gérer les vues et les actions de façon explicite là où la notion de slot et de composant halogen permet d'abstraire ces détails.