Imparare l'olmo da un sequencer di batteria (parte 1)

Pubblicato: 2022-03-10
Riassunto rapido ↬ Lo sviluppatore front-end Brian Holt guida i lettori nella creazione di un sequencer di batteria in Elm. Nella prima parte di questa serie in due parti, introduce la sintassi, l'impostazione e i concetti fondamentali di Elm. Imparerai come lavorare con l'architettura Elm per creare semplici applicazioni.

Se sei uno sviluppatore front-end che segue l'evoluzione delle applicazioni a pagina singola (SPA), è probabile che tu abbia sentito parlare di Elm, il linguaggio funzionale che ha ispirato Redux. In caso contrario, è un linguaggio di compilazione in JavaScript paragonabile a progetti SPA come React, Angular e Vue.

Come quelli, gestisce i cambiamenti di stato attraverso il suo dom virtuale con l'obiettivo di rendere il codice più manutenibile e performante. Si concentra sulla felicità degli sviluppatori, su strumenti di alta qualità e modelli semplici e ripetibili. Alcune delle sue differenze chiave includono la tipizzazione statica, messaggi di errore meravigliosamente utili e che si tratta di un linguaggio funzionale (al contrario di Object-Oriented).

La mia introduzione è arrivata attraverso un discorso tenuto da Evan Czaplicki, il creatore di Elm, sulla sua visione dell'esperienza di sviluppo front-end e, a sua volta, sulla visione di Elm. Poiché qualcuno si è concentrato anche sulla manutenibilità e sull'usabilità dello sviluppo front-end, il suo discorso ha davvero risuonato con me. Ho provato Elm in un progetto collaterale un anno fa e continuo a goderne sia le caratteristiche che le sfide in un modo che non facevo da quando ho iniziato a programmare; Sono di nuovo un principiante. Inoltre, mi trovo in grado di applicare molte delle pratiche di Elm in altre lingue.

Sviluppare la consapevolezza della dipendenza

Le dipendenze sono ovunque. Riducendoli, puoi aumentare la probabilità che il tuo sito sia utilizzabile dal maggior numero di persone nella più ampia varietà di scenari.Leggi un articolo correlato →

In questo articolo in due parti, costruiremo uno step sequencer per programmare i ritmi di batteria in Elm, mostrando alcune delle migliori caratteristiche del linguaggio. Oggi illustreremo i concetti fondamentali di Elm, ovvero iniziare, utilizzare i tipi, eseguire il rendering delle viste e aggiornare lo stato. La seconda parte di questo articolo approfondirà quindi argomenti più avanzati, come gestire facilmente refactor di grandi dimensioni, impostare eventi ricorrenti e interagire con JavaScript.

Gioca con il progetto finale qui e controlla il suo codice qui.

sequenziatore di passi in azione
Ecco come apparirà in azione lo step sequencer completato.

Iniziare con Elm

Per seguire questo articolo, ti consiglio di utilizzare Ellie, un'esperienza per sviluppatori Elm nel browser. Non è necessario installare nulla per eseguire Ellie e puoi sviluppare applicazioni completamente funzionali al suo interno. Se preferisci installare Elm sul tuo computer, il modo migliore per eseguire la configurazione è seguire la guida introduttiva ufficiale.

Altro dopo il salto! Continua a leggere sotto ↓

In questo articolo, mi collegherò alle versioni di Ellie in lavorazione, anche se ho sviluppato il sequencer localmente. E mentre i CSS possono essere scritti interamente in Elm, ho scritto questo progetto in PostCSS. Ciò richiede un po' di configurazione dell'Elm Reactor per lo sviluppo locale in modo da caricare gli stili. Per motivi di brevità, non toccherò gli stili in questo articolo, ma i collegamenti Ellie includono tutti gli stili CSS ridotti.

