Revenir à l'accueil

Une application web moderne en Elm

Publié le 25 du 10 2017

Note du 5 Décembre 2017

Attention, bien que le laïus sur la morphologie des messages et des modèles et toujours d’actualité. La proposition des Patch des messages Discrets a été, à juste titre, remise en question (car cela impose un style impératif). Je laisse tout de même l’article parce que je pense que sa première partie et celle sur le routing sont pertinentes et que j’assume mes erreurs… aha.

Reprise de l’article

Depuis 2003, comme nous l’indique cet article, le JavaScript est utilisé comme un bytecode, beaucoup de compilateurs/transpilateurs ciblent le JavaScript pour permettre à des développeuses et développeurs d’écrire des applications web clientes dans leur langage favori.

Les applications devenant de plus en plus complexes, principalement grâce à l’évolution des navigateurs web (qui est une conséquence de l’amélioration de notre matériel), beaucoup de développeuses et développeurs ont fait le choix d’utiliser des langages statiquement typés pour faciliter le développement. On peut citer, entre autres, TypeScript, PureScript, ReasonML / OCaml (avec BuckleScript ou Js_of_ocaml) et Elm. Tous ces outils proposant chacun des avantages (et aussi des inconvénients).

Au contraire de beaucoup d’autres technologies, Elm embarque une architecture qui fait office de framework pour construire des applications web. Même si dans un premier temps, chaque exercice proposé par le site est très clair, lorsque l’on doit écrire une application “riche” qui propose plusieurs formes d’interactivité, il faut parfois ruser, et remettre en question des pratiques que l’on estimait bonne.

Dans cet article, je vous propose, via des cas concrets, de mettre en place une nouvelle architecture au sein de la Elm-architecture pour permettre, de facilement faire croître une application Elm.

Cet article n’est pas un tutoriel d’apprentissage au langage Elm, une connaissance sommaire du langage est requise pour comprendre le code présenté. De plus, les morceaux de code présentés n’utilisent pas Elm-format, non pas pour être rebelle, mais ça aurait pu rendre l’article plus difficilement lisible.

Elm est un langage qui s’inscrit dans la famille des langages ML, et sa syntaxe s’inspire fortement de Haskell. On écrit une application “entière” en Elm et ensuite, elle est transpilée en JavaScript. En plus d’offrir une syntaxe confortable, Elm offre un système de types puissant et expressif (par rapport à JavaScript), qui évite au maxium les erreurs au Runtime. De plus, le compilateur de Elm est très bavard et éloquent sur les manières de résoudre une erreur à la compilation.

En plus d’être un très beau langage (argument subjectif, je l’accorde), Elm impose une manière de structurer son application web que l’on appelle la Elm-architecture. Cette architecture permet d’abstraire les concepts complexes qui sont liés, entre autre, à la programmation fonctionnelle réactive (comme, par exemple, les flèches ou les signaux). De ce fait, Elm est un langage facile à appréhender, ce qui explique sans doute son succès grandissant dans le monde des applications front-end. De ce fait, l’auteur lui même dit que Elm n’est pas un langage fonctionnel reactif, cependant, il tient beaucoup des promesses liés à cette famille de langage.

Utilisation de la Elm-architecture

La Elm-architecture repose sur quelques pilliers :

  1. On représente l’état de l’application via un type Model ;
  2. la représentation de la page est une fonction qui prend en paramètre un Model ;
  3. dans la fonction de vue, on peut envoyer des messages (de type Message) à une fonction de rafraîchissement ;
  4. la fonction de rafraîchissement prend en argument un Message et un Model et renvoie une nouvelle version du Model ;
  5. une application Elm est appelée un programme et initialise le Model et définit la fonction de vue et celle de rafraîchissement.

Dans ce premier exemple, nous allons simplement créer deux boutons et un texte qui affichera une valeur entière. Quand on appuyera sur le premier bouton, on incrémentera la valeur du texte, quand on appuyera sur le second, on décrémentera la valeur du texte.

(Vous pouvez retrouver une version exécutable de ce code ici.)

Dans cet exemple, notre modèle sera un nombre entier, celui qui sera affiché dans notre espace de texte. Et nous aurons deux messages possibles :

  1. un message pour incrémenter le modèle ;
  2. un message pour décrémenter le modèle.

Comme notre modèle est assez simple, la fonction de rafraîchissement est assez facile à implémenter.

type Msg 
 = Increment 
 | Decrement

