Apprendre Elm à partir d'un séquenceur de batterie (Partie 1)
Publié: 2022-03-10Si vous êtes un développeur front-end et que vous suivez l'évolution des applications monopage (SPA), vous avez probablement entendu parler d'Elm, le langage fonctionnel qui a inspiré Redux. Si ce n'est pas le cas, il s'agit d'un langage de compilation vers JavaScript comparable aux projets SPA tels que React, Angular et Vue.
Comme ceux-ci, il gère les changements d'état via son dom virtuel visant à rendre le code plus maintenable et performant. Il se concentre sur le bonheur des développeurs, des outils de haute qualité et des modèles simples et reproductibles. Certaines de ses principales différences incluent le fait qu'il s'agit de messages d'erreur de type statique, merveilleusement utiles, et qu'il s'agit d'un langage fonctionnel (par opposition à orienté objet).
Mon introduction est venue d'une conférence donnée par Evan Czaplicki, le créateur d'Elm, sur sa vision de l'expérience des développeurs front-end et, à son tour, sur la vision d'Elm. Comme quelqu'un s'est également concentré sur la maintenabilité et la convivialité du développement frontal, son discours m'a vraiment touché. J'ai essayé Elm dans un projet parallèle il y a un an et je continue de profiter de ses fonctionnalités et de ses défis d'une manière que je n'ai jamais connue depuis que j'ai commencé à programmer. Je suis à nouveau débutant. De plus, je me trouve capable d'appliquer de nombreuses pratiques d'Elm dans d'autres langues.
Développer la sensibilisation à la dépendance
Les dépendances sont partout. En les réduisant, vous pouvez améliorer la probabilité que votre site soit utilisable par le plus grand nombre de personnes dans la plus grande variété de scénarios.Lire un article connexe →
Dans cet article en deux parties, nous allons créer un séquenceur pas à pas pour programmer des rythmes de batterie dans Elm, tout en présentant certaines des meilleures fonctionnalités du langage. Aujourd'hui, nous allons parcourir les concepts fondamentaux d'Elm, c'est-à-dire la prise en main, l'utilisation des types, le rendu des vues et la mise à jour de l'état. La deuxième partie de cet article plongera ensuite dans des sujets plus avancés, tels que la gestion facile de grands refactors, la configuration d'événements récurrents et l'interaction avec JavaScript.
Jouez avec le projet final ici et découvrez son code ici.

