diff --git a/HWP.org b/HWP.org new file mode 100644 index 00000000..009946e5 --- /dev/null +++ b/HWP.org @@ -0,0 +1,44 @@ +#+TODO: TODO TO-CLEAN TO-REVIEW | DONE +#+TITLE: Haskell for the working programmer +#+AUTHOR: Yann Esposito +#+EMAIL: yann.esposito@gmail.com +#+LANGUAGE: en +#+KEYWORDS: haskell +#+PROPERTY: header-args :output-dir HWP :mkdirp yes :tangle-mode (identity #o755) + +#+BEGIN_COMMENT +/THIS IS A WORK IN PROGRESS/ + +*CONTRIBUTORS* + +- To extract all source code files do ~org-babel-tangle~ (, b t) in spacemacs +- To go to an imported file use (, ') + +Audience of this book: +- people that are asked to work in Haskell but don't know much about it +- people that already tried a bit of Haskell and want to go further +- people that need to make a professional level product with Haskell +- people that are ok not understanding every detail yet but need the job to be done + +Book is not for: +- Total beginner that just want to know what Haskell is about +- Haskell contributors that already know a lot of things about Haskell + + +Goals: + +After finishing this book an reader should be able to: + +- script in Haskell instead of bash +- write a REST API with a swagger documentation and deploy it +- perhaps write a frontend app (I don't know looks like a LOT of work) + +#+END_COMMENT + + +#+Include: HWP/1_Introduction.org +#+Include: HWP/2_Install.org +#+Include: HWP/3_Intermediate.org +#+Include: HWP/4_Production.org +#+Include: HWP/5_Advanced.org +#+Include: HWP/6_Appendices.org diff --git a/anoe.org b/anoe.org new file mode 100644 index 00000000..14c0088b --- /dev/null +++ b/anoe.org @@ -0,0 +1,376 @@ +#+Title: Alexandre Delanöe +#+Author: Yann Esposito + +* <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.