update msg model =
  case msg of
    Increment -> model + 1
    Decrement -> model - 1

Rien de très complexe, on se sert d’unions discriminées pour définir les différents messages recevables (parce que c’est commun) et la fonction de rafraîchissement se contente, en cas de réception du message Increment, on renvoie le modèle auquel on ajoute 1, en cas de réception du message Decrement, on renvoie le modèle auquel on retire 1.

Maintenant que nous avons notre logique de traitement des messages, nous pouvons implémenter notre vue. En Elm, le HTML s’écrit au moyen de fonctions :

view model =
  div []
    [ button [ onClick Decrement ] [ text "-" ]
    , div [] [ text (toString model) ]
    , button [ onClick Increment ] [ text "+" ]
    ]

L’attribut onClick, qui est aussi une fonction, enverra un message à la fonction de rafraîchissement. Maintenant, il ne reste plus qu’a créer notre fonction main qui instanciera un programme :

main =
  beginnerProgram { 
    model = 0
  , view = view
  , update = update 
  }

Et voila, je vous invite à tester le code en ligne, au delà de quelques importations complémentaires, il n y a rien de plus que ce que j’ai montré.

La génération de la vue est largement optimisée grâce à l’utilisation d’un DOM virtuel, qui va minimiser les transformations nécéssaire pour rendre la vue.

La Elm-architecture propose plusieurs types de programmes pour démarrer une application dans le contexte idéal. Elle introduit aussi une notion de signaux discrets qui sont appelés des souscriptions. En résumé, elle fit très bien avec les attentes fonctionnelles d’une application web moderne.

En résumé

La Elm-architecture est vraiment agréable à utiliser, elle n’est pas trop rigide et permet donc d’organiser son code (via des modules) de manière assez libre. Cependant, l’écriture d’une application complète se réveler complexe pour plusieurs raisons. En effet, même si l’architecture décrit un flot assez évident (et donc, compréhensible), la difficulté peut résider dans la morphologie des messages et des modèles.

Voyons, avant de nous ateler à la construction, ce que l’on pourrait attendre d’une application web moderne :

Le routing introduit une notion d’état bookmarkable, l’interactivité ponctuelle amène des éléments d’ergonomie qui améliorent la navigation et les messages discrets permettent de s’approcher des applications temps réel.

Forme des messages et des modèles

Il est important de préciser que cet article est avant tout un retour d’expérience; il n’a pas du tout pour vocation à prétendre à une quelconque révolution. Lorsque j’ai été amené à m’intéresser à Elm, les exemples “classiques” ont été assez faciles à appréhender. Cependant, quand j’ai été amené à structurer une application plus ambitieuse, j’ai dû expérimenter plusieurs voies. Je ne prétend pas que c’est la seule (ni même la meilleure) et je serai ravi de lire vos critiques en commentaire ou par courrier éléctronique !

Avec l’architecture proposée, on est souvent tenté de faire ce qui, à mon sens, est une erreur monumentale si l’on veut que notre application puisse croître en fonctionnalités. Avant d’entamer les retours concrets sur l’implémentation d’une application, j’aimerais mettre en lumière un point de vue que j’ai fait mûrir en développant des applications de plus en plus ambitieuses avec Elm.

  1. Le modèle ne doit pas servir que à décrire le modèle de données (dans le sens qu’on lui donne dans le back-end), il décrit l’état courant de l’application. De ce fait, dans une application complexe, on devrait souvent privilégier l’usage d’unions discriminées paramétrés qui n’exposent que les informations nécéssaires pour naviguer dans l’application plutôt que de tenter d’être exhaustif en exposant un enregistrement aplatit.

  2. les messages n’ont pas tous le même niveau sémantique dans une application, ils peuvent donc être regroupés et ordonnés au moyen, une fois de plus, d’unions discriminées.

Rappelons le type de la fonction update qui va ponctuellement générer des fragments de la vue :

update : Message -> Model -> Model

Cette signature indique que pour un message et un modèle, on produit le modèle à l’état suivant. Donc, le message reçu collabore avec le modèle courant pour produire le modèle suivant.