Démarrer avec Elm
Pour suivre cet article, je vous recommande d'utiliser Ellie, une expérience de développeur Elm dans le navigateur. Vous n'avez rien besoin d'installer pour exécuter Ellie et vous pouvez y développer des applications entièrement fonctionnelles. Si vous préférez installer Elm sur votre ordinateur, la meilleure façon de vous installer est de suivre le guide de démarrage officiel.
Tout au long de cet article, je ferai un lien vers les versions en cours d'élaboration d'Ellie, même si j'ai développé le séquenceur localement. Et bien que CSS puisse être entièrement écrit en Elm, j'ai écrit ce projet en PostCSS. Cela nécessite un peu de configuration du réacteur Elm pour le développement local afin que les styles soient chargés. Par souci de brièveté, je n'aborderai pas les styles dans cet article, mais les liens Ellie incluent tous les styles CSS minifiés.
Elm est un écosystème autonome qui comprend :
- Faire de l'orme
Pour compiler votre code Elm. Bien que Webpack soit toujours populaire pour la production de projets Elm aux côtés d'autres actifs, ce n'est pas obligatoire. Dans ce projet, j'ai choisi d'exclure Webpack et de compter surelm make
pour compiler le code. - Forfait Orme
Un gestionnaire de packages comparable à NPM pour l'utilisation de packages/modules créés par la communauté. - Réacteur d'orme
Pour exécuter un serveur de développement à compilation automatique. Plus remarquable, il inclut le débogueur de voyage dans le temps, ce qui facilite la navigation dans les états de votre application et la relecture des bogues. - Remplacement d'orme
Pour écrire ou tester des expressions Elm simples dans le terminal.
Tous les fichiers Elm sont considérés comme des modules
. Les premières lignes de tout fichier incluront le module FileName exposing (functions)
où FileName
est le nom de fichier littéral et functions
sont les fonctions publiques que vous souhaitez rendre accessibles aux autres modules. Immédiatement après la définition du module se trouvent les importations à partir de modules externes. Le reste des fonctions suit.
module Main exposing (main) import Html exposing (Html, text) main : Html msg main = text "Hello, World!"
Ce module, nommé Main.elm
, expose une seule fonction, main
, et importe du Html
et du text
à partir du module/package Html
. La fonction main
se compose de deux parties : l' annotation de type et la fonction proprement dite. Les annotations de type peuvent être considérées comme des définitions de fonction. Ils indiquent les types d'arguments et le type de retour. Dans ce cas, le nôtre indique que la fonction main
ne prend aucun argument et renvoie Html msg
. La fonction elle-même rend un nœud de texte contenant "Hello, World". Pour passer des arguments à une fonction, nous ajoutons des noms séparés par des espaces avant le signe égal dans la fonction. Nous ajoutons également les types d'arguments à l'annotation de type, dans l'ordre des arguments, suivis d'une flèche.
add2Numbers : Int -> Int -> Int add2Numbers first second = first + second
En JavaScript, une fonction comme celle-ci est comparable :
function add2Numbers(first, second) { return first + second; }
Et dans un langage typé, comme TypeScript, cela ressemble à :
function add2Numbers(first: number, second: number): number { return first + second; }
add2Numbers
prend deux entiers et renvoie un entier. La dernière valeur de l'annotation est toujours la valeur de retour car chaque fonction doit renvoyer une valeur. Nous appelons add2Numbers
avec 2 et 3 pour obtenir 5 comme add2Numbers 2 3
.
Tout comme vous liez les composants React, nous devons lier le code Elm compilé au DOM. La méthode standard de liaison consiste à appeler embed()
sur notre module et à lui transmettre l'élément DOM.
<script> const container = document.getElementById('app'); const app = Elm.Main.embed(container); <script>
Bien que notre application ne fasse vraiment rien, nous en avons assez pour compiler notre code Elm et rendre le texte. Vérifiez-le sur Ellie et essayez de changer les arguments en add2Numbers
à la ligne 26.

Modélisation des données avec des types
Issus d'un langage à typage dynamique comme JavaScript ou Ruby, les types peuvent sembler superflus. Ces langages déterminent le type que les fonctions prennent à partir de la valeur transmise lors de l'exécution. Les fonctions d'écriture sont généralement considérées comme plus rapides, mais vous perdez la sécurité de vous assurer que vos fonctions peuvent interagir correctement les unes avec les autres.
En revanche, Elm est typé statiquement. Il s'appuie sur son compilateur pour s'assurer que les valeurs transmises aux fonctions sont compatibles avant l'exécution. Cela signifie qu'il n'y a pas d'exceptions d'exécution pour vos utilisateurs, et c'est ainsi qu'Elm peut garantir sa garantie "aucune exception d'exécution". Là où les erreurs de type dans de nombreux compilateurs peuvent être particulièrement cryptiques, Elm s'attache à les rendre faciles à comprendre et à corriger.
Elm rend le démarrage avec les types très convivial. En fait, l'inférence de type d'Elm est si bonne que vous pouvez ignorer l'écriture d'annotations jusqu'à ce que vous soyez plus à l'aise avec elles. Si vous débutez avec les types, je vous recommande de vous fier aux suggestions du compilateur plutôt que d'essayer de les écrire vous-même.
Commençons à modéliser nos données à l'aide de types. Notre séquenceur pas à pas est une chronologie visuelle du moment où un échantillon de batterie particulier doit jouer. La chronologie se compose de pistes , chacune étant affectée à un échantillon de batterie spécifique et à la séquence d'étapes . Un pas peut être considéré comme un moment dans le temps ou comme un battement. Si une étape est active , l'échantillon doit être déclenché pendant la lecture, et si l'étape est inactive , l'échantillon doit rester silencieux. Pendant la lecture, le séquenceur parcourra chaque pas en jouant les échantillons des pas actifs. La vitesse de lecture est définie par les battements par minute (BPM) .

