Elm von einem Drum Sequencer lernen (Teil 1)

Veröffentlicht: 2022-03-10
Kurze Zusammenfassung ↬ Front-End-Entwickler Brian Holt führt die Leser durch den Aufbau eines Drum-Sequenzers in Elm. Im ersten Teil dieser zweiteiligen Serie stellt er die Syntax, den Aufbau und die Kernkonzepte von Elm vor. Sie lernen, mit der Elm-Architektur zu arbeiten, um einfache Anwendungen zu erstellen.

Wenn Sie ein Front-End-Entwickler sind, der die Entwicklung von Single-Page-Anwendungen (SPA) verfolgt, haben Sie wahrscheinlich schon von Elm gehört, der funktionalen Sprache, die Redux inspiriert hat. Falls nicht, handelt es sich um eine Kompilierungs-zu-JavaScript-Sprache, die mit SPA-Projekten wie React, Angular und Vue vergleichbar ist.

Wie diese verwaltet es Zustandsänderungen über seinen virtuellen Dom mit dem Ziel, den Code wartbarer und leistungsfähiger zu machen. Es konzentriert sich auf die Zufriedenheit der Entwickler, hochwertige Werkzeuge und einfache, wiederholbare Muster. Zu den wichtigsten Unterschieden gehören statisch typisierte, wunderbar hilfreiche Fehlermeldungen und dass es sich um eine funktionale Sprache handelt (im Gegensatz zu objektorientiert).

Meine Einführung erfolgte durch einen Vortrag von Evan Czaplicki, dem Schöpfer von Elm, über seine Vision für das Front-End-Entwicklererlebnis und die Vision für Elm. Da sich jemand auch auf die Wartbarkeit und Benutzerfreundlichkeit der Frontend-Entwicklung konzentrierte, fand sein Vortrag bei mir großen Anklang. Ich habe Elm vor einem Jahr in einem Nebenprojekt ausprobiert und genieße weiterhin sowohl seine Funktionen als auch seine Herausforderungen auf eine Weise, wie ich es nicht getan habe, seit ich mit dem Programmieren begonnen habe; Ich bin wieder ein Anfänger. Darüber hinaus finde ich mich in der Lage, viele von Elms Praktiken in anderen Sprachen anzuwenden.

Abhängigkeitsbewusstsein entwickeln

Abhängigkeiten sind überall. Indem Sie sie reduzieren, können Sie die Wahrscheinlichkeit erhöhen, dass Ihre Website von der größtmöglichen Anzahl von Personen in den unterschiedlichsten Szenarien verwendet werden kann. Lesen Sie einen verwandten Artikel →

In diesem zweiteiligen Artikel bauen wir einen Step-Sequencer zum Programmieren von Drum-Beats in Elm, während wir einige der besten Features der Sprache vorstellen. Heute gehen wir durch die grundlegenden Konzepte in Elm, dh erste Schritte, Verwendung von Typen, Rendern von Ansichten und Aktualisieren des Status. Der zweite Teil dieses Artikels wird dann in fortgeschrittenere Themen eintauchen, wie z. B. den einfachen Umgang mit großen Refactors, das Einrichten wiederkehrender Ereignisse und die Interaktion mit JavaScript.

Spielen Sie hier mit dem endgültigen Projekt und sehen Sie sich seinen Code hier an.

Stepsequenzer in Aktion
So sieht der fertige Step-Sequencer in Aktion aus.

Erste Schritte mit Elm

Um diesem Artikel zu folgen, empfehle ich die Verwendung von Ellie, einer In-Browser-Elm-Entwicklererfahrung. Sie müssen nichts installieren, um Ellie auszuführen, und Sie können darin voll funktionsfähige Anwendungen entwickeln. Wenn Sie Elm lieber auf Ihrem Computer installieren möchten, befolgen Sie am besten die offizielle Anleitung „Erste Schritte“.

Mehr nach dem Sprung! Lesen Sie unten weiter ↓

In diesem Artikel werde ich auf die in Arbeit befindlichen Ellie-Versionen verlinken, obwohl ich den Sequenzer lokal entwickelt habe. Und während CSS vollständig in Elm geschrieben werden kann, habe ich dieses Projekt in PostCSS geschrieben. Dies erfordert ein wenig Konfiguration des Elm Reactor für die lokale Entwicklung, um Stile laden zu können. Der Kürze halber werde ich in diesem Artikel nicht auf Stile eingehen, aber die Ellie-Links enthalten alle minimierten CSS-Stile.