Cette petite mise en garde peut sembler, à bien des égards, très naïve. Cependant, je sais que pour ma part, ma première idée, quand j’ai été amené à développer mes premières applications Elm, a été de vouloir, instinctivement, considérer mon modèle comme un modèle de données (et donc tâcher, au mieux, de le rendre exhaustif) et mes messages comme des actions atomiques ayant toutes le même niveau sémantique. Cette manière de procéder a eu comme conséquence que mon modèle était toujours invariablement trop peuplé, la séparation de mes vues en fonctions plus complexe, et ma fonction de rafraîchissement beaucoup trop longue et difficilement fragmentable.

Construire une application

Comme Elm ne propose pas d’outils de construction générique (comme les foncteurs applicatifs de OCaml par exemple), l’architecture que je propose n’est pas figé et devra s’adapter aux besoins de l’application. Cette partie de l’article est donc à prendre comme une méthodologie et non comme une architecture figée, au contraire de la Elm-architecture.

Je ne détaillerai pas tous les points techniques liés à Elm, par soucis de concision, cependant, le code utilisé pour expérimenter cette infrastructure est accessible sur ce dépôt (qui fait office d’expérimentation, le code n’est pas un exemple sur beaucoup d’aspects… désolé), il s’agit d’un projet Elixir / Phoenix. Cependant la partie Elixir n’est pas très intéressante (et n’a été mise en place que pour tester les Websockets).

Attention, je n’ai pas du tout la prétention d’innover; cependant, les exemples liés à l’utilisation de Elm-architecture couvrent généralement la partie SPA et j’ai la conviction que le mélange des actions ponctuelle avec celle du routing client peuvent parfois entraîner des difficultés à modeliser les types de messages et de modèles à utiliser.

Principe général de l’application

L’application est vraiment cheap (et laide), cependant, elle survole plusieurs cas d’usages relatifs aux ingrédients que nous avions établi comme étant les pré-requis à l’implémentation d’une application web moderne, on retrouve donc plusieurs pages qui utilisent différents concepts relatifs à ces ingrédients :

Bibliothèques utilisées

Voici un rapide récapitulatif des outils utilisés dans l’implémentation de cette expérience :

Comme cet article à été écrit à l’aube de la sortie de Elm 0.19, il est possible que dans un futur proche, certaines des propositions présentées dans l’article deviennent obsolètes !

Implémentation des états bookmarkables

Dans un premier temps, nous n’allons nous occuper que des états bookmarkables, il s’agit de page normales accessible via un point d’entrée, en l’occurence, l’URL.

Premièrement, nous allons définir qu’un modèle est composé de valeurs constantes, celles dont nous aurons besoin sur toutes les pages et de valeurs variables, soit la description de la page sur laquelle nous nous trouvons. Concrètement, l’état courant.

type alias Model =
    { state : State
    , messages : List String
    , total : Int
    }

messages correspondra à la liste de messages que l’on recevra (plus tard) et total au nombre de messages postés que l’on n’aurait pas vu (provenant d’autres clients). C’est dans state que l’on stockera la page courante.

Un état peut être deux choses :

  1. une page accessible ;
  2. une erreur, si par exemple la page n’existe pas.
type State
    = Routed Page.Page -- Si une page existe
    | Error Int String -- Si on doit remonter une erreur

Le type Page.Page est une union disciminée qui énumère toutes les pages statiques de l’application web :

type Page
    = Home
    | About Bool
    | Post { input : String }

L’avantage d’utiliser une union disciminée est qu’il ne faut pas normaliser le modèle de données d’une page. De ce fait, chaque page peut avoir son modèle spécialisé, ne contenant que les informations nécéssaires à son affichage. Les données communes à toutes les pages seront, elles, stockées dans le modèle, au même niveau que l’état courant.

Avec cette approche on peut déjà implémenter un mécanisme de vue via des fonctions :

-- Vue globale de l'application
global : Model -> Html Message
global model =
    let 
       content = case model.state of 
           Error code message -> [ error code message ]
           Routed content -> page model content
    in
      div [ Attributes.class "content" ]
          [ h1 []
              [ text "My page" ]
          , nav [] [{- Ici on mettra le menu -}]
          , div [] content
          ]

-- Vue d'une erreur
error : Int -> String -> Html message
error code message =
    div [ Attributes.class "error" ]
        [ h2 [] [ text (toString code) ]
        , text message
        ]

Et on peut implémenter la fonction page qui affichera, au cas par cas, les informations nécéssaires à l’affichage de la page :

page : Model -> Page -> List (Html Message)
page model page =
  case page of 
    Home -> 
       {- Code ou fonction pour afficher la page Home -}
    About toggle -> 
       {- Pareil pour About -}
    {- etc ... -}