Modélisation de notre application en JavaScript
Pour avoir une meilleure idée de nos types, considérons comment modéliser ce séquenceur de batterie en JavaScript. Il existe un éventail de pistes. Chaque objet piste contient des informations sur lui-même : le nom de la piste, l'échantillon/clip qui se déclenchera et la séquence de valeurs de pas.
tracks: [ { name: "Kick", clip: "kick.mp3", sequence: [On, Off, Off, Off, On, etc...] }, { name: "Snare", clip: "snare.mp3", sequence: [Off, Off, Off, Off, On, etc...] }, etc... ]
Nous devons gérer l'état de la lecture entre la lecture et l'arrêt.
playback: "playing" || "stopped"
Pendant la lecture, nous devons déterminer quel pas doit être joué. Nous devrions également considérer les performances de lecture, et plutôt que de parcourir chaque séquence de chaque piste à chaque fois qu'un pas est incrémenté ; nous devrions réduire toutes les étapes actives en une seule séquence de lecture. Chaque collection dans la séquence de lecture représente tous les échantillons qui doivent être lus. Par exemple, ["kick", "hat"]
signifie que les échantillons de grosse caisse et de charleston doivent jouer, tandis que ["hat"]
signifie que seul le charleston doit jouer. Nous avons également besoin que chaque collection limite l'unicité de l'échantillon, afin de ne pas nous retrouver avec quelque chose comme ["hat", "hat", "hat"]
.
playbackPosition: 1 playbackSequence: [ ["kick", "hat"], [], ["hat"], [], ["snare", "hat"], [], ["hat"], [], ... ],
Et nous devons définir le rythme de lecture, ou le BPM.
bpm: 120
Modélisation avec des types en orme
La transcription de ces données en types Elm décrit essentiellement ce dont nous nous attendons à ce que nos données soient constituées. Par exemple, nous faisons déjà référence à notre modèle de données en tant que model , nous l'appelons donc ainsi avec un alias de type. Les alias de type sont utilisés pour faciliter la lecture du code. Ils ne sont pas un type primitif comme un booléen ou un entier ; ce sont simplement des noms que nous donnons à un type primitif ou à une structure de données. En utilisant un, nous définissons toutes les données qui suivent notre structure de modèle comme un modèle plutôt que comme une structure anonyme. Dans de nombreux projets Elm, la structure principale est nommée Modèle.
type alias Model = { tracks : Array Track , playback : Playback , playbackPosition : PlaybackPosition , bpm : Int , playbackSequence : Array (Set Clip) }
Bien que notre modèle ressemble un peu à un objet JavaScript, il décrit un Elm Record. Les enregistrements sont utilisés pour organiser les données associées dans plusieurs champs qui ont leurs propres annotations de type. Ils sont faciles d'accès à l'aide de field.attribute
, et faciles à mettre à jour, ce que nous verrons plus tard. Les objets et les enregistrements sont très similaires, avec quelques différences clés :
- Les champs inexistants ne peuvent pas être appelés
- Les champs ne seront jamais
null
ouundefined
-
this
etself
ne peuvent pas être utilisés
Notre collection de pistes peut être composée de l'un des trois types possibles : listes, tableaux et ensembles. En bref, les listes sont des collections à usage général non indexées, les tableaux sont indexés et les ensembles ne contiennent que des valeurs uniques. Nous avons besoin d'un index pour savoir quelle étape de piste a été basculée, et puisque les tableaux sont indexés, c'est notre meilleur choix. Alternativement, nous pourrions ajouter un identifiant à la piste et filtrer à partir d'une liste.
Dans notre modèle, nous avons composé les pistes dans un tableau de piste , un autre enregistrement : tracks : Array Track
. La piste contient les informations sur elle-même. name et clip sont des chaînes, mais nous avons tapé clip alias car nous savons qu'il sera référencé ailleurs dans le code par d'autres fonctions. En l'aliasant, nous commençons à créer du code auto-documenté. La création de types et d'alias de type permet aux développeurs de modéliser le modèle de données sur le modèle commercial, créant ainsi un langage omniprésent.
type alias Track = { name : String , clip : Clip , sequence : Array Step } type Step = On | Off type alias Clip = String
Nous savons que la séquence sera un tableau de valeurs on/off. Nous pourrions le définir comme un tableau de booléens, comme sequence : Array Bool
, mais nous raterions une occasion d'exprimer notre modèle économique ! Considérant que les séquenceurs pas à pas sont constitués de pas , nous définissons un nouveau type appelé Step . Une étape peut être un alias de type pour un boolean
, mais nous pouvons aller plus loin : les étapes ont deux valeurs possibles, on et off, c'est ainsi que nous définissons le type d'union. Désormais, les étapes ne peuvent être que activées ou désactivées, ce qui rend tous les autres états impossibles.
Nous définissons un autre type pour Playback
, un alias pour PlaybackPosition
, et utilisons Clip lors de la définition de playbackSequence
en tant que tableau contenant des ensembles de Clips. BPM est assigné en tant que Int
standard.
type Playback = Playing | Stopped type alias PlaybackPosition = Int
Bien qu'il y ait un peu plus de frais généraux pour démarrer avec les types, notre code est beaucoup plus maintenable. Il s'auto-documente et utilise un langage omniprésent avec notre modèle d'entreprise. La confiance que nous gagnons en sachant que nos futures fonctions interagiront avec nos données d'une manière que nous attendons, sans nécessiter de tests, vaut bien le temps qu'il faut pour écrire une annotation. Et, nous pourrions compter sur l'inférence de type du compilateur pour suggérer les types afin de les écrire est aussi simple que de copier et coller. Voici la déclaration de type complète.