Elm è un ecosistema autonomo che include:

  • Olmo Make
    Per compilare il tuo codice Elm. Sebbene Webpack sia ancora popolare per la produzione di progetti Elm insieme ad altre risorse, non è necessario. In questo progetto, ho scelto di escludere Webpack e fare affidamento su elm make per compilare il codice.
  • Pacchetto Olmo
    Un gestore di pacchetti paragonabile a NPM per l'utilizzo di pacchetti/moduli creati dalla comunità.
  • Reattore dell'olmo
    Per eseguire un server di sviluppo a compilazione automatica. Più degno di nota, include il Time Traveling Debugger che semplifica l'esplorazione degli stati dell'applicazione e la riproduzione dei bug.
  • Elm Sost
    Per scrivere o testare semplici espressioni Elm nel terminale.

Tutti i file Elm sono considerati modules . Le righe iniziali di qualsiasi file includeranno il module FileName exposing (functions) dove FileName è il nome file letterale e le functions sono le funzioni pubbliche che si desidera rendere accessibili ad altri moduli. Immediatamente dopo la definizione del modulo ci sono le importazioni da moduli esterni. Segue il resto delle funzioni.

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

Questo modulo, denominato Main.elm , espone una singola funzione, main e importa Html e text dal modulo/pacchetto Html . La funzione main è composta da due parti: l' annotazione del tipo e la funzione vera e propria. Le annotazioni di tipo possono essere considerate definizioni di funzioni. Indicano i tipi di argomento e il tipo restituito. In questo caso, il nostro afferma che la funzione main non accetta argomenti e restituisce Html msg . La funzione stessa esegue il rendering di un nodo di testo contenente "Hello, World". Per passare argomenti a una funzione, aggiungiamo nomi separati da spazi prima del segno di uguale nella funzione. Aggiungiamo anche i tipi di argomento all'annotazione del tipo, nell'ordine degli argomenti, seguito da una freccia.

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

In JavaScript, una funzione come questa è paragonabile:

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

E in un linguaggio tipizzato, come TypeScript, sembra:

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

add2Numbers prende due numeri interi e restituisce un intero. L'ultimo valore nell'annotazione è sempre il valore restituito perché ogni funzione deve restituire un valore. Chiamiamo add2Numbers con 2 e 3 per ottenere 5 come add2Numbers 2 3 .

Proprio come leghi i componenti React, abbiamo bisogno di associare il codice Elm compilato al DOM. Il modo standard per associare è chiamare embed() sul nostro modulo e passarci l'elemento DOM.

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

Sebbene la nostra app in realtà non faccia nulla, abbiamo abbastanza per compilare il nostro codice Elm e renderizzare il testo. Dai un'occhiata su Ellie e prova a cambiare gli argomenti in add2Numbers alla riga 26.

L'app Elm rende i numeri aggiunti
La nostra semplice app Elm che visualizza i numeri aggiunti sullo schermo.

Modellazione dei dati con i tipi

Provenienti da un linguaggio tipizzato dinamicamente come JavaScript o Ruby, i tipi possono sembrare superflui. Tali linguaggi determinano quali funzioni di tipo prendono dal valore passato durante il runtime. La scrittura di funzioni è generalmente considerata più veloce, ma perdi la sicurezza di garantire che le tue funzioni possano interagire correttamente tra loro.

Al contrario, Elm è tipizzato staticamente. Si basa sul suo compilatore per garantire che i valori passati alle funzioni siano compatibili prima del runtime. Ciò significa nessuna eccezione di runtime per i tuoi utenti ed è così che Elm può garantire "nessuna eccezione di runtime". Laddove gli errori di digitazione in molti compilatori possono essere particolarmente criptici, Elm si concentra sul renderli facili da comprendere e correggere.

Elm rende molto amichevole iniziare con i tipi. In effetti, l'inferenza del tipo di Elm è così buona che puoi saltare la scrittura delle annotazioni finché non ti senti più a tuo agio con esse. Se sei nuovo di zecca ai tipi, ti consiglio di fare affidamento sui suggerimenti del compilatore piuttosto che provare a scriverli da solo.

