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

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
  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

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 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

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 ????