Le module qui s’occupe de rendre le HTML peut se contenter de n’exposer que la fonction global car c’est au final la seule qui, en-dehors du module, sera réellement utile.

Implémentation du router

Maintenant que nous avons des éléments pour construire des pages ayant chacun leur modèle spécifique, nous allons pouvoir implémenter le routing à proprement parlé.

Pour cela, nous allons, un peu à la manière de Page, implémenter une union disciminée pour définir les routes. Comme une URL peut ne pas aboutir à une route existante, la notion de route potentiellement aboutissable peut être modelisée par un Maybe Route. L’implémentation du module de routing est une tâche assez récurrente, et ne fais rien de plus qu’exploiter les modules elm-lang/navigation et evancz/url-parser. On ajoute des fonctions utilitaires pour transformer une Navigation.Location en Route et de quoi générer rapidement les attributs HTML href pour pointer vers une route :

On défini d’abord les différentes routes possibles et ensuite le parseur qui servira à transformer une Navigation.Location en Route :

type Route
    = Home
    | About
    | Post


routeParser : Parser (Route -> a) a
routeParser =
    Url.oneOf
        [ map Home (s "")
        , map About (s "about")
        , map Post (s "publish-message")
        ]

Ensuite, on peut créer une fonction pour transformer une Route en chaîne de caractères qui sera utilisé dans la fonction pour générer l’attribut href. Si j’utilise String.join c’est pour anticiper le moment où j’aurai des URLs avec plusieurs membres (séparés par des /) :

toString : Route -> String
toString route =
    let
        fragment =
            case route of
                Home -> [ "" ]
                About -> [ "about" ]
                Post -> [ "publish-message" ]
    in  
      "#/" ++ (String.join "/" fragment)

href : Route -> Attribute messsage
href route =
    Attributes.href (toString route)

On peut ensuite implémenter la fonction qui tâchera de produire notre route sur base de l’objet Navigation.Location :

fromLocation : Location -> Maybe Route
fromLocation location =
    if String.isEmpty (location.hash) then
        Just Home
    else
        parseHash routeParser location

Même si le code est un peu récurrent, on défini les routes accessibles au moyen d’un Parseur dont on se servira pour parser la location courante.

En utilisant un programme issu du module Navigation on pourra créer un écouteur qui a chaque changement dans l’URL, enverra un message contenant la route potentielle (via la fonction fromLocation). On peut donc créer un premier message :

type Message
    = Routing (Maybe Route) 

Maintenant que le changement d’URL broadcast un message, il faut le traiter dans la fonction de rafraîchissement.

J’ai décidé de créer une fonction dont le type est Model -> Maybe Route -> Model qui se chargera de construire le modèle adéquat pour une route donnée. De cette manière, je peux réutiliser cette fonction dans la phase d’initialisation de l’application :

doRouting : Model -> Maybe Route -> Model
doRouting model potentialRoute =
    case potentialRoute of
        Nothing ->
            { model | state = 
                Error 404 "The route does not exists" 
            }
        Just route ->
            case route of
                Home ->  
                  { model | state = Routed Page.Home }
                About -> 
                  { model | state = Routed (Page.About False) }
                Post ->  
                  { model 
                     | total = 0
                     , state = 
                          Routed (Page.Post { input = "" }) 
                  }

Globalement, je me contente de lancer une erreur si jamais la route n’existe pas et je me contente chaque fois de modifier le membre state de mon modèle (sauf dans le cas de Post, mais nous reviendrons sur ce point plus tard).

Je peux maintenant modifier le membre init de mon programme pour qu’il charge, au démarrage de l’application, la route adéquate, et donc qu’il génère le modèle que l’on attend :

init : Navigation.Location -> ( Model, Cmd Message )
init location =
    let
        route = fromLocation location
        model = { 
           messages = []
         , state = Routed Page.Home
         , total = 0 
        }
    in ( doRouting model route, Cmd.none )

Ensuite, il suffit de s’occuper de la fontion de rafraîchissement, dont le travail sera simplement d’utiliser la fonction doRouting, comme dans la fonction init :

update : Message -> Model -> ( Model, Cmd Message )
update message model =
    case message of
        Routing potentialRoute ->
            (doRouting model potentialRoute, Cmd.none)

Utilisation du router

