Track new files with git
This commit is contained in:
parent
9951e15998
commit
3642b793fd
2 changed files with 420 additions and 0 deletions
44
HWP.org
Normal file
44
HWP.org
Normal file
|
@ -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
|
376
anoe.org
Normal file
376
anoe.org
Normal file
|
@ -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.
|
Loading…
Reference in a new issue