Iniziamo a modellare i nostri dati usando i tipi. Il nostro step sequencer è una sequenza temporale visiva di quando dovrebbe suonare un particolare campione di batteria. La timeline è composta da tracce , ciascuna assegnata con uno specifico campione di batteria e la sequenza di passaggi . Un passo può essere considerato un momento o un battito. Se uno step è attivo , il campione dovrebbe essere attivato durante la riproduzione e se lo step è inattivo , il campione dovrebbe rimanere silenzioso. Durante la riproduzione, il sequencer si sposterà attraverso ogni step riproducendo i campioni degli step attivi. La velocità di riproduzione è impostata da Beats Per Minute (BPM) .

applicazione finale composta da tracce con sequenze di passaggi
Uno screenshot della nostra applicazione finale, composta da tracce con sequenze di passaggi.

Modellazione della nostra applicazione in JavaScript

Per avere un'idea migliore dei nostri tipi, consideriamo come modellare questo sequencer di batteria in JavaScript. Ci sono una serie di tracce. Ogni oggetto traccia contiene informazioni su se stesso: il nome della traccia, il campione/clip che si attiverà e la sequenza di valori di passaggio.

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

Dobbiamo gestire lo stato di riproduzione tra la riproduzione e l'arresto.

 playback: "playing" || "stopped"

Durante la riproduzione, dobbiamo determinare quale passaggio deve essere riprodotto. Dovremmo anche considerare le prestazioni di riproduzione e piuttosto che attraversare ogni sequenza in ogni traccia ogni volta che viene incrementato un passo; dovremmo ridurre tutti i passaggi attivi in ​​un'unica sequenza di riproduzione. Ciascuna raccolta all'interno della sequenza di riproduzione rappresenta tutti i campioni che devono essere riprodotti. Ad esempio, ["kick", "hat"] significa che dovrebbero suonare i campioni di kick e charleston, mentre ["hat"] significa che dovrebbe suonare solo il charleston. Abbiamo anche bisogno che ogni collezione limiti l'unicità al campione, quindi non finiamo con qualcosa come ["hat", "hat", "hat"] .

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

E abbiamo bisogno di impostare il ritmo di riproduzione, o il BPM.

 bpm: 120

Modellazione con tipi in olmo

Trascrivere questi dati in tipi Elm significa essenzialmente descrivere di cosa ci aspettiamo che siano fatti i nostri dati. Ad esempio, ci riferiamo già al nostro modello di dati come model , quindi lo chiamiamo con un alias di tipo. Gli alias di tipo vengono utilizzati per semplificare la lettura del codice. Non sono un tipo primitivo come un booleano o un intero; sono semplicemente nomi che diamo a un tipo primitivo o a una struttura dati. Usandone uno, definiamo tutti i dati che seguono la nostra struttura del modello come un modello piuttosto che come una struttura anonima. In molti progetti Elm, la struttura principale è denominata Model.

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

Sebbene il nostro modello assomigli un po' a un oggetto JavaScript, descrive un Elm Record. I record vengono utilizzati per organizzare i dati correlati in diversi campi che dispongono di annotazioni di tipo proprio. Sono di facile accesso utilizzando field.attribute e facili da aggiornare che vedremo più avanti. Oggetti e record sono molto simili, con alcune differenze fondamentali:

  • Non è possibile richiamare campi inesistenti
  • I campi non saranno mai null o undefined
  • this e self non possono essere usati

La nostra raccolta di tracce può essere composta da uno dei tre tipi possibili: List, Array e Set. In breve, gli elenchi sono raccolte di uso generale non indicizzate, gli array sono indicizzati e gli insiemi contengono solo valori univoci. Abbiamo bisogno di un indice per sapere quale passo della traccia è stato attivato e poiché gli array sono indicizzati, è la nostra scelta migliore. In alternativa, potremmo aggiungere un id alla traccia e filtrare da un elenco.