Elm ist ein in sich geschlossenes Ökosystem, das Folgendes umfasst:

  • Ulme machen
    Zum Kompilieren Ihres Elm-Codes. Obwohl Webpack immer noch beliebt ist, um Elm-Projekte neben anderen Assets zu produzieren, ist es nicht erforderlich. In diesem Projekt habe ich mich dafür entschieden, Webpack auszuschließen und mich auf elm make zu verlassen, um den Code zu kompilieren.
  • Elm-Paket
    Ein mit NPM vergleichbarer Paketmanager zur Verwendung von von der Community erstellten Paketen/Modulen.
  • Elm-Reaktor
    Zum Ausführen eines automatisch kompilierenden Entwicklungsservers. Bemerkenswerter ist, dass es den Time Travelling Debugger enthält, der es einfach macht, durch die Zustände Ihrer Anwendung zu gehen und Fehler wiederzugeben .
  • Ulme Ersatz
    Zum Schreiben oder Ausprobieren einfacher Elm-Ausdrücke im Terminal.

Alle Elm-Dateien werden als modules betrachtet. Die Anfangszeilen jeder Datei enthalten das module FileName exposing (functions) wobei FileName der wörtliche Dateiname ist und functions die öffentlichen Funktionen sind, die Sie anderen Modulen zugänglich machen möchten. Unmittelbar nach der Moduldefinition erfolgen Importe aus externen Modulen. Die restlichen Funktionen folgen.

 module Main exposing (main) import Html exposing (Html, text) main : Html msg main = text "Hello, World!"

Dieses Modul mit dem Namen Main.elm stellt eine einzelne Funktion, main , bereit und importiert Html und text aus dem Html -Modul/Paket. Die main besteht aus zwei Teilen: der Typanmerkung und der eigentlichen Funktion. Typanmerkungen können als Funktionsdefinitionen betrachtet werden. Sie geben die Argumenttypen und den Rückgabetyp an. In diesem Fall gibt unsere an, dass die main keine Argumente akzeptiert und Html msg . Die Funktion selbst rendert einen Textknoten, der „Hello, World“ enthält. Um Argumente an eine Funktion zu übergeben, fügen wir durch Leerzeichen getrennte Namen vor dem Gleichheitszeichen in der Funktion ein. Wir fügen auch die Argumenttypen in der Reihenfolge der Argumente zur Typanmerkung hinzu, gefolgt von einem Pfeil.

 add2Numbers : Int -> Int -> Int add2Numbers first second = first + second

In JavaScript ist eine Funktion wie diese vergleichbar:

 function add2Numbers(first, second) { return first + second; }

Und in einer typisierten Sprache wie TypeScript sieht es so aus:

 function add2Numbers(first: number, second: number): number { return first + second; }

add2Numbers nimmt zwei ganze Zahlen und gibt eine ganze Zahl zurück. Der letzte Wert in der Annotation ist immer der Rückgabewert, da jede Funktion einen Wert zurückgeben muss . Wir rufen add2Numbers mit 2 und 3 auf, um 5 zu erhalten, wie add2Numbers 2 3 .

So wie Sie React-Komponenten binden, müssen wir kompilierten Elm-Code an das DOM binden. Die Standardmethode zum Binden besteht darin, embed() in unserem Modul aufzurufen und das DOM-Element daran zu übergeben.

 <script> const container = document.getElementById('app'); const app = Elm.Main.embed(container); <script>

Obwohl unsere App nicht wirklich etwas tut, haben wir genug, um unseren Elm-Code zu kompilieren und Text zu rendern. Probieren Sie es bei Ellie aus und versuchen Sie, die Argumente in Zeile 26 in add2Numbers zu ändern.

Die Elm-App rendert zusätzliche Zahlen
Unsere einfache Elm-App, die zusätzliche Zahlen auf dem Bildschirm darstellt.

Datenmodellierung mit Typen

