URL: https://linuxfr.org/news/developpement-web-frontend-en-haskell-elm-et-purescript Title: Développement Web frontend en Haskell, Elm et Purescript Authors: nokomprendo Benoît Sibaud, Davy Defaud et palm123 Date: 2018年08月19日T02:40:35+02:00 License: CC By-SA Tags: haskell, elm, nix, purescript, miso, reflex et libreoffice Score: 46 Actuellement, le développement Web côté client (_frontend_) est très souvent réalisé en JavaScript ou dans des langages dérivés comme TypeScript. Il existe cependant d’autres outils intéressants, basés sur des langages de programmation fonctionnelle, qui permettent notamment d’éviter de nombreuses erreurs lors de l’exécution sur le navigateur. L’objectif de cette dépêche est de rappeler quelques généralités sur le développement Web _frontend_, et de présenter les outils [Elm](https://fr.wikipedia.org/wiki/Orme), [Purescript](http://www.purescript.org/), Miso et Reflex, à partir d’un exemple d’application (galerie d’images fournie via une API Web). *Attention : ceci n’est pas une étude rigoureuse et avancée mais juste un petit retour de petite expérience.* Voir également le [dépôt de code de l’exemple](https://framagit.org/nokomprendo/tuto_fonctionnel/tree/master/posts/tuto_fonctionnel_24/animals) et une [présentation en vidéo](https://youtu.be/RtxxVQuaFq8). ---- ---- # Généralités sur le web frontend ## Page web, application web, application native Historiquement, les pages web se contentaient d'afficher un contenu statique et de proposer des liens vers d'autres pages. Des éléments dynamiques sont ensuite progressivement apparus : animations, formulaires avec vérifications de saisies... si bien, qu'aujourd'hui, de nombreuses pages web sont de véritables interfaces utilisateurs, comparables aux logiciels classiques installés sur le système. On appelle ce genre de pages des *applications web* (exécutées par un navigateur web), à distinguer des *applications natives* (exécutées par le système d'exploitation). D'un point de vue utilisateur, les applications web et les applitions natives sont de plus en plus proches. Par exemple, on trouve des applition web de traitement de texte (frama-pad, google-docs) qui possèdent une bonne part des fonctionnalités de leurs équivalents natifs (libreoffice, msoffice). D'un point de vue développeur, les technologies utilisées sont historiquement très différentes. Les applications natives utilisent généralement des langages comme Java, C#, Swift et leurs frameworks associés. Les applications web utilisent les technologies issues du web HTML/CSS/JavaScript et dépendent quasi-systématiquement d'un accès réseau via des websockets, requêtes AJAX ou autres. Cependant, on note une convergence web/natif également à ce niveau, notamment avec l'apparition du framework Electron, qui permet d'utiliser des technologies web pour développer des applications natives. De même, des architectures logicielles comme le Modèle-Vue-Contrôleur, très courant en natif, a été repris dans de nombreux frameworks web. ## Les langages pour le web frontend Le langage des applis web est sans conteste le JavaScript. C'est un langage assez controversé mais qui a tout de même des avantages indéniables, surtout depuis les récentes normes (ES2015...) : flexibilité, expressivité, compilateurs Just-In-Time performants, intégration dans les navigateurs... Cependant, JavaScript permet facilement de faire des erreurs qui se produiront à l'exécution finale de l'application, les fameuses *runtime errors*. Pour éviter ces erreurs, on utilise souvent des outils comme des analyseurs statiques, debuggers ou tests unitaires. Une autre solution consiste à utiliser des frameworks (Angular, React...), des bibliothèques (Immutable, Redux...) voire des langages dérivés (TypeScript...) qui réduisent les sources d'erreurs possibles. ## Les langages fonctionnels pour le web frontend En fait, une bonne partie des solutions proposées pour rendre le développement en JavaScript plus robuste existe déjà naturellement dans des langages fonctionnels comme Haskell : immuabilité, fonctions pures, typage statique fort... Certains développeurs se sont donc naturellement inspirés des langages fonctionnels pour proposer des technos web frontend garantissant l'absence d'erreurs au runtime. L'idée de ces technos est de fournir un écosystème complet (langage fonctionnel, compilateur, bibliothèques...) adapté au web et produisant du code JavaScript exécutable par un navigateur. Parmi ces technos, on trouve Elm et Purescript, qui proposent des langages inspirés d'Haskell. Il est également possible d'utiliser directement le langage Haskell (éventuellement avec des bibliothèques comme Miso ou Reflex) et le compilateur Ghcjs pour produire du code JavaScript. Enfin, il existe des outils dans d'autres langages fonctionnels comme ClojureScript, Ocsigen (OCaml)... ## Les concepts de MVC, FRP, Virtual-DOM Ces trois concepts sont généralement au cœur des technos web frontend fonctionnelles. Ils sont également assez fréquents dans l'écosystème JavaScript classique. Le MVC (Model-View-Controler) est une architecture de code qui permet d'organiser une application en trois parties : le modèle (données « métier » à manipuler), la vue (affichage présenté à l'utilisateur) et le contrôleur (mise à jour de l'application en fonction des événements). Généralement, un MVC gére les événements de façon asynchrone et unidirectionnelle : les événements de la vue sont passés au contrôleur, qui modifie le modèle puis lance un rafraichissement de la vue... ![MVC](https://nokomprendo.frama.io/tuto_fonctionnel/posts/tuto_fonctionnel_24/images/mvc.jpg) [crédit : Ossi Hanhinen](https://www.futurice.com/blog/elm-in-the-real-world/) Le FRP (Functional Reactive Programming) est le principe de base des frameworks fonctionnels, sur lequel est implémenté le MVC. Le FRP permet d'implémenter le comportement dynamique des interfaces utilisateur. Il traite les flux d'événements au cours du temps et transmet ces flux entre les différents composants de l'application, le tout de façon fonctionnelle (sans effet de bord). ![Functional Reactive Programming](https://nokomprendo.frama.io/tuto_fonctionnel/posts/tuto_fonctionnel_24/images/frp.jpg) [crédit : André Staltz](https://gist.github.com/staltz/868e7e9bc2a7b8c1f754) Enfin, un concept essentiel aux applications web est le DOM virtuel. Le DOM (Document-Object-Model) décrit la structure d'une page web, donc de l'application, dans le navigateur. Au cours de son exécution, une appli web a besoin de manipuler le DOM, pour récupérer des données ou pour modifier l'affichage. Or, manipuler directement le DOM est coûteux et résulte en une application peu réactive. Pour améliorer les performances, les frameworks web utilisent un système de cache, le DOM virtuel, qui regroupe des modifications et ainsi minimise les accès au DOM. ![DOM et DOM virtuel](https://nokomprendo.frama.io/tuto_fonctionnel/posts/tuto_fonctionnel_24/images//vdom.jpg) [crédit : Naukri Engineering](https://medium.com/naukri-engineering/naukriengineering-virtual-dom-fa8019c626b) # Un exemple de base Pour illustrer les outils de web frontend fonctionnel, imaginons qu'on veuille implémenter une appli client-serveur de recherche et d'affichage d'images d'animaux. Le serveur fournit les images en HTTP à l'URL `/img/`. Il fournit également une API JSON à l'URL `/api/animals ` auquel il répond par la liste des animaux de sa base de données dont le type correspond au préfixe donné en paramètre. Chaque élément de la liste retournée contient le type de l'animal et le nom du fichier image (accessible via l'URL `/img`). Si aucun préfixe n'est donné, le serveur retourne la liste complète. L'appli web client à réaliser contient simplement une zone de texte permettant de saisir le préfixe. Il envoie des requêtes AJAX au serveur pour récupérer les animaux correspondant puis affiche les images correspondantes après les avoir également demandées au serveur. Ci-dessous une image du client implémenté en Purescript. L'ensemble du code est disponible sur ce [dépôt git](https://framagit.org/nokomprendo/tuto_fonctionnel/tree/master/posts/tuto_fonctionnel_24/animals). ![client en purescript](https://nokomprendo.frama.io/tuto_fonctionnel/posts/tuto_fonctionnel_24/images//tuto24-animals.jpg) # Elm ![logo ELM](https://nokomprendo.frama.io/tuto_fonctionnel/posts/tuto_fonctionnel_24/images//elm.jpg) [Elm](http://elm-lang.org/>) est un environnement complet de développement web frontend fonctionnel. Il fournit un langage fonctionnel (inspiré d'Haskell mais en beaucoup plus simple), un compilateur Elm vers JavaScript et des bibliothèques. Elm est basé sur un DOM virtuel performant et permet de développer des applis web selon une architecture MVC. Ci-dessous une implémentation en Elm de l'application d'exemple (note pour les développeurs Elm : désolé pour le non-respect des règles de formatage de code Elm mais j'ai subi des pressions de la part de la ligue de protection des molettes de souris). Ce code suit un schéma MVC et un style fonctionnel très classique. On définit un type `Animal`, avec ses fonctions de décodage de données JSON, ainsi que le modèle de l'application, c'est-à-dire simplement la liste des `Animal` à afficher. Au niveau du contrôleur, le type `Msg` définit les événements qui peuvent se produire. L'événement `MsgInput` modélise une action de l'utilisateur sur la zone de texte et l'événement `MsgAnimals` un message du serveur transmettant les `Animal` en réponse à une requête à l'API. Ces événements sont gérés dans la fonction `update` : `MsgInput` ne change pas le modèle mais lance une requête à l'API via la fonction `queryAnimals`, `MsgAnimals` met à jour le modèle avec les données `Animal` reçues. Enfin, la fonction `view` indique comment construire la vue de l'appli, à partir du modèle : un titre `h1`, une zone de texte `input` puis un `div` pour chaque `Animal` du modèle. ```haskell module Main exposing (..) import Html exposing (..) import Html.Attributes exposing (height, href, src, width) import Html.Events exposing (onClick, onInput) import Http import Json.Decode as JD main : Program Never Model Msg main = Html.program { init = init , view = view , update = update , subscriptions = subscriptions } -- Model type alias Animal = { animalType : String , animalImage : String } decodeAnimal : JD.Decoder Animal decodeAnimal = JD.map2 Animal (JD.field "animalType" JD.string) (JD.field "animalImage" JD.string) decodeAnimalList : JD.Decoder (List Animal) decodeAnimalList = JD.list decodeAnimal type alias Model = { modelAnimals : List Animal } init : ( Model, Cmd Msg ) init = ( Model [], queryAnimals "" ) -- Controler subscriptions : Model -> Sub Msg subscriptions _ = Sub.none type Msg = MsgInput String | MsgAnimals (Result Http.Error (List Animal)) update : Msg -> Model -> ( Model, Cmd Msg ) update msg model = case msg of MsgInput animalType -> ( model, queryAnimals animalType ) MsgAnimals (Ok Model animals) -> ( Model animals, Cmd.none ) MsgAnimals (Err _) -> ( Model [], Cmd.none ) queryAnimals : String -> Cmd Msg queryAnimals txt = let url = "http://localhost:3000/api/animals/" ++ txt in Http.send MsgAnimals (Http.get url decodeAnimalList) -- View view model = span [] [ h1 [] [ text "Animals (Elm)" ] , p [] [ input [ onInput MsgInput ] [] ] , span [] (List.map (\a -> div [] [ p [] [ text a.animalType ] , img [ src ("http://localhost:3000/img/" ++ a.animalImage) , height 240 , width 320 ] [] ] ) model.modelAnimals ) ] ``` Elm a l'avantage d'être particulièrement simple à utiliser. Les applis développées suivent toujours un MVC bien définis et les messages du compilateur sont particulièrement clairs. Le code JavaScript produit est léger et performant. Parmi les inconvénients, on peut noter que Elm est limité au schéma MVC qui, bien que très répandu, ne sera peut-être pas adapté à toutes les applications. # Purescript ![logo purescript](https://nokomprendo.frama.io/tuto_fonctionnel/posts/tuto_fonctionnel_24/images//purs.jpg) [Purescript](http://www.purescript.org/) est également un environnement complet (langage, compilateur, bibliothèques) mais plus général que Elm. En effet, son langage est plus proche d'Haskell et plus puissant (il supporte notamment les classes de type). De plus, Purescript propose différentes architecture de code, dont MVC. Enfin, Purescript peut être également utilisé pour le développement côté serveur. Ci-dessous une implémentation en Purescript de l’application d’exemple, utilisant la bibliothèque Halogen (FRP + DOM virtuel). Ce code est très proche du code Elm. Pour le modèle, on définit également un type `Animals`, avec les fonctions de décodage JSON, et un type `Model`. Pour le contrôleur, on définit un type `Query` qui permet de gérer la requête AJAX et sa réponse, via la fonction `eval`. Enfin, la vue suit le même schéma que l'implémentation en Elm. ```haskell module Main where import Control.Monad.Aff (Aff) import Control.Monad.Eff (Eff) import Data.Argonaut ((.?), class DecodeJson, decodeJson, Json) import Data.Either (Either(..)) import Data.Maybe (Maybe(..)) import Data.Traversable (traverse) import Halogen as H import Halogen.Aff as HA import Halogen.HTML as HH import Halogen.HTML.Events as HE import Halogen.HTML.Properties as HP import Halogen.VDom.Driver (runUI) import Network.HTTP.Affjax as AX import Prelude main :: Eff (HA.HalogenEffects (ajax :: AX.AJAX)) Unit main = HA.runHalogenAff do body <- HA.awaitBody io <- runUI ui unit body io.query $ H.action $ QueryAnimals "" ui :: forall eff. H.Component HH.HTML Query Unit Void (Aff (ajax :: AX.AJAX | eff)) ui = H.component { initialState: const initialState , render , eval , receiver: const Nothing } -- Model newtype Animal = Animal { animalType :: String , animalImage :: String } instance decodeJsonBlogPost :: DecodeJson Animal where decodeJson json = do obj <- decodeJson json animalType <- obj .? "animalType" animalImage <- obj .? "animalImage" pure $ Animal { animalType, animalImage } decodeAnimalArray :: Json -> Either String (Array Animal) decodeAnimalArray json = decodeJson json>>= traverse decodeJson type Model = { modelAnimals :: Array Animal } initialState :: Model initialState = { modelAnimals: [] } -- Controler data Query a = QueryAnimals String a eval :: forall eff. Query ~> H.ComponentDSL Model Query Void (Aff (ajax :: AX.AJAX | eff)) eval (QueryAnimals animal_type next) = do H.modify (_ { modelAnimals = [] }) response <- H.liftAff $ AX.get ("http://localhost:3000/api/animals/" animal_type) let animals = case decodeAnimalArray response.response of Left _ -> [] Right ra -> ra H.modify (_ { modelAnimals = animals }) pure next -- View render :: Model -> H.ComponentHTML Query render m = HH.span [] [ HH.h1 [] [ HH.text "Animals (Purescript)" ] , HH.p [] [ HH.input [ HE.onValueInput (HE.input QueryAnimals) ] ] , HH.span [] (map (\ (Animal {animalType, animalImage}) -> HH.div [] [ HH.p [] [ HH.text animalType ] , HH.img [ HP.src ("http://localhost:3000/img/" animalImage) , HP.width 320 , HP.height 240 ] ] ) m.modelAnimals) ] ``` Purescript a l'avantage d'être plus puissant et plus général que Elm. En contrepartie, il est moins simple à utiliser. Son environnement est également plus compliqué à utiliser : il faut gérer les dépendances Purescript avec `bower`, les dépendances nodejs avec `npm` et la compilation avec `pulp`. # Haskell/Miso ![Miso](https://nokomprendo.frama.io/tuto_fonctionnel/posts/tuto_fonctionnel_24/images/miso.jpg) [Miso](https://haskell-miso.org) est une bibliothèque Haskell pour développer des applis web frontend. Miso permet de coder une appli MVC + DOM virtuel et de la compiler en JavaScript grâce à Ghcjs. Ci-dessous une implémentation en Haskell + Miso de l’application d’exemple. Ce code est très similaire à l'implémentation en Elm. Miso annonce d'ailleurs explicitement s'inspirer de Elm. ```haskell {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE OverloadedStrings #-} import Data.Aeson (FromJSON, decodeStrict) import Data.Maybe (fromMaybe) import Data.Monoid (()) import Data.Text (Text) import GHC.Generics (Generic) import JavaScript.Web.XMLHttpRequest (Request(..), RequestData(..), Method(..), contents, xhrByteString) import Miso import Miso.String (MisoString, toMisoString, fromMisoString, pack) main :: IO () main = startApp App { model = Model [] , update = updateModel , view = viewModel , subs = [] , events = defaultEvents , initialAction = GetAnimals "" , mountPoint = Nothing } -- Model data Animal = Animal { animalType :: Text , animalImage :: Text } deriving (Eq, Generic, Show) instance FromJSON Animal data Model = Model { modelAnimals :: [Animal] } deriving (Eq, Show) -- Controler data Action = GetAnimals MisoString | SetAnimals [Animal] | NoOp deriving (Show, Eq) updateModel :: Action -> Model -> Effect Action Model updateModel (GetAnimals str) m = m <# (SetAnimals <$> queryAnimals str) updateModel (SetAnimals animals) m = noEff m { modelAnimals = animals } updateModel NoOp m = noEff m queryAnimals :: MisoString -> IO [Animal] queryAnimals str = do let uri = pack $ "http://localhost:3000/api/animals/" ++ fromMisoString str req = Request GET uri Nothing [] False NoData Just cont <- contents <$> xhrByteString req return $ fromMaybe [] $ decodeStrict cont -- View viewModel :: Model -> View Action viewModel (Model animals) = span_ [] [ h1_ [] [ text "Animals (Miso)" ] , p_ [] [ input_ [ onInput GetAnimals ] ] , span_ [] $ map fmtAnimal animals ] fmtAnimal :: Animal -> View Action fmtAnimal animal = div_ [] [ p_ [] [ text $ toMisoString $ animalType animal ] , img_ [ src_ $ toMisoString $ "http://localhost:3000/img/" animalImage animal , width_ "320" , height_ "240" ] ] ``` Pour un développeur Haskell, Miso est une bibliothèque intéressante, car elle permet d'implémenter des applis simples « à la Elm » tout en restant dans l'écosystème Haskell. En revanche, son environnement est moins mature : les outils Ghcjs + Miso + Nix ne sont pas complètement triviaux à mettre en place et les temps d'installation d'installation et de compilation plus longs. # Haskell/Reflex ![logo Reflex](https://nokomprendo.frama.io/tuto_fonctionnel/posts/tuto_fonctionnel_24/images//reflex.jpg) [Reflex](https://reflex-frp.org/) est une bibliothèque Haskell de FRP générique. Elle est complétée par des projets associés : reflex-dom (DOM virtuel), reflex-platform (système de compilation multi-plateforme)... Une application Reflex peut être compilée avec Ghc ou avec Ghcjs et ainsi produire des applications web ou natives (PC ou mobile). Ci-dessous une implémentation en Haskell + Reflex de l’application d’exemple. Contrairement aux implémentations précédentes, ce code ne suit pas une architecture MVC mais gère explicitement les éléments graphiques et leurs flux d'événements. Il est tout à fait possible d'organiser le code selon un MVC mais ceci est à la charge du programmeur. ```haskell {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} import Data.Aeson (FromJSON) import Data.Default (def) import Data.Maybe (fromJust) import Data.Monoid (()) import Data.Text (Text) import GHC.Generics (Generic) import Prelude import Reflex (holdDyn) import Reflex.Dom import Reflex.Dom.Xhr (decodeXhrResponse, performRequestAsync, XhrRequest(..)) main :: IO () main = mainWidget ui -- Model data Animal = Animal { animalType :: Text , animalImage :: Text } deriving (Eq, Generic, Show) instance FromJSON Animal -- View / Controler ui :: MonadWidget t m => m () ui = do el "h1" $ text "Animals (Reflex)" myInput <- el "p" $ textInput def evStart <- getPostBuild let evs = [ () <$ _textInput_input myInput , evStart ] let evCode = tagPromptlyDyn (value myInput) (leftmost evs) evResponse <- performRequestAsync $ queryAnimals <$> evCode let evResult = fromJust . decodeXhrResponse <$> evResponse dynAnimals :: (Dynamic t [Animal]) <- holdDyn [] evResult _ <- el "span" $ simpleList dynAnimals displayAnimal return () queryAnimals :: Text -> XhrRequest () queryAnimals code = XhrRequest "GET" ("http://localhost:3000/api/animals/" code) def displayAnimal :: MonadWidget t m => Dynamic t Animal -> m () displayAnimal dynAnimal = do let imgSrc = () "http://localhost:3000/img/" . animalImage <$> dynAnimal let imgAttrs0 = ("width" =: "320") ("height" =: "240") let imgAttrs = (() imgAttrs0) . (=:) "src" <$> imgSrc el "div" $ do el "p" $ dynText $ animalType <$> dynAnimal elDynAttr "img" imgAttrs $ dynText imgSrc ``` Reflex est un projet assez ambitieux, censé permettre de développer tout type d'interfaces utilisateur et pour de nombreuses plateformes différentes. Il a l'avantage d'utiliser le langage Haskell. En revanche, il est assez compliqué à prendre en main. Programmer directement en FRP nécessite un vrai apprentissage ainsi qu'une bonne compréhension des foncteurs, applicatives et monades. Enfin, il faut structurer son code soigneusement mais soi-même. # Conclusion Elm, Purescript, Miso et Reflex permettent de développer des applis web tout en profitant des avantages de la programmation fonctionnelle. Ces outils facilitent effectivement la validation de code et le refactoring, et réduisent voire suppriment les erreurs au runtime. Le principal inconvénient réside dans le choix parmi ces outils, qui nécessite de faire un compromis entre simplicité et flexibilité. Elm et Purescript sont de bonnes options pour du web « pur et dur ». Miso et Reflex sont plutôt à réserver à des développeurs Haskell expérimentés. Enfin, en dehors d'Haskell, il peut être intéressant de considérer d'autres outils comme ClojureScript, Ocsigen...

AltStyle によって変換されたページ (->オリジナル) /