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

380 lines
14 KiB
Org Mode

#+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.