Aus einer dynamisch typisierten Sprache wie JavaScript oder Ruby kommend, scheinen Typen überflüssig zu sein. Diese Sprachen bestimmen, welchen Typ Funktionen von dem Wert übernehmen, der während der Laufzeit übergeben wird. Das Schreiben von Funktionen gilt im Allgemeinen als schneller, aber Sie verlieren die Sicherheit, sicherzustellen, dass Ihre Funktionen ordnungsgemäß miteinander interagieren können.

Im Gegensatz dazu ist Elm statisch typisiert. Es verlässt sich auf seinen Compiler, um sicherzustellen, dass an Funktionen übergebene Werte vor der Laufzeit kompatibel sind. Dies bedeutet keine Laufzeitausnahmen für Ihre Benutzer, und so kann Elm seine „Keine Laufzeitausnahmen“-Garantie geben. Wo Tippfehler in vielen Compilern besonders kryptisch sein können, konzentriert sich Elm darauf, sie leicht verständlich und korrigierbar zu machen.

Elm macht den Einstieg mit Typen sehr freundlich. Tatsächlich ist die Typinferenz von Elm so gut, dass Sie das Schreiben von Anmerkungen überspringen können, bis Sie damit besser vertraut sind. Wenn Sie mit Typen ganz neu sind, empfehle ich, sich auf die Vorschläge des Compilers zu verlassen, anstatt zu versuchen, sie selbst zu schreiben.

Beginnen wir mit der Modellierung unserer Daten mithilfe von Typen. Unser Step-Sequencer ist eine visuelle Zeitleiste, wann ein bestimmtes Drum-Sample abgespielt werden soll. Die Timeline besteht aus Tracks , denen jeweils ein bestimmtes Drum-Sample und die Schrittfolge zugeordnet sind. Ein Schritt kann als Moment oder Schlag betrachtet werden. Wenn ein Schritt aktiv ist, sollte das Sample während der Wiedergabe getriggert werden, und wenn der Schritt inaktiv ist, sollte das Sample stumm bleiben. Während der Wiedergabe bewegt sich der Sequenzer durch jeden Schritt und spielt die Samples der aktiven Schritte ab. Die Wiedergabegeschwindigkeit wird durch Beats Per Minute (BPM) festgelegt.

endgültige Anwendung aus Tracks mit Schrittfolgen
Ein Screenshot unserer endgültigen Anwendung, bestehend aus Tracks mit Schrittfolgen.

Modellierung unserer Anwendung in JavaScript

Um eine bessere Vorstellung von unseren Typen zu bekommen, betrachten wir, wie dieser Drum-Sequenzer in JavaScript modelliert wird. Es gibt eine Reihe von Spuren. Jedes Track-Objekt enthält Informationen über sich selbst: den Track-Namen, das Trigger-Sample/den Clip und die Sequenz der Step-Werte.

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

Wir müssen den Wiedergabestatus zwischen Wiedergabe und Stopp verwalten.

 playback: "playing" || "stopped"

Während der Wiedergabe müssen wir bestimmen, welcher Schritt gespielt werden soll. Wir sollten auch die Wiedergabeleistung berücksichtigen und anstatt jedes Mal, wenn ein Schritt erhöht wird, jede Sequenz in jeder Spur zu durchlaufen; Wir sollten alle aktiven Schritte auf eine einzige Wiedergabesequenz reduzieren. Jede Sammlung innerhalb der Wiedergabesequenz stellt alle Samples dar, die abgespielt werden sollen. Zum Beispiel bedeutet ["kick", "hat"] , dass die Kick- und Hi-Hat-Samples gespielt werden sollen, wohingegen ["hat"] bedeutet, dass nur die Hi-Hat gespielt werden soll. Wir brauchen auch jede Sammlung, um die Eindeutigkeit des Beispiels einzuschränken, damit wir nicht mit etwas wie ["hat", "hat", "hat"] enden.

 playbackPosition: 1 playbackSequence: [ ["kick", "hat"], [], ["hat"], [], ["snare", "hat"], [], ["hat"], [], ... ],

Und wir müssen das Tempo der Wiedergabe oder die BPM einstellen.

 bpm: 120

Modellieren mit Typen in Ulme