Nous avons maintenants des états bookmarkables, même si on pourra se plaindre de la redondance des définitions (notamment entre Page et Route) et du fait qu’il faille modifier la fonction doRouting et les vues pour ajouter des nouvelles pages, le compilateur nous aide en nous soulignant les cas de correspondance de motifs non-exhaustive (c’est en partie pour ça qu’il vaut mieux, dans la mesure du possible, éviter les captures générales via _ ->. Cependant, parfois, il est compliqué de s’en passer.

Nous pouvons rapidement ajouter un menu dans notre vue globale, c’est très facile à mettre en place grâce à la fonction Router.href :

global : Model -> Html Message
global model =
    let 
       content = case model.state of 
           Error code message -> [ error code message ]
           Routed content -> page model content
    in
      div [ Attributes.class "content" ]
          [ h1 []
              [ text "My Page" ]
          , nav []
              [ a [ Router.href Router.Home ] [ text "Home" ]
              , a [ Router.href Router.About ] [ text "About" ]
              , a [ Router.href Router.Post ]
                  [ text 
                     -- Ici on affiche le nombre 
                     -- de messages reçus depuis
                     -- la dernière visite
                     ( "Post a  message (" 
                       ++ (toString model.total) 
                       ++ ")") 
                   ]
              ]
          , div [] (fragment model)
          ]

Ce que l’on retiendra principalement de l’implémentation des états bookmarkables pour le développement de notre application est :

Implémentation des actions ponctuelles

En plus de pouvoir “formellement” changer d’URL, on voudrait pouvoir effectuer des actions ponctuelles, qui elles, seraient des états non-bookmarkables. Cela permettrait, entre autre, de faire des modifications relatives à une page que l’on est en train de visiter, par exemple “ouvrir” ou “fermer” une <div> ou modifier l’état général (le modèle) de l’application.

Prenons par exemple la page About qui est paramétrée par un booléen, imaginons que nous voudrions qu’en fonction de la valeur de ce paramètre, un fragment de HTML soit affiché ou non à l’écran.

Voyons une fonction que nous aurions pu appeler dans notre fonction pour rendre les vues des pages :

about : Bool -> List (Html Message)
about toggle =
    let
        toggler =
            if toggle then "opened"
            else "closed"
    in
        [ button [] [ text "Toggle content" ] -- THE button ;)
        , div [ Attributes.class "page" ]
            [ h2 [] [ text "About" ]
            , text "This is an "
            , span [ Attributes.class toggler ] [ text "ugly" ]
            , text " experience !"
            ]
        ]

Actuellement, grâce à l’organisation de notre code, l’utilisation du booléen est déjà prise en charge, cependant, il faut implémenter un message pour demander explicitement de changer la valeur du booléen.

Les patches

Concrètement, ce que l’on veut faire ici, c’est simplement modifier un modèle, un patch n’est donc rien de plus qu’une fonction qui prend en argument un modèle et renvoie la version modifiée de ce modèle. On peut donc étendre nos messages pour prendre en charge les patches :

type Message
    = Routing (Maybe Route)
    | Patch (Model -> ( Model, Cmd Message ))

L’ajout d’un nouveau message implique la transformation de la fonction de rafraîchissement. Lorsque l’on reçoit un message Patch f, il suffit d’appliquer la fonction au modèle courant (car on délègue au patch la responsabilité de savoir s’il peut s’appliquer au modèle en cours ou non) :

update : Message -> Model -> ( Model, Cmd Message )
update message model =
    case message of
        Routing potentialRoute ->
            (doRouting model potentialRoute, Cmd.none)

        Patch apply_patch ->
            apply_patch model

Pour reprendre notre exemple précédent, voici une implémentation possible pour changer le modèle de notre page About :

toggleAbout : Model -> ( Model, Cmd message )
toggleAbout model =
   let 
       state = case model.state of 
          Routed (Page.About t) -> Routed (Page.About (not t))
          _ -> Error 401 "Unauthorized case"
    in ({ model | state = state}, Cmd.none)

Une fois que la fonction de patch est créée, on peut s’en servir assez facilement dans la vue :

button [onClick (Patch toggleAbout)] [ text "Toggle content" ]

Si je ne wrappe pas directement le résultat de ma fonction dans le constructeur Patch, c’est en partie pour pouvoir m’en reservir dans d’autres contextes que les patches. Cependant, je n’ai pas réellement d’avis sur ce qu’il serait mieux de faire, donc je tâche de prôner la réutilisabilité.