Nel nostro modello, abbiamo composto le tracce su un array di track , un altro record: tracks : Array Track . La traccia contiene le informazioni su se stessa. Sia il nome che la clip sono stringhe, ma abbiamo digitato clip con alias perché sappiamo che verrà referenziato altrove nel codice da altre funzioni. Con l'alias, iniziamo a creare codice auto-documentante. La creazione di tipi e alias di tipo consente agli sviluppatori di modellare il modello di dati sul modello di business, creando un linguaggio onnipresente.

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

Sappiamo che la sequenza sarà un array di valori on/off. Potremmo impostarlo come un array di booleani, come sequence : Array Bool , ma perderemmo un'opportunità per esprimere il nostro modello di business! Considerando che gli step sequencer sono fatti di step , definiamo un nuovo tipo chiamato Step . Un Step potrebbe essere un alias di tipo per un boolean , ma possiamo fare un ulteriore passo avanti: Steps ha due valori possibili, on e off, quindi è così che definiamo il tipo di unione. Ora i passaggi possono essere attivati ​​o disattivati, rendendo impossibili tutti gli altri stati.

Definiamo un altro tipo per Playback , un alias per PlaybackPosition e utilizziamo Clip quando definiamo playbackSequence come un array contenente set di clip. BPM è assegnato come standard Int .

 type Playback = Playing | Stopped type alias PlaybackPosition = Int

Sebbene ci sia un po' più di sovraccarico nell'iniziare con i tipi, il nostro codice è molto più gestibile. Si auto-documenta e utilizza un linguaggio onnipresente con il nostro modello di business. La fiducia che acquisiamo sapendo che le nostre funzioni future interagiranno con i nostri dati nel modo previsto, senza richiedere test, vale il tempo necessario per scrivere un'annotazione. E potremmo fare affidamento sull'inferenza del tipo del compilatore per suggerire i tipi, quindi scriverli è semplice come copiare e incollare. Ecco la dichiarazione di tipo completa.

Usando l'architettura dell'olmo

L'architettura Elm è un semplice modello di gestione dello stato che è emerso naturalmente nel linguaggio. Crea focus attorno al modello di business ed è altamente scalabile. A differenza di altri framework SPA, Elm è convinto della sua architettura: è il modo in cui sono strutturate tutte le applicazioni, il che rende l'onboarding un gioco da ragazzi. L'architettura si compone di tre parti:

  • Il modello , contenente lo stato dell'applicazione, e la struttura che digitiamo alias model
  • La funzione di aggiornamento , che aggiorna lo stato
  • E la funzione di visualizzazione , che rende visivamente lo stato

Iniziamo a costruire il nostro sequencer di batteria imparando l'architettura Elm in pratica mentre procediamo. Inizieremo inizializzando la nostra applicazione, eseguendo il rendering della vista, quindi aggiornando lo stato dell'applicazione. Provenendo da uno sfondo Ruby, tendo a preferire file più brevi e suddividere le mie funzioni Elm in moduli anche se è molto normale avere file Elm di grandi dimensioni. Ho creato un punto di partenza su Ellie, ma localmente ho creato i seguenti file:

  • Types.elm, contenente tutte le definizioni dei tipi
  • Main.elm, che inizializza ed esegue il programma
  • Update.elm, contenente la funzione di aggiornamento che gestisce lo stato
  • View.elm, contenente il codice Elm per il rendering in HTML

Inizializzazione della nostra applicazione

È meglio iniziare in piccolo, quindi riduciamo il modello per concentrarci sulla costruzione di una singola traccia contenente passaggi che si attivano e disattivano. Anche se pensiamo già di conoscere l'intera struttura dei dati, iniziare in piccolo ci consente di concentrarci sul rendering delle tracce come HTML. Riduce la complessità e il codice You Ain't Gonna Need It. Successivamente, il compilatore ci guiderà attraverso il refactoring del nostro modello. Nel file Types.elm, manteniamo i nostri tipi Step e Clip ma cambiamo il modello e la traccia.

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