Das Umschreiben dieser Daten in Elm-Typen beschreibt im Wesentlichen, woraus wir erwarten, dass unsere Daten bestehen. Zum Beispiel beziehen wir uns bereits auf unser Datenmodell als model , also nennen wir es so mit einem Typ-Alias. Typaliase werden verwendet, um Code leichter lesbar zu machen. Sie sind kein primitiver Typ wie ein boolescher Wert oder eine Ganzzahl; sie sind einfach Namen, die wir einem primitiven Typ oder einer Datenstruktur geben. Mit einem definieren wir alle Daten, die unserer Modellstruktur folgen, als Modell und nicht als anonyme Struktur. In vielen Elm-Projekten heißt die Hauptstruktur Model.

 type alias Model = { tracks : Array Track , playback : Playback , playbackPosition : PlaybackPosition , bpm : Int , playbackSequence : Array (Set Clip) }

Obwohl unser Modell ein bisschen wie ein JavaScript-Objekt aussieht, beschreibt es einen Elm-Datensatz. Datensätze werden verwendet, um zusammengehörige Daten in mehreren Feldern zu organisieren, die ihre eigenen Typanmerkungen haben. Sie sind mit field.attribute leicht zugänglich und leicht zu aktualisieren, was wir später sehen werden. Objekte und Datensätze sind sich sehr ähnlich, mit einigen wesentlichen Unterschieden:

  • Nicht vorhandene Felder können nicht aufgerufen werden
  • Felder sind niemals null oder undefined
  • this und self können nicht verwendet werden

Unsere Track-Sammlung kann aus einem von drei möglichen Typen bestehen: Listen, Arrays und Sets. Kurz gesagt, Listen sind nicht indizierte Sammlungen für den allgemeinen Gebrauch, Arrays sind indiziert und Sets enthalten nur eindeutige Werte. Wir brauchen einen Index, um zu wissen, welcher Spurschritt umgeschaltet wurde, und da Arrays indiziert sind, ist dies unsere beste Wahl. Alternativ könnten wir dem Track eine ID hinzufügen und aus einer Liste filtern.

In unserem Modell haben wir Tracks in ein Array von track gesetzt, einen anderen Datensatz: tracks : Array Track . Track enthält die Informationen über sich selbst. Sowohl name als auch clip sind Strings, aber wir haben aliased clip eingegeben, weil wir wissen, dass an anderer Stelle im Code von anderen Funktionen darauf verwiesen wird. Indem wir es aliasieren, beginnen wir damit, selbstdokumentierenden Code zu erstellen. Durch das Erstellen von Typen und Typaliasen können Entwickler das Datenmodell an das Geschäftsmodell anpassen und so eine allgegenwärtige Sprache erstellen.

 type alias Track = { name : String , clip : Clip , sequence : Array Step } type Step = On | Off type alias Clip = String

Wir wissen, dass die Sequenz ein Array von Ein/Aus-Werten sein wird. Wir könnten es als ein Array von booleschen Werten festlegen, wie beispielsweise sequence : Array Bool , aber wir würden eine Gelegenheit verpassen, unser Geschäftsmodell auszudrücken! In Anbetracht der Tatsache, dass Step-Sequenzer aus Steps bestehen, definieren wir einen neuen Typ namens Step . Ein Step könnte ein Typ-Alias ​​für einen boolean Wert sein, aber wir können noch einen Schritt weiter gehen: Steps haben zwei mögliche Werte, on und off, also definieren wir den Union-Typ so. Jetzt können Schritte immer nur Ein oder Aus sein, was alle anderen Zustände unmöglich macht.

Wir definieren einen anderen Typ für Playback , einen Alias ​​für PlaybackPosition , und verwenden Clip , wenn wir playbackSequence als ein Array definieren, das Sätze von Clips enthält. BPM wird standardmäßig als Int zugewiesen.

 type Playback = Playing | Stopped type alias PlaybackPosition = Int

Obwohl es etwas mehr Overhead gibt, mit Typen zu beginnen, ist unser Code viel besser wartbar. Es ist selbstdokumentierend und verwendet eine allgegenwärtige Sprache mit unserem Geschäftsmodell. Das Vertrauen, das wir gewinnen, wenn wir wissen, dass unsere zukünftigen Funktionen mit unseren Daten auf eine Weise interagieren, die wir erwarten, ohne dass Tests erforderlich sind, ist die Zeit wert, die zum Schreiben einer Anmerkung benötigt wird. Und wir könnten uns auf die Typinferenz des Compilers verlassen, um die Typen vorzuschlagen, sodass das Schreiben so einfach wie Kopieren und Einfügen ist. Hier ist die vollständige Typdeklaration.