Utilisation de l'architecture Elm
L'architecture Elm est un modèle de gestion d'état simple qui a naturellement émergé dans le langage. Il met l'accent sur le modèle commercial et est hautement évolutif. Contrairement aux autres frameworks SPA, Elm a une opinion sur son architecture - c'est la façon dont toutes les applications sont structurées, ce qui facilite l'intégration. L'architecture se compose de trois parties :
- Le model , contenant l'état de l'application, et la structure que l'on tape alias model
- La fonction de mise à jour, qui met à jour l'état
- Et la fonction de vue , qui rend l'état visuellement
Commençons à construire notre séquenceur de batterie en apprenant l'architecture Elm en pratique au fur et à mesure. Nous allons commencer par initialiser notre application, rendre la vue, puis mettre à jour l'état de l'application. Venant d'un milieu Ruby, j'ai tendance à préférer les fichiers plus courts et à diviser mes fonctions Elm en modules bien qu'il soit très normal d'avoir de gros fichiers Elm. J'ai créé un point de départ sur Ellie, mais localement j'ai créé les fichiers suivants :
- Types.elm, contenant toutes les définitions de type
- Main.elm, qui initialise et exécute le programme
- Update.elm, contenant la fonction de mise à jour qui gère l'état
- View.elm, contenant le code Elm à restituer en HTML
Initialisation de notre application
Il est préférable de commencer petit, nous réduisons donc le modèle pour nous concentrer sur la construction d'une seule piste contenant des étapes qui s'activent et se désactivent. Alors que nous pensons déjà connaître toute la structure des données, commencer petit nous permet de nous concentrer sur le rendu des pistes au format HTML. Cela réduit la complexité et le code You Ain't Gonna Need It. Plus tard, le compilateur nous guidera dans la refactorisation de notre modèle. Dans le fichier Types.elm, nous gardons nos types Step et Clip mais changeons le modèle et la piste.
type alias Model = { track : Track } type alias Track = { name : String , sequence : Array Step } type Step = On | Off type alias Clip = String
Pour rendre Elm au format HTML, nous utilisons le package Elm Html. Il a des options pour créer trois types de programmes qui s'appuient les uns sur les autres :
- Programme débutant
Un programme réduit qui exclut les effets secondaires et est particulièrement utile pour apprendre l'architecture Elm. - Programme
Le programme standard qui gère les effets secondaires, utile pour travailler avec des bases de données ou des outils qui existent en dehors d'Elm. - Programme avec drapeaux
Un programme étendu qui peut s'initialiser avec des données réelles au lieu des données par défaut.
C'est une bonne pratique d'utiliser le type de programme le plus simple possible car il est facile de le modifier ultérieurement avec le compilateur. Il s'agit d'une pratique courante lors de la programmation en Elm ; n'utilisez que ce dont vous avez besoin et changez-le plus tard. Pour nos besoins, nous savons que nous devons gérer JavaScript, qui est considéré comme un effet secondaire, nous créons donc un Html.program
. Dans Main.elm, nous devons initialiser le programme en transmettant des fonctions à ses champs.
main : Program Never Model Msg main = Html.program { init = init , view = view , update = update , subscriptions = always Sub.none }
Chaque champ du programme transmet une fonction au runtime Elm, qui contrôle notre application. En un mot, le runtime Elm :
- Démarre le programme avec nos valeurs initiales de
init
. - Rend la première vue en passant notre modèle initialisé dans
view
. - Renvoie continuellement la vue lorsque des messages sont transmis à la mise à
update
à partir de vues, de commandes ou d'abonnements.
Localement, nos fonctions d' view
et de mise à update
seront importées respectivement de View.elm
et Update.elm
, et nous les créerons dans un instant. les subscriptions
écoutent les messages pour provoquer des mises à jour, mais pour l'instant, nous les ignorons en affectant always Sub.none
. Notre première fonction, init
, initialise le modèle. Considérez init
comme les valeurs par défaut pour le premier chargement. Nous le définissons avec une seule piste nommée "kick" et une séquence de pas Off. Comme nous n'obtenons pas de données asynchrones, nous ignorons explicitement les commandes avec Cmd.none
pour initialiser sans effets secondaires.
init : ( Model, Cmd.Cmd Msg ) init = ( { track = { sequence = Array.initialize 16 (always Off) , name = "Kick" } } , Cmd.none )
Notre annotation de type init correspond à notre programme. C'est une structure de données appelée tuple, qui contient un nombre fixe de valeurs. Dans notre cas, le Model
et les commandes. Pour l'instant, nous ignorons toujours les commandes en utilisant Cmd.none
jusqu'à ce que nous soyons prêts à gérer les effets secondaires ultérieurement. Notre application ne rend rien, mais elle compile !
Rendu de notre application
Construisons nos points de vue. À ce stade, notre modèle a une seule piste, c'est donc la seule chose dont nous avons besoin pour le rendu. La structure HTML devrait ressembler à :
<div class="track"> <p class "track-title">Kick</p> <div class="track-sequence"> <button class="step _active"></button> <button class="step"></button> <button class="step"></button> <button class="step"></button> etc... </div> </div>
Nous allons construire trois fonctions pour afficher nos vues :
- Un pour rendre une seule piste, qui contient le nom de la piste et la séquence
- Un autre pour rendre la séquence elle-même
- Et un de plus pour rendre chaque bouton d'étape individuel dans la séquence
Notre première fonction d'affichage rendra une seule piste. Nous nous appuyons sur notre annotation de type, renderTrack : Track -> Html Msg
, pour appliquer une seule piste traversée. L'utilisation de types signifie que nous savons toujours que renderTrack
aura une piste. Nous n'avons pas besoin de vérifier si le champ de name
existe sur l'enregistrement, ou si nous avons passé une chaîne au lieu d'un enregistrement. Elm ne compilera pas si nous essayons de passer autre chose que Track
à renderTrack
. Mieux encore, si nous commettons une erreur et essayons accidentellement de passer autre chose qu'une piste à la fonction, le compilateur nous enverra des messages conviviaux pour nous orienter dans la bonne direction.
renderTrack : Track -> Html Msg renderTrack track = div [ class "track" ] [ p [ class "track-title" ] [ text track.name ] , div [ class "track-sequence" ] (renderSequence track.sequence) ]
Cela peut sembler évident, mais tout Elm est Elm, y compris l'écriture HTML. Il n'y a pas de langage de template ou d'abstraction pour écrire du HTML — c'est tout Elm. Les éléments HTML sont des fonctions Elm, qui prennent le nom, une liste d'attributs et une liste d'enfants. Donc div [ class "track" ] []
affiche <div class="track"></div>
. Les listes sont séparées par des virgules dans Elm, donc l'ajout d'un identifiant à la div ressemblerait à div [ class "track", id "my-id" ] []
.
La div wraping track-sequence
transmet la séquence de la piste à notre deuxième fonction, renderSequence
. Il prend une séquence et renvoie une liste de boutons HTML. Nous pourrions conserver renderSequence
dans renderTrack
pour ignorer la fonction supplémentaire, mais je trouve qu'il est beaucoup plus facile de raisonner en divisant les fonctions en plus petits morceaux. De plus, nous avons une autre opportunité de définir une annotation de type plus stricte.
renderSequence : Array Step -> List (Html Msg) renderSequence sequence = Array.indexedMap renderStep sequence |> Array.toList
Nous mappons chaque étape de la séquence et la transmettons à la fonction renderStep
. En JavaScript, le mappage avec un index s'écrirait comme suit :
sequence.map((node, index) => renderStep(index, node))
Comparé à JavaScript, le mappage dans Elm est presque inversé. Nous appelons Array.indexedMap
, qui prend deux arguments : la fonction à appliquer dans la carte ( renderStep
) et le tableau à mapper ( sequence
). renderStep
est notre dernière fonction et elle détermine si un bouton est actif ou inactif. Nous utilisons indexedMap
car nous devons transmettre l'index de l'étape (que nous utilisons comme ID) à l'étape elle-même afin de la transmettre à la fonction de mise à jour.
renderStep : Int -> Step -> Html Msg renderStep index step = let classes = if step == On then "step _active" else "step" in button [ class classes ] []
renderStep
accepte l'index comme premier argument, l'étape comme second et renvoie le HTML rendu. En utilisant un bloc let...in
pour définir des fonctions locales, nous attribuons la classe _active
à On Steps et appelons notre fonction de classes dans la liste des attributs du bouton.

