#+Title:Nicolas Meetings * Etude Transient ** Installation et Test *** Node.js Il faut avoir une version récente de node.js (j'ai la 6.5.0) *** Cabal file #+BEGIN_SRC cabal build-depends: base , transient , transient-universe , transformers , ghcjs-hplay , containers #+END_SRC *** stack.yaml #+BEGIN_SRC yaml resolver: lts-6.15 packages: - '.' extra-deps: - ghcjs-perch-0.3.3 - transient-0.4.2 - transient-universe-0.3.2.1 - ghcjs-hplay-0.3.4 flags: {} extra-package-dbs: [] #+END_SRC *** stack-ghcjs.yaml #+BEGIN_SRC yaml resolver: lts-6.15 packages: - '.' extra-deps: - ghcjs-perch-0.3.3 - transient-0.4.2 - transient-universe-0.3.2.1 - ghcjs-hplay-0.3.4 flags: {} extra-package-dbs: [] # GHCJS compiler: ghcjs-0.2.0.9006015_ghc-7.10.3 compiler-check: match-exact setup-info: ghcjs: source: ghcjs-0.2.0.9006015_ghc-7.10.3: url: "https://tolysz.org/ghcjs/lts-6.15-9006015.tar.gz" sha1: 4d513006622bf428a3c983ca927837e3d14ab687 #+END_SR *** Compile & Start **** Compile #+BEGIN_SRC zsh #!/usr/bin/env bash -x if command -v stack >/dev/null 2>&1 then echo "stack is already installed." else echo "stack is not installed. I will try to install it." curl -sSL https://get.haskellstack.org/ | sh fi # Downloading Haskell Compiler this can be very long stack setup stack --stack-yaml stack-ghcjs.yaml setup # Building the project the first time it will also download and compile # library stack build stack --stack-yaml stack-ghcjs.yaml build # Link GHCJS result to the correct directory (static/out.jsexe) ./mklink.sh #+END_SRC **** Start #+BEGIN_SRC zsh #!/usr/bin/env bash stack exec -- wse-exe -p start/localhost/3000 #+END_SRC **** Test 1. =./compile.sh= 2. =./start.sh= 3. Aller sur =http://localhost:3000= avec plusieurs navigateurs. À noter des reload nécessaires avec Safari, marche très bien avec Firefox. ** Explication *** Le Code Voici le code suivant #+BEGIN_SRC haskell import Prelude hiding(div,id) import Transient.Base import Transient.Move import Transient.EVars import Transient.Move.Utils import GHCJS.HPlay.View hiding (option,input) import Data.IORef import Data.String (fromString) import Control.Monad.IO.Class import Data.Monoid ((<>)) import Data.Typeable (Typeable) data MyForm = MyForm { i1, i2 :: String } deriving (Show,Read,Typeable) main :: IO () main = keep $ do rdata <- liftIO $ newIORef (MyForm "i1" "i2") dataAvailable <- newEVar initNode $ formWidget rdata dataAvailable <|> syncWidget dataAvailable <|> longLivingProcess rdata dataAvailable formWidget :: IORef MyForm -> EVar MyForm -> Cloud () formWidget rdata dataAvailable = onBrowser $ do local . render . rawHtml $ do h1 "State" div ! id (fromString "msg") $ "No message yet." st <- atRemote . localIO $ readIORef rdata -- read the state data myForm <- local . render $ MyForm <$> getString (Just (i1 st)) `fire` OnKeyUp <*> getString (Just (i2 st)) `fire` OnKeyUp -- notify the long living process atRemote $ localIO $ writeEVar dataAvailable myForm syncWidget :: EVar MyForm -> Cloud () syncWidget dataAvailable = onBrowser $ do -- display in the browser received <- atRemote $ local $ readEVar dataAvailable local . render . at (fromString "#msg") Insert . rawHtml $ p $ "Check other navigators: " <> i1 received <> ", " <> i2 received longLivingProcess :: IORef MyForm -> EVar MyForm -> Cloud () longLivingProcess rdata dataAvailable = onServer $ local $ do liftIO $ print "longliving" dat <- readEVar dataAvailable liftIO $ writeIORef rdata dat liftIO $ print $ show dat <> " arrived" #+END_SRC Que se passe-t-til ? On voit un formulaire de deux input. Lorsque l'on modifie un champs, un message se met à jour automatiquement et on voit que le formulaire est envoyé au serveur lorsque l'on regarde les logs. On remarque que tous les messages se synchronisent sur tous les navigateurs! *** Les widgets - formWidget (le formulaire) - longLivingProcess (le process sur le serveur) - syncWidget (le message au dessus du formulaire) Lorsque l'on utilise `(<|>)` celà signifie que l'on va lancer le code sur toutes les instances de l'application (toutes les instances frontend et backend lancées). Mais on peut lancer des commandes seulement sur le frontend, d'autres seulement en backend. **** Init - on initialise un IORef qui joue le rôle d'une DB. - Dans une vrai application il faudrait remplacer IORef par un système persistant - On initialise aussi un EVar (Event Var) donc on va envoyer des events. **** formWidget (le formulaire) #+BEGIN_SRC haskell formWidget :: IORef MyForm -> EVar MyForm -> Cloud () formWidget rdata dataAvailable = onBrowser $ do local . render . rawHtml $ do h1 "State" div ! id (fromString "msg") $ "No message yet." st <- atRemote . localIO $ readIORef rdata -- read the state data myForm <- local . render $ MyForm <$> getString (Just (i1 st)) `fire` OnKeyUp <*> getString (Just (i2 st)) `fire` OnKeyUp -- notify the long living process atRemote $ localIO $ writeEVar dataAvailable myForm #+END_SRC - le `onBrowser` au début montre bien que on ne s'exécute que sur le browser. - le `local` signifie que l'on fait quelque chose en local sur le front - on a un `atRemote` qui signifie que cette opération doit avoir lieu sur une instance `remote` (le serveur) - en remote on récupère simplement la valeur dans le IORef et on la récupère coté front (black magic!) - les `fire` OnKeyUp signifie que la continuation (ce qu'il y a après la ligne courante) et relancée à chaque event keyUp. **** longLivingProcess (le process sur le serveur) #+BEGIN_SRC haskell longLivingProcess :: IORef MyForm -> EVar MyForm -> Cloud () longLivingProcess rdata dataAvailable = onServer $ local $ do liftIO $ print "longliving" dat <- readEVar dataAvailable liftIO $ writeIORef rdata dat liftIO $ print $ show dat <> " arrived" #+END_SRC - le `onServer` signifie bien que l'on est sur le serveur - est le code est très simple, lorsqu'il reçoit un évênement via l'EVar, il le sauvegarde dans l'IORef et il log la valeur reçue **** syncWidget (le message sous le formulaire) #+BEGIN_SRC haskell syncWidget :: EVar MyForm -> Cloud () syncWidget dataAvailable = onBrowser $ do -- display in the browser received <- atRemote $ local $ readEVar dataAvailable local . render . at (fromString "#msg") Insert . rawHtml $ p $ "Check other navigators: " <> i1 received <> ", " <> i2 received #+END_SRC - le onBrowser est là pour signifier que l'on se situe sur le browser - on "écoute" l'EVar à partir du serveur - dans le render on écrase (via un insert JS) par du HTML maison. L'idée du on écoute, c'est simplement que chaque ligne du `do` sauvegarde des continuations qui se relancent lorsque l'on fait un readEVar par exemple. *** Retour Donc après avoir joué avec cette première étape et essayer de comprendre comment utiliser la librairie. Je dois dire que je suis plutôt impressioné. J'avais peur de tomber sur du code "magique". Mais ça n'est pas tant le cas. Ce qui a l'air magique : - décrire au même endroit comment doit se comporter le front et le back et comment ils interagissent. - Communication entre process qu'ils soient dans un navigateur web ou pas La monade transient est certes complexe, mais la première étape est de comprendre que la monade capture les continuations. En ce qui concerne l'utilsation avancé du typage. Je ne pense pas qu'il en fasse grand usage. Il utilise juste ce qui est nécessaire pour ne pas faire bugger sa lib. Par contre il n'est jamais demandé à l'utiliser de créer des types. De mon point de vue, sa monade reste très (trop?) ouverte. J'ai aussi constaté des bugs assez incompréhensibles, suffisamment pour avoir un peu peur d'utiliser ça en prod. À mon avis nous avons deux choix pour avancer: - soit modifier sa lib pour utiliser =Transient m= plutôt que =TransIO = Transient IO= directement. - Soit nous créer une Free Monade qui copie l'API de transient pour nos besoins et dont l'un des interpréteur appelera transient. On peut aussi imaginer un quelque chose entre les deux. Pour ma part, je vais discuter un peu avec Corona en ce qui concerne les bugs, le fait qu'il travaille sous windows ne doit pas aider je présume. * 2016-09-05 - Transient, MFlow - Kmett: Monad Homomorphisms * 2016-07-29 ** Attribute Grammar - https://wiki.haskell.org/The_Monad.Reader/Issue4/Why_Attribute_Grammars_Matter - AST → AST - Repmin: un seul parcours d'arbre - Transducer de DAG - Patrick Bahr - Composing & Decomposing Data Types (Patrick Bahr) ** x TODO - pl-cli demo (avec la liste des constructors) - pl-server * 2016-07-11 ** List value constructors from a types ~~~~ ghci> :m +Data.Data ghci> let x = toConstr (Just False) ghci> constrType x DataType {tycon = "Prelude.Maybe", datarep = AlgRep [Nothing,Just]} ~~~~ ~~~ Prelude Data.Data> :set -XDeriveDataTypeable Prelude Data.Data> data Cmd = X | Foo Int | Bar Bool deriving (Data) Prelude Data.Data> let y = toConstr X Prelude Data.Data> constrType y DataType {tycon = "Cmd", datarep = AlgRep [X,Foo,Bar]} ~~~ import ... (Cmd(..)) - Cmd ???? - Cmd => [X,Foo,Bar] ???? - Foo => Foo :: Int -> Cmd ????