Verwenden der Elm-Architektur

Die Elm-Architektur ist ein einfaches Zustandsverwaltungsmuster, das in der Sprache natürlich entstanden ist. Es schafft Fokus auf das Geschäftsmodell und ist hochgradig skalierbar. Im Gegensatz zu anderen SPA-Frameworks ist Elm eigensinnig in Bezug auf seine Architektur – es ist die Art und Weise, wie alle Anwendungen strukturiert sind, was das Onboarding zum Kinderspiel macht. Die Architektur besteht aus drei Teilen:

  • Das Modell , das den Status der Anwendung enthält, und die Struktur, die wir als Alias- Modell eingeben
  • Die Update -Funktion, die den Status aktualisiert
  • Und die Ansichtsfunktion , die den Zustand visuell wiedergibt

Beginnen wir mit dem Bau unseres Drum-Sequenzers und lernen dabei die Elm-Architektur in der Praxis kennen. Wir beginnen damit, unsere Anwendung zu initialisieren, die Ansicht zu rendern und dann den Anwendungsstatus zu aktualisieren. Da ich einen Ruby-Hintergrund habe, neige ich dazu, kürzere Dateien zu bevorzugen und meine Elm-Funktionen in Module aufzuteilen, obwohl es sehr normal ist, große Elm-Dateien zu haben. Ich habe einen Ausgangspunkt auf Ellie erstellt, aber lokal habe ich die folgenden Dateien erstellt:

  • Types.elm, die alle Typdefinitionen enthält
  • Main.elm, das das Programm initialisiert und ausführt
  • Update.elm, enthält die Update-Funktion, die den Status verwaltet
  • View.elm, enthält Elm-Code zum Rendern in HTML

Initialisieren unserer Anwendung

Es ist am besten, klein anzufangen, also reduzieren wir das Modell, um uns darauf zu konzentrieren, eine einzelne Spur zu bauen, die Schritte enthält, die ein- und ausschalten. Wir glauben zwar bereits , dass wir die gesamte Datenstruktur kennen, aber wenn wir klein anfangen, können wir uns auf das Rendern von Spuren als HTML konzentrieren. Es reduziert die Komplexität und den You Ain't Gonna Need It-Code. Später führt uns der Compiler durch das Refactoring unseres Modells. In der Types.elm-Datei behalten wir unsere Step- und Clip-Typen bei, ändern aber das Modell und die Spur.

 type alias Model = { track : Track } type alias Track = { name : String , sequence : Array Step } type Step = On | Off type alias Clip = String

Um Elm als HTML zu rendern, verwenden wir das Elm Html-Paket. Es bietet Optionen zum Erstellen von drei Arten von Programmen, die aufeinander aufbauen:

  • Anfängerprogramm
    Ein reduziertes Programm, das Nebenwirkungen ausschließt und besonders zum Erlernen der Elm-Architektur geeignet ist.
  • Programm
    Das Standardprogramm, das Nebeneffekte handhabt, nützlich für die Arbeit mit Datenbanken oder Tools, die außerhalb von Elm existieren.
  • Programm mit Fahnen
    Ein erweitertes Programm, das sich selbst mit echten Daten anstelle von Standarddaten initialisieren kann.

Es empfiehlt sich, einen möglichst einfachen Programmtyp zu verwenden, da dieser später mit dem Compiler leicht geändert werden kann. Dies ist eine gängige Praxis beim Programmieren in Elm; Verwenden Sie nur das, was Sie brauchen, und ändern Sie es später. Wir wissen, dass wir für unsere Zwecke mit JavaScript umgehen müssen, das als Nebeneffekt betrachtet wird, also erstellen wir ein Html.program . In Main.elm müssen wir das Programm initialisieren, indem wir Funktionen an seine Felder übergeben.

 main : Program Never Model Msg main = Html.program { init = init , view = view , update = update , subscriptions = always Sub.none }

Jedes Feld im Programm übergibt eine Funktion an die Elm Runtime, die unsere Anwendung steuert. Kurz gesagt, die Elm Runtime:

  • Startet das Programm mit unseren Anfangswerten von init .
  • Rendert die erste Ansicht, indem unser initialisiertes Modell in view übergeben wird.
  • Rendert die Ansicht kontinuierlich neu, wenn Nachrichten von Ansichten, Befehlen oder Abonnements an die update übergeben werden.