Mise à jour de l'état de l'application
À ce stade, notre application restitue les 16 étapes de la séquence de coup de pied, mais cliquer n'active pas l'étape. Afin de mettre à jour l'état de l'étape, nous devons transmettre un message ( Msg
) à la fonction de mise à jour. Pour ce faire, nous définissons un message et l'attachons à un gestionnaire d'événements pour notre bouton.
Dans Types.elm, nous devons définir notre premier message, ToggleStep
. Il faudra un Int
pour l'index de séquence et un Step
. Ensuite, dans renderStep
, nous attachons le message ToggleStep
à l'événement de clic du bouton, ainsi que l'index de séquence et l'étape en tant qu'arguments. Cela enverra le message à notre fonction de mise à jour, mais à ce stade, la mise à jour ne fera rien.
type Msg = ToggleStep Int Step renderStep index step = let ... in button [ onClick (ToggleStep index step) , class classes ] []
Les messages sont des types normaux, mais nous les avons définis comme le type provoquant des mises à jour, ce qui est la convention dans Elm. Dans Update.elm, nous suivons l'architecture Elm pour gérer les changements d'état du modèle. Notre fonction de mise à jour prendra un Msg
et le Model
actuel, et renverra un nouveau modèle et potentiellement une commande. Les commandes gèrent les effets secondaires, que nous examinerons dans la deuxième partie. Nous savons que nous aurons plusieurs types de Msg
, nous avons donc mis en place un bloc de cas de correspondance de modèle. Cela nous oblige à gérer tous nos cas tout en séparant également le flux d'état. Et le compilateur s'assurera que nous ne manquons aucun cas qui pourrait changer notre modèle.
La mise à jour d'un enregistrement dans Elm se fait un peu différemment de la mise à jour d'un objet en JavaScript. Nous ne pouvons pas modifier directement un champ sur l'enregistrement comme record.field = *
car nous ne pouvons pas utiliser this
ou self
, mais Elm a des assistants intégrés. Étant donné un enregistrement comme brian = { name = "brian" }
, nous pouvons mettre à jour le champ name comme { brian | name = "BRIAN" }
{ brian | name = "BRIAN" }
. Le format suit { record | field = newValue }
{ record | field = newValue }
.
Voici comment mettre à jour les champs de niveau supérieur, mais les champs imbriqués sont plus délicats dans Elm. Nous devons définir nos propres fonctions d'assistance, nous allons donc définir quatre fonctions d'assistance pour plonger dans les enregistrements imbriqués :
- Un pour basculer la valeur du pas
- Un pour renvoyer une nouvelle séquence, contenant la valeur de pas mise à jour
- Un autre pour choisir à quelle piste appartient la séquence
- Et une dernière fonction pour renvoyer une nouvelle piste, contenant la séquence mise à jour qui contient la valeur de pas mise à jour
Nous commençons par ToggleStep
pour basculer la valeur de pas de la séquence de piste entre On et Off. Nous utilisons à nouveau un bloc let...in
pour créer des fonctions plus petites dans l'instruction case. Si l'étape est déjà désactivée, nous l'activons et vice-versa.
toggleStep = if step == Off then On else Off
toggleStep
sera appelé depuis newSequence
. Les données sont immuables dans les langages fonctionnels, donc plutôt que de modifier la séquence, nous créons en fait une nouvelle séquence avec une valeur de pas mise à jour pour remplacer l'ancienne.
newSequence = Array.set index toggleStep selectedTrack.sequence
newSequence
utilise Array.set
pour trouver l'index que nous voulons basculer, puis crée la nouvelle séquence. Si set ne trouve pas l'index, il renvoie la même séquence. Il s'appuie sur selectedTrack.sequence
pour savoir quelle séquence modifier. selectedTrack
est notre fonction d'assistance clé utilisée pour que nous puissions accéder à notre enregistrement imbriqué. À ce stade, c'est étonnamment simple car notre modèle n'a qu'une seule piste.
selectedTrack = model.track
Notre dernière fonction d'assistance relie tout le reste. Encore une fois, puisque les données sont immuables, nous remplaçons notre piste entière par une nouvelle piste contenant une nouvelle séquence.
newTrack = { selectedTrack | sequence = newSequence }
newTrack
est appelé en dehors du bloc let...in
, où nous renvoyons un nouveau modèle, contenant la nouvelle piste, qui restitue la vue. Nous ne transmettons pas d'effets secondaires, nous utilisons donc à nouveau Cmd.none
. L'ensemble de notre fonction de update
à jour ressemble à :
update : Msg -> Model -> ( Model, Cmd Msg ) update msg model = case msg of ToggleStep index step -> let selectedTrack = model.track newTrack = { selectedTrack | sequence = newSequence } toggleStep = if step == Off then On else Off newSequence = Array.set index toggleStep selectedTrack.sequence in ( { model | track = newTrack } , Cmd.none )
Lorsque nous exécutons notre programme, nous voyons une piste rendue avec une série d'étapes. Cliquer sur l'un des boutons d'étape déclenche ToggleStep
, qui exécute notre fonction de mise à jour pour remplacer l'état du modèle.

