Développement web frontend en Haskell, Elm et Purescript
Voir aussi : vidéo peertube - vidéo youtube - dépôt git - article linuxfr.org
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 ce tutoriel est de rappeler quelques généralités sur le développement web frontend, et de présenter les outils Elm, Purescript, Miso et Reflex, à partir d’un exemple d’application (gallerie d’images fournie via une API web).
Attention : ceci n’est pas d’une étude rigoureuse et avancée mais juste un petit retour de petite expérience.
Généralités sur le web frontend
Page web, application native, application web
- page web :
- web “historique”
- pages statiques + liens, puis éléments dynamiques
- HTML/CSS/JS
- application native :
- logiciel exécuté directement sur le système d’exploitation
- Java/C#/Swift/C++…
exemple d’application native (libreoffice) :
- application web :
- logiciel exécuté dans le navigateur (DOM)
- communications réseaux (AJAX, websockets…)
- HTML/CSS/JS
exemple d’application web (google-docs) :
- convergence web/natif :
- objectifs similaires : UI graphique
- mêmes architectures de code : Modèle-Vue-Contrôleur…
- C++ dans du web (Emscripten/WebAssembly), HTML/CSS/JS pour du natif (Electron)
exemple d’application Electron (atom) :
Les langages en frontend
généralement JavaScript
nombreux avantages : expressif, flexible, disponible dans le navigateur…
mais propice aux erreurs au runtime
solutions classiques : debugger, tests unitaires, frameworks (angular, react…), langages dérivés (TypeScript…)
Haskell et le frontend
très peu répandu : dev frontend très souvent en JS (ou dérivés)
intérêt des langages fonctionnels : typage statique, immuabilité… = moins d’erreurs au runtime
compilateur/transpileur vers JS : écrire dans un langage puis convertir en JS classique exécutable dans un navigateur
en Haskell classique : GHCJS + bibliothèques (Miso, Reflex)
langages inspirés d’Haskell mais dédiés au web : Elm, Purescript
autres : ClojureScript, OCaml (Ocsigen)…
Fonctionnement des frameworks web fonctionnels
- MVC (Model-View-Controler) : architecture générale de l’application
- FRP (Functional Reactive Programming) : comportement dynamique de l’application (événements + comportement selon le temps)
- Virtual-DOM : manipulation efficace de la structure de l’application dans le navigateur
Projet d’exemple
appli client-serveur de recherche d’images d’animaux
côté serveur : API JSON + fichiers images
côté client : affichage d’une zone de texte et des images + requêtes AJAX classiques
Elm
Présentation de Elm
écosystème complet : langage (Haskell très simplifié), compilateur, bibliothèques
Virtual-DOM + MVC
réputé simple et performant
Projet d’exemple en Elm
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
: Program Never Model Msg
main = Html.program
main init = init
{ = view
, view = update
, update = subscriptions
, subscriptions
}
-- Model
type alias Animal =
: String
{ animalType : String
, animalImage
}
: JD.Decoder Animal
decodeAnimal = JD.map2 Animal
decodeAnimal "animalType" JD.string)
(JD.field "animalImage" JD.string)
(JD.field
: JD.Decoder (List Animal)
decodeAnimalList = JD.list decodeAnimal
decodeAnimalList
type alias Model = { modelAnimals : List Animal }
init : ( Model, Cmd Msg )
init = ( Model [], queryAnimals "" )
-- Controler
: Model -> Sub Msg
subscriptions = Sub.none
subscriptions _
type Msg
= MsgInput String
| MsgAnimals (Result Http.Error (List Animal))
: Msg -> Model -> ( Model, Cmd Msg )
update =
update msg model case msg of
MsgInput animalType -> ( model, queryAnimals animalType )
MsgAnimals (Ok Model animals) -> ( Model animals, Cmd.none )
MsgAnimals (Err _) -> ( Model [], Cmd.none )
: String -> Cmd Msg
queryAnimals =
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)" ]
MsgInput ] [] ]
, p [] [ input [ onInput span []
,
(List.map-> div [] [ p [] [ text a.animalType ]
(\a
, img"http://localhost:3000/img/" ++ a.animalImage)
[ src (240
, height 320
, width
]
[]
].modelAnimals
) model
) ]
Conclusion sur Elm
- avantages :
- simple
- efficace
- MVC adapté pour de nombreuses applis web
- inconvénients :
- limité au MVC
Purescript
Présentation de Purescript
écosystème complet : langage (proche d’Haskell), compilateur, bibliothèques
plus général que Elm : MVC, FRP, Virtual-DOM, client/serveur…
Projet d’exemple en Purescript
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
= HA.runHalogenAff do
main <- HA.awaitBody
body <- runUI ui unit body
io .query $ H.action $ QueryAnimals ""
io
ui :: forall eff. H.Component HH.HTML Query Unit Void (Aff (ajax :: AX.AJAX | eff))
= H.component
ui : const initialState
{ initialState
, render
, eval: const Nothing
, receiver
}
-- Model
newtype Animal = Animal
animalType :: String
{ animalImage :: String
,
}
instance decodeJsonBlogPost :: DecodeJson Animal where
= do
decodeJson json <- decodeJson json
obj <- obj .? "animalType"
animalType <- obj .? "animalImage"
animalImage pure $ Animal { animalType, animalImage }
decodeAnimalArray :: Json -> Either String (Array Animal)
= decodeJson json >>= traverse decodeJson
decodeAnimalArray json
type Model = { modelAnimals :: Array Animal }
initialState :: Model
= { modelAnimals: [] }
initialState
-- Controler
data Query a = QueryAnimals String a
eval :: forall eff. Query ~> H.ComponentDSL Model Query Void (Aff (ajax :: AX.AJAX | eff))
QueryAnimals animal_type next) = do
eval (= [] })
H.modify (_ { modelAnimals <- H.liftAff $ AX.get ("http://localhost:3000/api/animals/" <> animal_type)
response let animals = case decodeAnimalArray response.response of
Left _ -> []
Right ra -> ra
= animals })
H.modify (_ { modelAnimals pure next
-- View
render :: Model -> H.ComponentHTML Query
=
render m
HH.span []"Animals (Purescript)" ]
[ HH.h1 [] [ HH.text QueryAnimals) ] ]
, HH.p [] [ HH.input [ HE.onValueInput (HE.input
, HH.span [] map (\ (Animal {animalType, animalImage})
(-> HH.div []
[ HH.p [] [ HH.text animalType ]"http://localhost:3000/img/" <> animalImage)
, HH.img [ HP.src (320
, HP.width 240
, HP.height
]
].modelAnimals)
) m ]
Conclusion sur Purescript
- avantages :
- adapté au web
- plus complet/général que Elm
- langage plus puissant que Elm
- inconvénients :
- plus compliqué que Elm
- mise en place d’un projet plus compliquée
Haskell/Miso
Présentation de Miso
bibliothèque frontend en Haskell + GHCJS
MVC + Virtual-DOM
inspiré de Elm
Projet d’exemple en Miso
{-# 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 ()
= startApp App
main = Model []
{ model = updateModel
, update = viewModel
, view = []
, subs = defaultEvents
, events = GetAnimals ""
, initialAction = Nothing
, mountPoint
}
-- 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
GetAnimals str) m = m <# (SetAnimals <$> queryAnimals str)
updateModel (SetAnimals animals) m = noEff m { modelAnimals = animals }
updateModel (NoOp m = noEff m
updateModel
queryAnimals :: MisoString -> IO [Animal]
= do
queryAnimals str let uri = pack $ "http://localhost:3000/api/animals/" ++ fromMisoString str
= Request GET uri Nothing [] False NoData
req Just cont <- contents <$> xhrByteString req
return $ fromMaybe [] $ decodeStrict cont
-- View
viewModel :: Model -> View Action
Model animals) =
viewModel (
span_ []"Animals (Miso)" ]
[ h1_ [] [ text GetAnimals ] ]
, p_ [] [ input_ [ onInput $ map fmtAnimal animals
, span_ []
]
fmtAnimal :: Animal -> View Action
=
fmtAnimal animal
div_ [] $ toMisoString $ animalType animal ]
[ p_ [] [ text $ toMisoString $ "http://localhost:3000/img/" <> animalImage animal
, img_ [ src_ "320"
, width_ "240"
, height_
] ]
Conclusion sur Miso
- avantages :
- simple (MVC similaire à Elm)
- langage et écosystème Haskell
- inconvénients :
- maturité/compatibilité des différents outils ghcjs/miso/nix
- temps d’installation et de compilation
Haskell/Reflex
Présentation de Reflex
bibliothèque Haskell de FRP générique, + reflex-dom, reflex-platform…
peut produire des applis : web, natives, android, ios (via GHC ou GHCJS)
Projet d’exemple en Reflex
{-# 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 ()
= mainWidget ui
main
-- Model
data Animal = Animal
animalType :: Text
{ animalImage :: Text
,deriving (Eq, Generic, Show)
}
instance FromJSON Animal
-- View / Controler
ui :: MonadWidget t m => m ()
= do
ui "h1" $ text "Animals (Reflex)"
el <- el "p" $ textInput def
myInput <- getPostBuild
evStart let evs = [ () <$ _textInput_input myInput , evStart ]
let evCode = tagPromptlyDyn (value myInput) (leftmost evs)
<- performRequestAsync $ queryAnimals <$> evCode
evResponse let evResult = fromJust . decodeXhrResponse <$> evResponse
dynAnimals :: (Dynamic t [Animal]) <- holdDyn [] evResult
<- el "span" $ simpleList dynAnimals displayAnimal
_ return ()
queryAnimals :: Text -> XhrRequest ()
= XhrRequest "GET" ("http://localhost:3000/api/animals/" <> code) def
queryAnimals code
displayAnimal :: MonadWidget t m => Dynamic t Animal -> m ()
= do
displayAnimal dynAnimal let imgSrc = (<>) "http://localhost:3000/img/" . animalImage <$> dynAnimal
let imgAttrs0 = ("width" =: "320") <> ("height" =: "240")
let imgAttrs = ((<>) imgAttrs0) . (=:) "src" <$> imgSrc
"div" $ do
el "p" $ dynText $ animalType <$> dynAnimal
el "img" imgAttrs $ dynText imgSrc elDynAttr
Conclusion sur Reflex
- avantages :
- langage et écosystème Haskell
- potentiel intéressant (types d’applis, domaines…)
- inconvénients : assez compliqué
- apprentissage du FRP
- foncteurs, applicatives, monades
- architecture de code
Conclusion sur le web frontend en fonctionnel
quelques outils intéressants : Elm, Purescript, Miso, Reflex…
avantages : moins d’erreurs au runtime, validation, refactoring
inconvénients : outils pas toujours très matures, compromis simplicité/flexibilité pour le choix des outils
je suis / je développe appli « classique » appli « compliquée » développeur web Elm Purescript développeur haskell Miso Reflex ou Purescript à voir également : ClojureScript, Ocsigen…