Lokal werden unsere view und update jeweils aus View.elm und Update.elm importiert, und wir werden diese gleich erstellen. subscriptions warten auf Nachrichten, um Aktualisierungen zu verursachen, aber im Moment ignorieren wir sie, indem wir always Sub.none . Unsere erste Funktion, init , initialisiert das Modell. Stellen Sie init wie die Standardwerte für das erste Laden vor. Wir definieren es mit einer einzelnen Spur namens „Kick“ und einer Folge von Off-Schritten. Da wir keine asynchronen Daten erhalten, ignorieren wir ausdrücklich Befehle mit Cmd.none , um ohne Seiteneffekte zu initialisieren.

 init : ( Model, Cmd.Cmd Msg ) init = ( { track = { sequence = Array.initialize 16 (always Off) , name = "Kick" } } , Cmd.none )

Unsere Init-Type-Anmerkung passt zu unserem Programm. Es ist eine Datenstruktur namens Tupel, die eine feste Anzahl von Werten enthält. In unserem Fall das Model und die Befehle. Im Moment ignorieren wir Befehle immer, indem wir Cmd.none verwenden, bis wir bereit sind, später mit Nebenwirkungen umzugehen. Unsere App rendert nichts, aber sie kompiliert!

Rendern unserer Anwendung

Lassen Sie uns unsere Ansichten aufbauen. An diesem Punkt hat unser Modell eine einzelne Spur, das ist also das Einzige, was wir rendern müssen. Die HTML-Struktur sollte wie folgt aussehen:

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

Wir werden drei Funktionen erstellen, um unsere Ansichten zu rendern:

  1. Eine zum Rendern einer einzelnen Spur, die den Spurnamen und die Sequenz enthält
  2. Ein weiterer, um die Sequenz selbst zu rendern
  3. Und eine weitere, um jede einzelne Step-Schaltfläche innerhalb der Sequenz zu rendern

Unsere erste Ansichtsfunktion rendert eine einzelne Spur. Wir verlassen uns auf unsere Typanmerkung, renderTrack : Track -> Html Msg , um eine einzelne Spur zu erzwingen, die durchlaufen wird. Die Verwendung von Typen bedeutet, dass wir immer wissen, dass renderTrack eine Spur haben wird. Wir müssen nicht prüfen, ob das name im Datensatz vorhanden ist oder ob wir anstelle eines Datensatzes eine Zeichenfolge übergeben haben. Elm wird nicht kompiliert, wenn wir versuchen, etwas anderes als Track an renderTrack zu übergeben. Noch besser, wenn wir einen Fehler machen und versehentlich versuchen, etwas anderes als einen Track an die Funktion zu übergeben, gibt uns der Compiler freundliche Meldungen, die uns in die richtige Richtung weisen.

 renderTrack : Track -> Html Msg renderTrack track = div [ class "track" ] [ p [ class "track-title" ] [ text track.name ] , div [ class "track-sequence" ] (renderSequence track.sequence) ]

Es mag offensichtlich erscheinen, aber alles Elm ist Elm, einschließlich des Schreibens von HTML. Es gibt keine Vorlagensprache oder Abstraktion, um HTML zu schreiben – es ist alles Elm. HTML-Elemente sind Elm-Funktionen, die den Namen, eine Liste von Attributen und eine Liste von untergeordneten Elementen annehmen. Also gibt div [ class "track" ] [] <div class="track"></div> . Listen sind in Elm durch Kommas getrennt, sodass das Hinzufügen einer ID zum div wie folgt aussehen würde div [ class "track", id "my-id" ] [] .

Die Div Wrapping track-sequence übergibt die Sequenz des Tracks an unsere zweite Funktion, renderSequence . Es nimmt eine Sequenz und gibt eine Liste von HTML-Schaltflächen zurück. Wir könnten renderSequence in renderTrack behalten, um die zusätzliche Funktion zu überspringen, aber ich finde es viel einfacher, Funktionen in kleinere Teile zu zerlegen. Darüber hinaus erhalten wir eine weitere Möglichkeit, eine strengere Typannotation zu definieren.

 renderSequence : Array Step -> List (Html Msg) renderSequence sequence = Array.indexedMap renderStep sequence |> Array.toList