Le seul problème “cosmétique” à cette méthode est qu’elle est oblige à traiter un cas trivial et donc, par soucis de confort, à faire une clause universelle, donc, potentiellement, occulter certaines erreurs. Même si cette approche ne me satisfait pas totalement, je pense qu’en considérant qu’un patch ne concerne à priori qu’une seule page, ce n’est pas dramatique.

En opposition, le fait de pouvoir traiter “plusieurs cas” de modèle dans un patch peut aussi être intéressant dans certains cas de figures.

Actuellement, je pense que le fait que Elm occulte certains aspects liés à l’algèbre des types implique qu’il n’existe (pour peu que l’on respecte ce type d’architecture) pas de solution satisfaisante. Cependant, si vous avez des idées ou pistes, n’hésitez pas à laisser un commentaire ou à m’écrire un courrier électronique !

Des patches plus complexes

On se rend vite compte que les patches fonctionnent très bien avec des émetteurs d’événements comme onClick, mais pourrait-on les utiliser avec, par exemple, l’émetteur onInput (préconisé pour le traitement de données liés à des champs de textes) ? Observons le modèle de la page Post :

type Page
    = Home
    | About Bool
    | Post { input : String }

Le type de onInput est : (String -> msg) -> Attribute msg, pour cela, nous allons passer à la fonction onInput une fonction dont l’argument unique sera la chaine de caractères demandée par l’événement, dans notre fonction anonyme, nous pourrons passer cette chaine à notre patch :

input
   [ Attributes.placeholder "A message"
   , Attributes.value state.input
   , onInput (\s -> Patch (recordInput s))
   ] []

Voyons, par exemple, comment sauvegarder dans le modèle de la page Post le contenu du champ de texte :

recordInput : String -> Model -> ( Model, Cmd message )
recordInput text model  =
   let 
       state = case model.state of 
          Routed (Page.Step state) ->
              { model
                | state = 
                   Routed (Page.Step {state | input = text})    
              }
          _ -> Error 401 "Unauthorized case"
    in ({ model | state = state}, Cmd.none)

Même si l’on peut se plaindre d’une certaine redondance et, peut être, d’un excès de verbosité, les patches offrent une manière commode de gérer les mutations de modèles et si on les avait remplacés par une succession de messages, il aurait tout de même fallu gérer les modèles “pertinents” au cas par cas, de ce fait, ils me semblent tout indiquer pour mettre en place facilement une notion d’action ponctuelle qui n’implique pas la modification de la fonction de rafraîchissement.

De plus, ils n’empiètent pas sur le router, ils semblent donc convenir relativement bien pour des applications hybrides, qui imposent l’usage d’un router et d’actions ponctuelles.

Les messages discrets

Le mécanisme de Souscriptions de Elm est largement suffisant pour implémenter les messages discrets, de plus, ils se marient très bien avec les channels de Elixir. De plus, comme généralement, le nombre de messages discrets étant pris en charge par une application de taille normale est assez limité (au contraire des actions ponctuelle), il n’y a, à mon sens, rien de mal à les traiter au cas par cas. Cependant, si nous voulions traiter ces messages discrets de la même manière que nous traitons les patches, je peux proposer cette extension au type Message :

type Message
    = Routing (Maybe Route)
    | Patch (Model -> ( Model, Cmd Message ))
    | Discrete (Chan -> Model -> ( Model, Cmd Message )) Chan

type Chan
    = Anonymous { body : String } -- I'm a stereotype !
    | Indentified { body: String, name: String }

(Dans cet exemple, j’utilise un port qui communique avec les channels de Phoenix).

Comme pour les routes, je sépare mes différents types de messages discrets au moyen d’une union discriminée, ce qui me permettra de traiter au cas par cas, dans mes fonctions équivalentes à des patches, les données que je reçois.

A la différence d’un patche normal, qui se contente, sur base d’un modèle, d’en produire un nouveau, cette fois, les messages discrets agissent comme la fonction de rafraîchissement de la Elm-architecture, ils joignent un message (de type Chan) et un modèle pour construire un nouvau modèle :

update : Message -> Model -> ( Model, Cmd Message )
update message model =
    case message of
        Routing potentialRoute ->
            (doRouting model potentialRoute, Cmd.none)

        Patch apply_patch ->
            apply_patch model

        Discrete apply_patch pub ->
            apply_patch pub model

