#+Title: Alexandre Delanöe #+Author: Yann Esposito * <2018-09-05 Wed> - John Rolls théorie de la justice. Se concentrer sur le plus mal loti. - democracie liquide - * <2018-07-09 Mar> ** Purescript thermite vs halogen #+BEGIN_SRC purescript data Component state action eff props = Component { initialState :: State , update :: PerformAction eff State props Action -- performAction , view :: Spec eff State props Action -- spec } #+END_SRC * <2018-07-10 Mar> ** 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. #+BEGIN_SRC purescript 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 } #+END_SRC La composition des action se fait de la même manière. Avec un type produit de toutes les actions des sous-composants: #+BEGIN_SRC purescript 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 #+END_SRC 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 #+BEGIN_SRC purescript -- ========== Lenses Declarations ============= #+END_SRC 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: #+BEGIN_SRC Component/ Actions.purs View.purs Lenses.purs #+END_SRC 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 #+BEGIN_SRC !! 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 #+END_SRC 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: #+BEGIN_SRC purescript 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 #+END_SRC Ç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: #+BEGIN_SRC purescript 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))] #+END_SRC le type de ~slot'~ est: #+BEGIN_SRC purescript 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 #+END_SRC #+BEGIN_QUOTE 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 #+END_QUOTE 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.