Wir bilden jeden Schritt in der Sequenz ab und übergeben ihn an die renderStep Funktion. In JavaScript würde die Zuordnung mit einem Index wie folgt geschrieben:

 sequence.map((node, index) => renderStep(index, node))

Im Vergleich zu JavaScript ist das Mapping in Elm fast umgekehrt. Wir rufen Array.indexedMap auf, das zwei Argumente akzeptiert: die in der Karte anzuwendende Funktion ( renderStep ) und das zuzuordnende Array ( sequence ). renderStep ist unsere letzte Funktion und bestimmt, ob eine Schaltfläche aktiv oder inaktiv ist. Wir verwenden indexedMap , weil wir den Schrittindex (den wir als ID verwenden) an den Schritt selbst weitergeben müssen, um ihn an die Update-Funktion zu übergeben.

 renderStep : Int -> Step -> Html Msg renderStep index step = let classes = if step == On then "step _active" else "step" in button [ class classes ] []

renderStep akzeptiert den Index als erstes Argument, den Schritt als zweites und gibt gerenderten HTML-Code zurück. Unter Verwendung eines let...in -Blocks zum Definieren lokaler Funktionen weisen wir On Steps die Klasse _active zu und rufen unsere Klassenfunktion in der Liste der Schaltflächenattribute auf.

Die Kick-Spur, die eine Abfolge von Schritten enthält
Die Kick-Spur, die eine Abfolge von Schritten enthält

Anwendungsstatus aktualisieren

An diesem Punkt rendert unsere App die 16 Schritte in der Kick-Sequenz, aber durch Klicken wird der Schritt nicht aktiviert. Um den Schrittzustand zu aktualisieren, müssen wir eine Nachricht ( Msg ) an die Update-Funktion übergeben. Dazu definieren wir eine Nachricht und hängen sie an einen Event-Handler für unsere Schaltfläche an.

In Types.elm müssen wir unsere erste Nachricht ToggleStep definieren. Es wird ein Int für den Sequenzindex und einen Step . Als Nächstes hängen wir in renderStep die Nachricht ToggleStep an das On-Click-Ereignis der Schaltfläche an, zusammen mit dem Sequenzindex und dem Schritt als Argumente. Dadurch wird die Nachricht an unsere Update-Funktion gesendet, aber zu diesem Zeitpunkt wird das Update noch nichts bewirken.

 type Msg = ToggleStep Int Step renderStep index step = let ... in button [ onClick (ToggleStep index step) , class classes ] []

Nachrichten sind reguläre Typen, aber wir haben sie als den Typ definiert, der Aktualisierungen verursacht, was die Konvention in Elm ist. In Update.elm folgen wir der Elm-Architektur, um die Modellzustandsänderungen zu handhaben. Unsere Aktualisierungsfunktion nimmt eine Msg und das aktuelle Model und gibt ein neues Modell und möglicherweise einen Befehl zurück. Befehle handhaben Nebeneffekte, die wir uns in Teil zwei ansehen werden. Wir wissen, dass wir mehrere Msg -Typen haben werden, also richten wir einen Case-Block mit Mustervergleich ein. Dies zwingt uns, alle unsere Fälle zu bearbeiten und gleichzeitig den Statusfluss zu trennen. Und der Compiler stellt sicher, dass wir keine Fälle übersehen, die unser Modell ändern könnten.

Das Aktualisieren eines Datensatzes in Elm erfolgt etwas anders als das Aktualisieren eines Objekts in JavaScript. Wir können ein Feld im Datensatz nicht direkt wie record.field = * , da wir this oder self nicht verwenden können, aber Elm hat eingebaute Helfer. Bei einem Datensatz wie brian = { name = "brian" } können wir das Namensfeld wie { brian | name = "BRIAN" } aktualisieren { brian | name = "BRIAN" } . Das Format folgt { record | field = newValue } { record | field = newValue } .