Implémentons maintenant un patch discret. Sans rentrer dans les détails (inintéressants) de la partie serveur, l’idée est que quand un client arrive sur l’application, il joint un channel quelconque et sur la page Post, il a la possiblité d’envoyer un message anonyme qui sera broadcasté à toutes les personnes qui se trouvent sur la page au même moment.

Dans le modèle, on garde la liste des messages (des chaines de caractères) qui ont été publiées durant la session de navigation de l’utilisateur et on compte le nombre de message qu’il reçoit pendant qu’il n’est pas sur la page Post. Donc, à chaque fois qu’il se rend sur la page Post, ce compteur de message est remis à zéro :

handleMessage : Chan -> Model -> ( Model, Cmd message )
handleMessage channel model =
    let
        offset =
            case model.state of
                Routed (Page.Post _) -> 0
                _ -> 1
    in
        case channel of
            Anonymous message ->
                ( { model
                    | messages = message.body :: model.messages
                    , total = model.total + offset
                  } , Cmd.none
                )
            _ -> (model, Cmd.none)       

On peut maintenant ajouter ça aux subscriptions de l’application. On convertit les résultats que l’on obtient (potentiellement) en continu via des ports en messages discrets :

subscriptions : Model -> Sub Message
subscriptions model =
  Sub.batch [ 
     Ports.subAnonymMessage
       (\s -> Discrete handleMessage (Anonymous s)) 
  ]

Par soucis d’extensibilité, j’utilise Sub.batch dans le cas où je devrais manipuler plusieurs signaux discrets différents.

Comme l’entièreté de la logique des patches discrets repose sur les souscriptions, il ne faut que rajouter des producers et des traitement dans le batch pour être capable de manipuler n’importe quel type de signaux (Workers, Websockets ou encore des BroadcastChannels).

Conclusion

Dans cet article, nous avons couvert certains éléments d’interactions utiles pour la conception d’une application web moderne. Comme je l’ai répété à de multiple reprises tout au long de l’article, je ne propose pas une architecture inclue dans la Elm-architecture et le code présenté ici n’est, hélas, pas générique.

Le manque d’expressivité du système de types et des modules1 prive le code général d’une certaine forme de réutilisabilité. Même si je le regrette un peu, je dois avouer que c’est loin d’être dramatique et que le langage et son framework me sont très agréables !

En organisant son code entre des modules (qui unissent des fonctionnalités sémantiquement proches), et en n’hésitant pas à ne pas aplatir les modèles et les messages on peut facilement approcher l’idée derrière les composants que l’on nous vend à toutes les sauces et le passage par la fonction de rafraîchissement permet d’unifier l’ensemble des composants (qui sont en fait la conjonction d’un type, et d’une interface).

Quoi qu’il en soit, j’attend avec impatience la sortie de Elm 0.19 et même si, à mon sens, il serait sûrement possible de faire quelques améliorations au niveau du système de types et de modules (mais à quel prix ?), Elm reste très utilisable en production et très agréable à utiliser. Comme l’ensemble des concepts qu’il manipule sont exposé dans la Elm-architecture, sa learn-curve est très souple ce qui fait de Elm un langage accessible, y comprit pour les gens n’ayant pas un très gros bagage en programmation fonctionnelle statiquement typée.

Le compilateur est une véritable aide pour les phase de refactoring et pour tâcher de traiter un maximum de cas dans toutes les membranes du programme.

Même si actuellement, je ne suis pas satisfait à 100% des premières conclusions que j’amène pour construire une application web hybride en Elm, l’écriture de cet article m’a réellement donné envie de me plonger plus en profondeur dans le langage pour, pourquoi pas, aboutir sur un autre article plus technique.

J’espère que cet article aura tout de même pu mettre en lumière certaines idées qui me semblent pertinentes. Et je résumerai l’ensemble de cet article par quelques points :

Pour finir, Elm c’est vraiment cool et facile à appréhender (pour preuve, la partie la plus dure a été, selon moi, la mise en place de Webpack) !


  1. Attention, ce n’est évidemment pas une critique, mais une constatation en comparaison avec des langages comme OCaml ou Haskell. En aucun cas je considère ce manque d’expressivité comme une faiblesse sur tous les autres aspects. Je suis cependant conscient que la tournure de phrase peut être prise comme une critique virulente… d’où cette note en bas de page …

Cet article est terminé !
Si jamais vous avez des remarques, n'hésitez pas à écrire un commentaire ou à contribuer