Per rendere Elm come HTML, utilizziamo il pacchetto Elm Html. Ha opzioni per creare tre tipi di programmi che si basano l'uno sull'altro:

  • Programma per principianti
    Un programma ridotto che esclude gli effetti collaterali ed è particolarmente utile per l'apprendimento dell'architettura Elm.
  • Programma
    Il programma standard che gestisce gli effetti collaterali, utile per lavorare con database o strumenti che esistono al di fuori di Elm.
  • Programma con le bandiere
    Un programma esteso che può inizializzarsi con dati reali anziché con dati predefiniti.

È buona norma utilizzare il tipo di programma più semplice possibile perché è facile modificarlo in seguito con il compilatore. Questa è una pratica comune durante la programmazione in Elm; usa solo ciò che ti serve e cambialo in seguito. Per i nostri scopi, sappiamo che dobbiamo occuparci di JavaScript, che è considerato un effetto collaterale, quindi creiamo un Html.program . In Main.elm abbiamo bisogno di inizializzare il programma passando le funzioni ai suoi campi.

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

Ogni campo del programma passa una funzione a Elm Runtime, che controlla la nostra applicazione. In poche parole, Elm Runtime:

  • Avvia il programma con i nostri valori iniziali da init .
  • Rende la prima vista passando in view il nostro modello inizializzato.
  • Esegue continuamente il rendering della vista quando i messaggi vengono passati per l' update da viste, comandi o sottoscrizioni.

A livello locale, le nostre funzioni di view e update verranno importate rispettivamente da View.elm e Update.elm e le creeremo tra poco. le subscriptions ascoltano i messaggi per causare aggiornamenti, ma per ora li ignoriamo assegnando always Sub.none . La nostra prima funzione, init , inizializza il modello. Pensa a init come ai valori predefiniti per il primo caricamento. Lo definiamo con un'unica traccia denominata “kick” e una sequenza di passi Off. Poiché non riceviamo dati asincroni, ignoriamo esplicitamente i comandi con Cmd.none per inizializzare senza effetti collaterali.

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

La nostra annotazione del tipo init corrisponde al nostro programma. È una struttura dati chiamata tupla, che contiene un numero fisso di valori. Nel nostro caso, il Model ei comandi. Per ora, ignoriamo sempre i comandi usando Cmd.none finché non siamo pronti a gestire gli effetti collaterali in un secondo momento. La nostra app non esegue il rendering, ma compila!

Rendering della nostra applicazione

Costruiamo le nostre opinioni. A questo punto, il nostro modello ha una singola traccia, quindi è l'unica cosa che dobbiamo renderizzare. La struttura HTML dovrebbe essere simile a:

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

Costruiremo tre funzioni per rendere le nostre viste:

  1. Uno per eseguire il rendering di una singola traccia, che contiene il nome della traccia e la sequenza
  2. Un altro per rendere la sequenza stessa
  3. E un altro per eseguire il rendering di ogni singolo pulsante di passaggio all'interno della sequenza

La nostra prima funzione di visualizzazione renderà una singola traccia. Facciamo affidamento sulla nostra annotazione di tipo, renderTrack : Track -> Html Msg , per imporre una singola traccia passata. L'uso dei tipi significa che sappiamo sempre che renderTrack avrà una traccia. Non è necessario verificare se il campo del name esiste nel record o se abbiamo passato una stringa anziché un record. Elm non verrà compilato se proviamo a passare qualcosa di diverso da Track a renderTrack . Ancora meglio, se commettiamo un errore e proviamo accidentalmente a passare qualcosa di diverso da una traccia alla funzione, il compilatore ci darà messaggi amichevoli per indicarci la giusta direzione.

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