So aktualisieren Sie Felder der obersten Ebene, aber verschachtelte Felder sind in Elm schwieriger. Wir müssen unsere eigenen Hilfsfunktionen definieren, also definieren wir vier Hilfsfunktionen, um in verschachtelte Datensätze einzutauchen:

  1. Einer zum Umschalten des Schrittwerts
  2. Eine, um eine neue Sequenz zurückzugeben, die den aktualisierten Schrittwert enthält
  3. Ein weiterer, um auszuwählen, zu welcher Spur die Sequenz gehört
  4. Und eine letzte Funktion, um eine neue Spur zurückzugeben, die die aktualisierte Sequenz enthält, die den aktualisierten Schrittwert enthält

Wir beginnen mit ToggleStep , um den Schrittwert der Spursequenz zwischen Ein und Aus umzuschalten. Wir verwenden wieder einen let...in -Block, um kleinere Funktionen innerhalb der case-Anweisung zu erstellen. Wenn der Schritt bereits Aus ist, machen wir ihn Ein und umgekehrt.

 toggleStep = if step == Off then On else Off

toggleStep wird von newSequence . Daten sind in funktionalen Sprachen unveränderlich. Anstatt also die Sequenz zu ändern, erstellen wir tatsächlich eine neue Sequenz mit einem aktualisierten Schrittwert, um den alten zu ersetzen.

 newSequence = Array.set index toggleStep selectedTrack.sequence

newSequence verwendet Array.set , um den Index zu finden, den wir umschalten möchten, und erstellt dann die neue Sequenz. Wenn set den Index nicht findet, gibt es die gleiche Sequenz zurück. Es verlässt sich auf selectedTrack.sequence , um zu wissen, welche Sequenz geändert werden soll. selectedTrack ist unsere wichtigste Hilfsfunktion, die verwendet wird, damit wir in unseren verschachtelten Datensatz gelangen können. An dieser Stelle ist es überraschend einfach, denn unser Modell hat nur eine einzige Spur.

 selectedTrack = model.track

Unsere letzte Hilfsfunktion verbindet alle anderen. Da Daten unveränderlich sind, ersetzen wir wiederum unsere gesamte Spur durch eine neue Spur, die eine neue Sequenz enthält.

 newTrack = { selectedTrack | sequence = newSequence }

newTrack wird außerhalb des let...in -Blocks aufgerufen, wo wir ein neues Modell zurückgeben, das die neue Spur enthält, die die Ansicht neu rendert. Wir geben keine Nebeneffekte weiter, also verwenden wir wieder Cmd.none . Unsere gesamte update Funktion sieht so aus:

 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 )

Wenn wir unser Programm ausführen, sehen wir einen gerenderten Track mit einer Reihe von Schritten. Wenn Sie auf eine der Schrittschaltflächen klicken, wird ToggleStep , das auf unsere Aktualisierungsfunktion trifft, um den Modellzustand zu ersetzen.

Die Kick-Spur, die eine Folge aktiver Schritte enthält
Die Kick-Spur, die eine Folge aktiver Schritte enthält

Wenn unsere Anwendung skaliert, werden wir sehen, wie das wiederholbare Muster der Elm-Architektur die Handhabung des Zustands vereinfacht. Die Vertrautheit mit den Modell-, Aktualisierungs- und Ansichtsfunktionen hilft uns, uns auf unsere Geschäftsdomäne zu konzentrieren, und erleichtert den Einstieg in die Elm-Anwendung eines anderen.

Eine Pause machen

Das Schreiben in einer neuen Sprache erfordert Zeit und Übung. Die ersten Projekte, an denen ich arbeitete, waren einfache TypeForm-Klone, mit denen ich die Elm-Syntax, die Architektur und Paradigmen der funktionalen Programmierung lernte. An diesem Punkt haben Sie bereits genug gelernt, um etwas Ähnliches zu tun. Wenn Sie gespannt sind, empfehle ich Ihnen, den offiziellen Leitfaden „Erste Schritte“ durchzuarbeiten. Evan, der Schöpfer von Elm, führt Sie anhand praktischer Beispiele durch Motivationen für Elm, Syntax, Typen, die Elm-Architektur, Skalierung und mehr.

In Teil zwei tauchen wir in eine der besten Funktionen von Elm ein: die Verwendung des Compilers zum Refactoring unseres Step-Sequencers. Außerdem lernen wir, wie man mit wiederkehrenden Ereignissen umgeht, Befehle für Nebeneffekte verwendet und mit JavaScript interagiert. Bleib dran!