Nauka wiązu z sekwencera bębnowego (część 1)
Opublikowany: 2022-03-10Jeśli jesteś programistą front-end śledzącym ewolucję aplikacji jednostronicowych (SPA), prawdopodobnie słyszałeś o Elm, języku funkcjonalnym, który zainspirował Redux. Jeśli nie, jest to język kompilacji do JavaScript porównywalny z projektami SPA, takimi jak React, Angular i Vue.
Podobnie jak te, zarządza zmianami stanu poprzez swój wirtualny dom, aby kod był bardziej konserwowalny i wydajny. Koncentruje się na szczęściu programisty, wysokiej jakości narzędziach i prostych, powtarzalnych wzorcach. Niektóre z jego kluczowych różnic to statycznie wpisane, cudownie pomocne komunikaty o błędach oraz to, że jest to język funkcjonalny (w przeciwieństwie do zorientowanego obiektowo).
Moje wprowadzenie pojawiło się w przemówieniu wygłoszonym przez Evana Czaplickiego, twórcę Elm, na temat jego wizji front-endowego doświadczenia deweloperskiego, a co za tym idzie wizji Elm. Ponieważ ktoś skupił się również na łatwości utrzymania i użyteczności front-endowego rozwoju, jego wypowiedź naprawdę do mnie trafiła. Wypróbowałem Elm w projekcie pobocznym rok temu i nadal cieszę się zarówno jego funkcjami, jak i wyzwaniami w sposób, w jaki nie miałem odkąd zacząłem programować; Znowu jestem początkującym. Ponadto jestem w stanie zastosować wiele praktyk Elma w innych językach.
Rozwijanie świadomości zależności
Zależności są wszędzie. Zmniejszając je, możesz zwiększyć prawdopodobieństwo, że Twoja witryna będzie używana przez największą liczbę osób w najróżniejszych scenariuszach. Przeczytaj powiązany artykuł →
W tym dwuczęściowym artykule zbudujemy sekwencer krokowy do programowania uderzeń perkusji w Elm, prezentując jednocześnie niektóre z najlepszych funkcji tego języka. Dzisiaj omówimy podstawowe koncepcje w Elm, tj. rozpoczęcie pracy, używanie typów, renderowanie widoków i aktualizowanie stanu. W drugiej części tego artykułu zagłębimy się w bardziej zaawansowane tematy, takie jak łatwa obsługa dużych refaktorów, konfigurowanie powtarzających się zdarzeń i interakcja z JavaScript.
Zagraj z ostatecznym projektem tutaj i sprawdź jego kod tutaj.
Pierwsze kroki z Elm
Aby kontynuować ten artykuł, polecam korzystanie z Ellie, środowiska programistycznego Elm w przeglądarce. Nie musisz niczego instalować, aby uruchomić Ellie i możesz w nim tworzyć w pełni funkcjonalne aplikacje. Jeśli wolisz zainstalować Elm na swoim komputerze, najlepszym sposobem na skonfigurowanie jest postępowanie zgodnie z oficjalnym przewodnikiem dla początkujących.
W tym artykule będę zamieszczał linki do wersji Ellie, które są w toku, chociaż opracowałem sekwencer lokalnie. I chociaż CSS można napisać w całości w Elm, napisałem ten projekt w PostCSS. Wymaga to trochę konfiguracji w Reaktorze Elm dla lokalnego rozwoju, aby mieć załadowane style. Ze względu na zwięzłość nie będę poruszał stylów w tym artykule, ale linki Ellie zawierają wszystkie zminimalizowane style CSS.
Wiąz to samowystarczalny ekosystem, który obejmuje:
- Wiąz Make
Do kompilacji kodu Elm. Chociaż pakiet Webpack jest nadal popularny do produkcji projektów Elm wraz z innymi zasobami, nie jest wymagany. W tym projekcie zdecydowałem się wykluczyć Webpack i polegać naelm make
do kompilacji kodu. - Pakiet Wiązów
Menedżer pakietów porównywalny z NPM do korzystania z pakietów/modułów stworzonych przez społeczność. - Reaktor Wiązów
Do uruchamiania automatycznie kompilującego się serwera deweloperskiego. Co ważniejsze, zawiera debuger podróży w czasie, który ułatwia przechodzenie przez stany aplikacji i powtarzanie błędów. - Wiąz Repl
Do pisania lub testowania prostych wyrażeń Elm w terminalu.
Wszystkie pliki Elm są uważane za modules
. Początkowe wiersze każdego pliku będą zawierać module FileName exposing (functions)
, gdzie FileName
jest dosłowną nazwą pliku, a functions
są funkcjami publicznymi, które chcesz udostępnić innym modułom. Zaraz po definicji modułu następuje import z modułów zewnętrznych. Pozostałe funkcje następują.
module Main exposing (main) import Html exposing (Html, text) main : Html msg main = text "Hello, World!"
Ten moduł o nazwie Main.elm
udostępnia pojedynczą funkcję main
i importuje Html
i text
z modułu/pakietu Html
. Funkcja main
składa się z dwóch części: adnotacji typu i funkcji rzeczywistej. Adnotacje typu można traktować jako definicje funkcji. Określają typy argumentów i typ zwracany. W tym przypadku nasz stwierdza, że funkcja main
nie przyjmuje żadnych argumentów i zwraca Html msg
. Sama funkcja renderuje węzeł tekstowy zawierający „Hello, World”. Aby przekazać argumenty do funkcji, dodajemy nazwy oddzielone spacjami przed znakiem równości w funkcji. Dodajemy również typy argumentów do adnotacji typu, w kolejności argumentów, po których następuje strzałka.
add2Numbers : Int -> Int -> Int add2Numbers first second = first + second
W JavaScript funkcja taka jak ta jest porównywalna:
function add2Numbers(first, second) { return first + second; }
A w języku Typed, takim jak TypeScript, wygląda to tak:
function add2Numbers(first: number, second: number): number { return first + second; }
add2Numbers
przyjmuje dwie liczby całkowite i zwraca liczbę całkowitą. Ostatnia wartość w adnotacji jest zawsze wartością zwracaną, ponieważ każda funkcja musi zwracać wartość. add2Numbers
z 2 i 3, aby otrzymać 5, tak jak add2Numbers 2 3
.
Tak jak łączysz komponenty Reacta, tak musimy powiązać skompilowany kod Elm z DOM. Standardowym sposobem wiązania jest wywołanie funkcji embed()
w naszym module i przekazanie do niego elementu DOM.
<script> const container = document.getElementById('app'); const app = Elm.Main.embed(container); <script>
Chociaż nasza aplikacja tak naprawdę nic nie robi, mamy wystarczająco dużo, aby skompilować nasz kod Elm i renderować tekst. Sprawdź to na Ellie i spróbuj zmienić argumenty na add2Numbers
w linii 26.
Modelowanie danych z typami
Pochodzące z dynamicznie typowanego języka, takiego jak JavaScript lub Ruby, typy mogą wydawać się zbędne. Te języki określają, jakie funkcje typu pobierają z wartości przekazywanej w czasie wykonywania. Pisanie funkcji jest ogólnie uważane za szybsze, ale tracisz pewność, że Twoje funkcje mogą prawidłowo współdziałać ze sobą.
W przeciwieństwie do tego, Elm jest wpisany statycznie. Polega na swoim kompilatorze, aby zapewnić zgodność wartości przekazywanych do funkcji przed uruchomieniem. Oznacza to brak wyjątków w czasie wykonywania dla użytkowników i w ten sposób Elm może zapewnić gwarancję „braku wyjątków w czasie wykonywania”. Tam, gdzie błędy typu w wielu kompilatorach mogą być szczególnie tajemnicze, Elm koncentruje się na tym, aby były łatwe do zrozumienia i poprawienia.
Wiąz sprawia, że zaczynanie z typami jest bardzo przyjazne. W rzeczywistości wnioskowanie typu Elma jest tak dobre, że możesz pominąć pisanie adnotacji, dopóki nie poczujesz się z nimi bardziej komfortowo. Jeśli nie znasz typów, radzę polegać na sugestiach kompilatora, zamiast próbować pisać je samodzielnie.
Zacznijmy modelować nasze dane za pomocą typów. Nasz sekwencer krokowy to wizualna oś czasu, w której powinna zostać odtworzona konkretna próbka perkusyjna. Linia czasu składa się ze ścieżek , z których każda ma przypisaną określoną próbkę perkusyjną i sekwencję kroków . Krok można uznać za chwilę w czasie lub takt. Jeśli krok jest aktywny , próbka powinna zostać wyzwolona podczas odtwarzania, a jeśli krok jest nieaktywny , próbka powinna pozostać wyciszona. Podczas odtwarzania sekwencer będzie przechodził przez każdy krok odtwarzając próbki aktywnych kroków. Szybkość odtwarzania jest ustawiana przez liczbę uderzeń na minutę (BPM) .
Modelowanie naszej aplikacji w JavaScript
Aby lepiej zrozumieć nasze typy, zastanówmy się, jak modelować ten sekwencer perkusyjny w JavaScript. Istnieje szereg utworów. Każdy obiekt ścieżki zawiera informacje o sobie: nazwę ścieżki, próbkę/klip, który zostanie wyzwolony oraz sekwencję wartości kroków.
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... ]
Musimy zarządzać stanem odtwarzania między odtwarzaniem a zatrzymaniem.
playback: "playing" || "stopped"
Podczas odtwarzania musimy określić, który krok ma być odtwarzany. Powinniśmy również rozważyć wydajność odtwarzania, a nie przechodzenie przez każdą sekwencję na każdej ścieżce za każdym razem, gdy krok jest zwiększany; powinniśmy zredukować wszystkie aktywne kroki do jednej sekwencji odtwarzania. Każda kolekcja w sekwencji odtwarzania reprezentuje wszystkie próbki, które powinny zostać odtworzone. Na przykład ["kick", "hat"]
oznacza, że powinny być odtwarzane próbki kopnięcia i hi-hatu, podczas gdy ["hat"]
oznacza, że powinien być odtwarzany tylko hi-hat. Potrzebujemy również każdej kolekcji, aby ograniczyć unikalność do próbki, więc nie otrzymamy czegoś takiego jak ["hat", "hat", "hat"]
.
playbackPosition: 1 playbackSequence: [ ["kick", "hat"], [], ["hat"], [], ["snare", "hat"], [], ["hat"], [], ... ],
I musimy ustawić tempo odtwarzania, czyli BPM.
bpm: 120
Modelowanie z typami w Elm
Transkrypcja tych danych na typy Elm zasadniczo opisuje, z czego oczekujemy, że nasze dane będą wykonane. Na przykład, już odwołujemy się do naszego modelu danych jako model , więc nazywamy go aliasem typu. Aliasy typów służą do ułatwienia czytania kodu. Nie są prymitywnym typem , takim jak boolean lub liczba całkowita; są to po prostu nazwy, które nadajemy prymitywnemu typowi lub strukturze danych. Używając jednego, definiujemy wszelkie dane, które są zgodne z naszą strukturą modelu, jako model , a nie jako strukturę anonimową. W wielu projektach Elm główna struktura nosi nazwę Model.
type alias Model = { tracks : Array Track , playback : Playback , playbackPosition : PlaybackPosition , bpm : Int , playbackSequence : Array (Set Clip) }
Chociaż nasz model wygląda trochę jak obiekt JavaScript, opisuje Elm Record. Rekordy służą do organizowania powiązanych danych w kilka pól, które mają własne adnotacje typu. Są one łatwo dostępne za pomocą field.attribute
i łatwe do aktualizacji, co zobaczymy później. Obiekty i zapisy są bardzo podobne, z kilkoma kluczowymi różnicami:
- Nie można wywołać nieistniejących pól
- Pola nigdy nie będą
null
aniundefined
-
this
iself
nie można użyć
Nasza kolekcja ścieżek może składać się z jednego z trzech możliwych typów: list, tablic i zestawów. Krótko mówiąc, Listy to nieindeksowane kolekcje ogólnego użytku, tablice są indeksowane, a zestawy zawierają tylko wartości unikatowe. Potrzebujemy indeksu, aby wiedzieć, który krok ścieżki został przełączony, a ponieważ tablice są indeksowane, jest to nasz najlepszy wybór. Alternatywnie możemy dodać identyfikator do ścieżki i filtrować z listy.
W naszym modelu złożyliśmy ścieżki na tablicę track , inny rekord: tracks : Array Track
. Ścieżka zawiera informacje o sobie. Zarówno nazwa, jak i klip są ciągami, ale wpisaliśmy klip z aliasem, ponieważ wiemy, że inne funkcje będą się do niego odwoływać w innym miejscu kodu. Tworząc aliasy, zaczynamy tworzyć kod samodokumentujący. Tworzenie typów i aliasów typów umożliwia programistom modelowanie modelu danych do modelu biznesowego, tworząc wszechobecny język.
type alias Track = { name : String , clip : Clip , sequence : Array Step } type Step = On | Off type alias Clip = String
Wiemy, że sekwencja będzie tablicą wartości włączenia/wyłączenia. Moglibyśmy ustawić go jako tablicę wartości logicznych, na przykład sequence : Array Bool
, ale stracilibyśmy okazję do wyrażenia naszego modelu biznesowego! Biorąc pod uwagę, że sekwencery krokowe składają się z kroków , definiujemy nowy typ o nazwie Step . Step może być aliasem typu dla wartości boolean
, ale możemy pójść o krok dalej: Steps ma dwie możliwe wartości, on i off, więc tak definiujemy typ unii. Teraz kroki mogą być tylko włączone lub wyłączone, co sprawia, że wszystkie inne stany są niemożliwe.
Definiujemy inny typ dla Playback
, alias dla PlaybackPosition
, i używamy Clip podczas definiowania playbackSequence
jako tablicy zawierającej zestawy klipów. BPM jest przypisany jako standard Int
.
type Playback = Playing | Stopped type alias PlaybackPosition = Int
Chociaż rozpoczęcie pracy z typami wiąże się z nieco większym obciążeniem, nasz kod jest znacznie łatwiejszy w utrzymaniu. Dokumentuje się samodzielnie i używa wszechobecnego języka w naszym modelu biznesowym. Pewność, jaką zyskujemy, wiedząc, że nasze przyszłe funkcje będą współdziałać z naszymi danymi w sposób, jakiego oczekujemy, bez konieczności przeprowadzania testów, jest warta poświęcenia czasu na napisanie adnotacji. I możemy polegać na wnioskach o typie kompilatora, aby zasugerować typy, więc ich pisanie jest tak proste, jak kopiowanie i wklejanie. Oto pełna deklaracja typu.
Korzystanie z architektury wiązów
Architektura Elm to prosty wzorzec zarządzania stanem, który w naturalny sposób pojawia się w języku. Skupia się na modelu biznesowym i jest wysoce skalowalny. W przeciwieństwie do innych frameworków SPA, Elm jest przekonany o swojej architekturze — to sposób, w jaki ustrukturyzowane są wszystkie aplikacje, co sprawia, że onboarding jest dziecinnie prosty. Architektura składa się z trzech części:
- Model zawierający stan aplikacji oraz strukturę którą wpisujemy aliasowany model
- Funkcja aktualizacji , która aktualizuje stan
- Oraz funkcja widoku , która przedstawia stan wizualnie
Zacznijmy budować nasz sekwencer perkusyjny, ucząc się w praktyce Architektury Wiązów. Zaczniemy od zainicjowania naszej aplikacji, wyrenderowania widoku, a następnie zaktualizowania stanu aplikacji. Pochodząc z Ruby, wolę krótsze pliki i dzielę funkcje Elm na moduły, chociaż bardzo normalne jest posiadanie dużych plików Elm. Stworzyłem punkt wyjścia na Ellie, ale lokalnie utworzyłem następujące pliki:
- Types.elm, zawierający wszystkie definicje typów
- Main.elm, który inicjuje i uruchamia program
- Update.elm, zawierający funkcję aktualizacji, która zarządza stanem
- View.elm, zawierający kod Elm do renderowania do HTML
Inicjowanie naszej aplikacji
Najlepiej zacząć od małych rzeczy, więc ograniczamy model, aby skupić się na zbudowaniu pojedynczej ścieżki zawierającej kroki, które można włączać i wyłączać. Chociaż wydaje nam się , że znamy całą strukturę danych, zaczynanie od małych pozwala nam skupić się na renderowaniu ścieżek jako HTML. Zmniejsza złożoność i kod nie będzie Ci potrzebny. Później kompilator poprowadzi nas przez refaktoryzację naszego modelu. W pliku Types.elm zachowujemy typy Step i Clip, ale zmieniamy model i śledzenie.
type alias Model = { track : Track } type alias Track = { name : String , sequence : Array Step } type Step = On | Off type alias Clip = String
Aby renderować Elm jako HTML, używamy pakietu Elm Html. Posiada opcje tworzenia trzech typów programów, które bazują na sobie:
- Program dla początkujących
Zredukowany program, który wyklucza efekty uboczne i jest szczególnie przydatny do nauki architektury Elm. - Program
Standardowy program obsługujący efekty uboczne, przydatny do pracy z bazami danych lub narzędziami istniejącymi poza Elm. - Program z flagami
Rozszerzony program, który może zainicjować się przy użyciu rzeczywistych danych zamiast danych domyślnych.
Dobrą praktyką jest używanie najprostszego możliwego typu programu, ponieważ łatwo go później zmienić za pomocą kompilatora. Jest to powszechna praktyka podczas programowania w Elm; używaj tylko tego, czego potrzebujesz i zmieniaj to później. Dla naszych celów wiemy, że musimy mieć do czynienia z JavaScript, który jest uważany za efekt uboczny, więc tworzymy Html.program
. W Main.elm musimy zainicjować program poprzez przekazanie funkcji do jego pól.
main : Program Never Model Msg main = Html.program { init = init , view = view , update = update , subscriptions = always Sub.none }
Każde pole w programie przekazuje funkcję do Elm Runtime, który steruje naszą aplikacją. Krótko mówiąc, Elm Runtime:
- Uruchamia program z naszymi początkowymi wartościami z
init
. - Renderuje pierwszy widok, przekazując nasz zainicjowany model do
view
. - Ciągle ponownie renderuje widok, gdy komunikaty są przesyłane do
update
z widoków, poleceń lub subskrypcji.
Lokalnie, nasze funkcje view
i update
zostaną zaimportowane odpowiednio z View.elm
i Update.elm
, i utworzymy je za chwilę. subscriptions
nasłuchują komunikatów, aby spowodować aktualizacje, ale na razie ignorujemy je, przypisując always Sub.none
. Nasza pierwsza funkcja, init
, inicjuje model. Pomyśl o init
jak o wartościach domyślnych dla pierwszego ładowania. Definiujemy to pojedynczym utworem o nazwie „kick” i sekwencją kroków Off. Ponieważ nie otrzymujemy danych asynchronicznych, jawnie ignorujemy polecenia z Cmd.none
, aby zainicjować bez skutków ubocznych.
init : ( Model, Cmd.Cmd Msg ) init = ( { track = { sequence = Array.initialize 16 (always Off) , name = "Kick" } } , Cmd.none )
Nasza adnotacja typu init pasuje do naszego programu. Jest to struktura danych zwana krotką, która zawiera ustaloną liczbę wartości. W naszym przypadku Model
i komendy. Na razie zawsze ignorujemy polecenia, używając Cmd.none
, dopóki nie będziemy gotowi później zająć się efektami ubocznymi. Nasza aplikacja nic nie renderuje, ale się kompiluje!
Renderowanie naszej aplikacji
Zbudujmy nasze poglądy. W tym momencie nasz model ma jedną ścieżkę, więc to jedyna rzecz, którą musimy wyrenderować. Struktura HTML powinna wyglądać tak:
<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>
Zbudujemy trzy funkcje do renderowania naszych widoków:
- Jeden do renderowania pojedynczego utworu, który zawiera nazwę utworu i sekwencję
- Kolejny do renderowania samej sekwencji
- I jeszcze jeden, aby wyrenderować każdy pojedynczy przycisk kroku w sekwencji
Nasza pierwsza funkcja widoku wyrenderuje pojedynczą ścieżkę. Polegamy na naszej adnotacji typu renderTrack : Track -> Html Msg
, aby wymusić przechodzenie pojedynczej ścieżki. Używanie typów oznacza, że zawsze wiemy, że renderTrack
będzie miał ścieżkę. Nie musimy sprawdzać, czy pole name
istnieje w rekordzie lub czy przekazaliśmy ciąg znaków zamiast rekordu. Elm nie skompiluje się, jeśli spróbujemy przekazać do renderTrack
coś innego niż Track
. Co więcej, jeśli popełnimy błąd i przypadkowo spróbujemy przekazać do funkcji coś innego niż ścieżkę, kompilator przekaże nam przyjazne komunikaty, które wskażą nam właściwy kierunek.
renderTrack : Track -> Html Msg renderTrack track = div [ class "track" ] [ p [ class "track-title" ] [ text track.name ] , div [ class "track-sequence" ] (renderSequence track.sequence) ]
Może wydawać się to oczywiste, ale wszystko Elm to Elm, łącznie z pisaniem HTML. Nie ma języka szablonów ani abstrakcji do pisania HTML — to wszystko to Elm. Elementy HTML to funkcje Elm, które przyjmują nazwę, listę atrybutów i listę dzieci. Zatem div [ class "track" ] []
wyprowadza <div class="track"></div>
. Listy są oddzielone przecinkami w Elm, więc dodanie identyfikatora do elementu div wyglądałoby jak div [ class "track", id "my-id" ] []
.
Zawijająca div track-sequence
przekazuje sekwencję ścieżki do naszej drugiej funkcji, renderSequence
. Pobiera sekwencję i zwraca listę przycisków HTML. Moglibyśmy zachować renderSequence
w renderTrack
, aby pominąć dodatkową funkcję, ale uważam, że dzielenie funkcji na mniejsze części jest znacznie łatwiejsze do zrozumienia. Dodatkowo otrzymujemy kolejną możliwość zdefiniowania bardziej rygorystycznej adnotacji typu.
renderSequence : Array Step -> List (Html Msg) renderSequence sequence = Array.indexedMap renderStep sequence |> Array.toList
Mapujemy każdy krok w sekwencji i przekazujemy go do funkcji renderStep
. W JavaScript mapowanie z indeksem wyglądałoby tak:
sequence.map((node, index) => renderStep(index, node))
W porównaniu do JavaScript, mapowanie w Elm jest prawie odwrócone. Array.indexedMap
, który przyjmuje dwa argumenty: funkcję do zastosowania w mapie ( renderStep
) i tablicę do mapowania ( sequence
). renderStep
to nasza ostatnia funkcja, która określa, czy przycisk jest aktywny, czy nieaktywny. Używamy indexedMap
, ponieważ musimy przekazać indeks kroku (który używamy jako identyfikatora) do samego kroku, aby przekazać go do funkcji aktualizacji.
renderStep : Int -> Step -> Html Msg renderStep index step = let classes = if step == On then "step _active" else "step" in button [ class classes ] []
renderStep
akceptuje indeks jako pierwszy argument, krok jako drugi i zwraca wyrenderowany kod HTML. Używając bloku let...in
do zdefiniowania funkcji lokalnych, przypisujemy klasę _active
do On Steps i wywołujemy funkcję naszych klas na liście atrybutów przycisku.
Aktualizowanie stanu aplikacji
W tym momencie nasza aplikacja renderuje 16 kroków w sekwencji wykopów, ale kliknięcie nie aktywuje kroku. Aby zaktualizować stan kroku, musimy przekazać komunikat ( Msg
) do funkcji aktualizacji. Robimy to, definiując komunikat i dołączając go do obsługi zdarzeń dla naszego przycisku.
W Types.elm musimy zdefiniować naszą pierwszą wiadomość, ToggleStep
. To zajmie Int
dla indeksu sekwencji i Step
. Następnie w renderStep
dołączamy komunikat ToggleStep
do zdarzenia kliknięcia przycisku wraz z indeksem sekwencji i krokiem jako argumentami. Spowoduje to wysłanie wiadomości do naszej funkcji aktualizacji, ale w tym momencie aktualizacja właściwie nic nie zrobi.
type Msg = ToggleStep Int Step renderStep index step = let ... in button [ onClick (ToggleStep index step) , class classes ] []
Komunikaty są zwykłymi typami, ale zdefiniowaliśmy je jako typ powodujący aktualizacje, co jest konwencją w Elm. W Update.elm postępujemy zgodnie z architekturą Elm, aby obsłużyć zmiany stanu modelu. Nasza funkcja aktualizacji pobierze Msg
i bieżący Model
oraz zwróci nowy model i potencjalnie polecenie. Polecenia obsługują skutki uboczne, o których przyjrzymy się w części drugiej. Wiemy, że będziemy mieć wiele Msg
wiadomości, więc ustawiliśmy blok dopasowujący się do wzorca. To zmusza nas do obsługi wszystkich naszych spraw, jednocześnie oddzielając przepływ stanów. A kompilator upewni się, że nie przeoczymy żadnych przypadków, które mogłyby zmienić nasz model.
Aktualizacja rekordu w Elm odbywa się trochę inaczej niż aktualizacja obiektu w JavaScript. Nie możemy bezpośrednio zmienić pola w rekordzie, takiego jak record.field = *
, ponieważ nie możemy użyć this
lub self
, ale Elm ma wbudowanych pomocników. Mając rekord taki jak brian = { name = "brian" }
, możemy zaktualizować pole nazwy, takie jak { brian | name = "BRIAN" }
{ brian | name = "BRIAN" }
. Format następujący: { record | field = newValue }
{ record | field = newValue }
.
Oto jak zaktualizować pola najwyższego poziomu, ale pola zagnieżdżone są trudniejsze w Elm. Musimy zdefiniować własne funkcje pomocnicze, więc zdefiniujemy cztery funkcje pomocnicze, aby zagłębić się w zagnieżdżone rekordy:
- Jeden, aby przełączyć wartość kroku
- Jeden, aby zwrócić nową sekwencję, zawierającą zaktualizowaną wartość kroku
- Kolejny wybór, do którego utworu należy sekwencja
- I ostatnia funkcja zwracająca nową ścieżkę, zawierającą zaktualizowaną sekwencję, która zawiera zaktualizowaną wartość kroku
Zaczynamy od ToggleStep
, aby przełączać wartość kroku sekwencji ścieżek między On i Off. Ponownie używamy bloku let...in
, aby utworzyć mniejsze funkcje w instrukcji case. Jeśli krok jest już wyłączony, włączamy go i na odwrót.
toggleStep = if step == Off then On else Off
toggleStep
zostanie wywołany z newSequence
. Dane są niezmienne w językach funkcjonalnych, więc zamiast modyfikować sekwencję, w rzeczywistości tworzymy nową sekwencję ze zaktualizowaną wartością kroku, aby zastąpić starą.
newSequence = Array.set index toggleStep selectedTrack.sequence
newSequence
używa Array.set
do znalezienia indeksu, który chcemy przełączać, a następnie tworzy nową sekwencję. Jeśli set nie znajdzie indeksu, zwraca tę samą sekwencję. Opiera się na selectedTrack.sequence
, aby wiedzieć, którą sekwencję zmodyfikować. selectedTrack
to nasza kluczowa funkcja pomocnicza, dzięki której możemy sięgnąć do naszego zagnieżdżonego rekordu. W tym momencie jest to zaskakująco proste, ponieważ nasz model ma tylko jedną ścieżkę.
selectedTrack = model.track
Nasza ostatnia funkcja pomocnicza łączy całą resztę. Ponownie, ponieważ dane są niezmienne, zastępujemy całą naszą ścieżkę nową ścieżką zawierającą nową sekwencję.
newTrack = { selectedTrack | sequence = newSequence }
newTrack
jest wywoływana poza blokiem let...in
, w którym zwracamy nowy model, zawierający nową ścieżkę, która ponownie renderuje widok. Nie przekazujemy efektów ubocznych, więc ponownie używamy Cmd.none
. Cała nasza funkcja update
wygląda tak:
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 )
Kiedy uruchamiamy nasz program, widzimy wyrenderowaną ścieżkę z serią kroków. Kliknięcie dowolnego przycisku kroku wyzwala ToggleStep
, który uruchamia naszą funkcję aktualizacji, aby zastąpić stan modelu.
Wraz ze skalowaniem naszej aplikacji zobaczymy, jak powtarzalny wzorzec architektury Elm sprawia, że obsługa stanu staje się prosta. Znajomość funkcji modelu, aktualizacji i widoku pomaga nam skoncentrować się na naszej domenie biznesowej i ułatwia przeskoczenie do cudzej aplikacji Elm.
Zrobić sobie przerwę
Pisanie w nowym języku wymaga czasu i praktyki. Pierwszymi projektami, nad którymi pracowałem, były proste klony TypeForm, których używałem do nauki składni Elm, architektury i paradygmatów programowania funkcjonalnego. W tym momencie nauczyłeś się już wystarczająco dużo, aby zrobić coś podobnego. Jeśli jesteś chętny, polecam zapoznanie się z Oficjalnym przewodnikiem dla początkujących. Evan, twórca Elm, prowadzi cię przez motywacje dla Elm, składnię, typy, architekturę Elm, skalowanie i nie tylko, używając praktycznych przykładów.
W części drugiej zagłębimy się w jedną z najlepszych funkcji Elma: użycie kompilatora do refaktoryzacji naszego sekwencera kroków. Ponadto dowiemy się, jak radzić sobie ze zdarzeniami powtarzającymi się, korzystać z poleceń dotyczących efektów ubocznych i jak korzystać z JavaScript. Bądźcie czujni!