Au fur et à mesure que notre application évoluera, nous verrons comment le modèle reproductible de l'architecture Elm simplifie la gestion de l'état. La familiarité de ses fonctions de modèle, de mise à jour et d'affichage nous aide à nous concentrer sur notre domaine d'activité et facilite l'accès à l'application Elm de quelqu'un d'autre.
Faire une pause
Écrire dans une nouvelle langue demande du temps et de la pratique. Les premiers projets sur lesquels j'ai travaillé étaient de simples clones TypeForm que j'ai utilisés pour apprendre la syntaxe Elm, l'architecture et les paradigmes de programmation fonctionnelle. À ce stade, vous en avez déjà suffisamment appris pour faire quelque chose de similaire. Si vous êtes impatient, je vous recommande de parcourir le Guide de démarrage officiel. Evan, le créateur d'Elm, vous explique les motivations d'Elm, la syntaxe, les types, l'architecture Elm, la mise à l'échelle, etc., à l'aide d'exemples pratiques.
Dans la deuxième partie, nous allons plonger dans l'une des meilleures fonctionnalités d'Elm : utiliser le compilateur pour refactoriser notre séquenceur pas à pas. De plus, nous apprendrons à gérer les événements récurrents, à utiliser des commandes pour les effets secondaires et à interagir avec JavaScript. Restez à l'écoute!