9.2 KiB
#+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
build-depends: base
, transient
, transient-universe
, transformers
, ghcjs-hplay
, containers
stack.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: []
stack-ghcjs.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
Start
#!/usr/bin/env bash
stack exec -- wse-exe -p start/localhost/3000
Test
./compile.sh
./start.sh
- 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
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"
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)
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
- 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)
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"
- 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)
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
- 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 queTransIO = 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 ????