Potrebbe sembrare ovvio, ma tutto Elm è Elm, inclusa la scrittura di HTML. Non esiste un linguaggio di creazione di modelli o un'astrazione per scrivere HTML: è tutto Elm. Gli elementi HTML sono funzioni Elm, che prendono il nome, un elenco di attributi e un elenco di figli. Quindi div [ class "track" ] [] restituisce <div class="track"></div> . Gli elenchi sono separati da virgole in Elm, quindi l'aggiunta di un id al div sembrerebbe div [ class "track", id "my-id" ] [] .

Il div wrapping track-sequence passa la sequenza della traccia alla nostra seconda funzione, renderSequence . Prende una sequenza e restituisce un elenco di pulsanti HTML. Potremmo mantenere renderSequence in renderTrack per saltare la funzione aggiuntiva, ma trovo molto più facile suddividere le funzioni in parti più piccole su cui ragionare. Inoltre, abbiamo un'altra opportunità per definire un'annotazione di tipo più rigorosa.

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

Mappiamo su ogni passaggio della sequenza e lo passiamo alla funzione renderStep . In JavaScript la mappatura con un indice verrebbe scritta come:

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

Rispetto a JavaScript, la mappatura in Elm è quasi invertita. Chiamiamo Array.indexedMap , che accetta due argomenti: la funzione da applicare nella mappa ( renderStep ) e l'array su cui mappare ( sequence ). renderStep è la nostra ultima funzione e determina se un pulsante è attivo o inattivo. Usiamo indexedMap perché dobbiamo passare l'indice del passo (che usiamo come ID) al passo stesso per passarlo alla funzione di aggiornamento.

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

renderStep accetta l'indice come primo argomento, il passaggio come secondo e restituisce l'HTML renderizzato. Usando un blocco let...in per definire le funzioni locali, assegniamo la classe _active a On Steps e chiamiamo la nostra funzione classi nell'elenco degli attributi del pulsante.

La traccia kick contenente una sequenza di passaggi
La traccia kick contenente una sequenza di passaggi

Aggiornamento dello stato dell'applicazione

A questo punto, la nostra app esegue il rendering dei 16 passaggi nella sequenza del calcio, ma il clic non attiva il passaggio. Per aggiornare lo stato del passaggio, dobbiamo passare un messaggio ( Msg ) alla funzione di aggiornamento. Lo facciamo definendo un messaggio e allegandolo a un gestore di eventi per il nostro pulsante.

In Types.elm, dobbiamo definire il nostro primo messaggio, ToggleStep . Ci vorrà un Int per l'indice della sequenza e uno Step . Successivamente, in renderStep , alleghiamo il messaggio ToggleStep button's on click, insieme all'indice di sequenza e allo step come argomenti. Questo invierà il messaggio alla nostra funzione di aggiornamento, ma a questo punto l'aggiornamento in realtà non farà nulla.

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

I messaggi sono tipi normali, ma li abbiamo definiti come il tipo che causa gli aggiornamenti, che è la convenzione in Elm. In Update.elm seguiamo l'architettura Elm per gestire le modifiche allo stato del modello. La nostra funzione di aggiornamento prenderà un Msg e il Model corrente e restituirà un nuovo modello e potenzialmente un comando. I comandi gestiscono gli effetti collaterali, che esamineremo nella seconda parte. Sappiamo che avremo più tipi di Msg , quindi abbiamo impostato un case block di corrispondenza dei modelli. Questo ci costringe a gestire tutti i nostri casi separando anche il flusso di stato. E il compilatore sarà sicuro di non perdere nessun caso che potrebbe cambiare il nostro modello.

L'aggiornamento di un record in Elm viene eseguito in modo leggermente diverso rispetto all'aggiornamento di un oggetto in JavaScript. Non possiamo modificare direttamente un campo nel record come record.field = * perché non possiamo usare this o self , ma Elm ha degli helper integrati. Dato un record come brian = { name = "brian" } , possiamo aggiornare il campo del nome come { brian | name = "BRIAN" } { brian | name = "BRIAN" } . Il formato segue { record | field = newValue } { record | field = newValue } .

Ecco come aggiornare i campi di primo livello, ma i campi nidificati sono più complicati in Elm. Dobbiamo definire le nostre funzioni di supporto, quindi definiremo quattro funzioni di supporto per approfondire i record nidificati:

  1. Uno per cambiare il valore del passo
  2. Uno per restituire una nuova sequenza, contenente il valore del passo aggiornato
  3. Un altro per scegliere a quale traccia appartiene la sequenza
  4. E un'ultima funzione per restituire una nuova traccia, contenente la sequenza aggiornata che contiene il valore del passo aggiornato

Iniziamo con ToggleStep per alternare il valore del passo della sequenza di tracce tra On e Off. Usiamo di nuovo un blocco let...in per creare funzioni più piccole all'interno dell'istruzione case. Se il passaggio è già disattivato, lo rendiamo attivo e viceversa.

 toggleStep = if step == Off then On else Off

toggleStep verrà chiamato da newSequence . I dati sono immutabili nei linguaggi funzionali, quindi invece di modificare la sequenza, stiamo effettivamente creando una nuova sequenza con un valore di passaggio aggiornato per sostituire quella precedente.

 newSequence = Array.set index toggleStep selectedTrack.sequence

newSequence usa Array.set per trovare l'indice che vogliamo attivare, quindi crea la nuova sequenza. Se set non trova l'indice, restituisce la stessa sequenza. Si basa su selectedTrack.sequence per sapere quale sequenza modificare. selectedTrack è la nostra funzione di supporto chiave utilizzata in modo da poter raggiungere il nostro record nidificato. A questo punto è sorprendentemente semplice perché il nostro modello ha un solo binario.

 selectedTrack = model.track

La nostra ultima funzione di supporto collega tutto il resto. Ancora una volta, poiché i dati sono immutabili, sostituiamo l'intera traccia con una nuova traccia che contiene una nuova sequenza.

 newTrack = { selectedTrack | sequence = newSequence }

newTrack viene chiamato all'esterno del blocco let...in , dove restituiamo un nuovo modello, contenente la nuova traccia, che esegue nuovamente il rendering della vista. Non stiamo passando gli effetti collaterali, quindi utilizziamo di nuovo Cmd.none . La nostra intera funzione di update è simile a:

 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 )

Quando eseguiamo il nostro programma, vediamo una traccia renderizzata con una serie di passaggi. Facendo clic su uno qualsiasi dei pulsanti dei passaggi si attiva ToggleStep , che attiva la nostra funzione di aggiornamento per sostituire lo stato del modello.

La traccia kick contenente una sequenza di passi attivi
La traccia kick contenente una sequenza di passi attivi

Man mano che la nostra applicazione si ridimensiona, vedremo come il modello ripetibile dell'architettura Elm renda semplice la gestione dello stato. La familiarità con le sue funzioni di modello, aggiornamento e visualizzazione ci aiuta a concentrarci sul nostro dominio aziendale e rende facile passare all'applicazione Elm di qualcun altro.

Prendere una pausa

Scrivere in una nuova lingua richiede tempo e pratica. I primi progetti su cui ho lavorato erano semplici cloni TypeForm che ho usato per imparare la sintassi di Elm, l'architettura e i paradigmi di programmazione funzionale. A questo punto, hai già imparato abbastanza per fare qualcosa di simile. Se sei impaziente, ti consiglio di consultare la Guida introduttiva ufficiale. Evan, il creatore di Elm, ti guida attraverso le motivazioni di Elm, la sintassi, i tipi, l'architettura Elm, il ridimensionamento e altro, usando esempi pratici.

Nella seconda parte ci addentreremo in una delle migliori caratteristiche di Elm: usare il compilatore per refactoring del nostro step sequencer. Inoltre, impareremo come gestire gli eventi ricorrenti, usando i comandi per gli effetti collaterali e interagendo con JavaScript. Rimani sintonizzato!