Aprendendo Elm com um sequenciador de bateria (Parte 1)

Publicados: 2022-03-10
Resumo rápido ↬ O desenvolvedor front-end Brian Holt orienta os leitores na construção de um sequenciador de bateria no Elm. Na primeira parte desta série de duas partes, ele apresenta a sintaxe, a configuração e os conceitos principais do Elm. Você aprenderá a trabalhar com a arquitetura Elm para criar aplicativos simples.

Se você é um desenvolvedor front-end acompanhando a evolução dos aplicativos de página única (SPA), provavelmente já ouviu falar do Elm, a linguagem funcional que inspirou o Redux. Se você não tiver, é uma linguagem de compilação para JavaScript comparável a projetos SPA como React, Angular e Vue.

Assim como esses, ele gerencia as mudanças de estado por meio de seu dom virtual com o objetivo de tornar o código mais sustentável e performático. Ele se concentra na felicidade do desenvolvedor, ferramentas de alta qualidade e padrões simples e repetíveis. Algumas de suas principais diferenças incluem ser digitadas estaticamente, mensagens de erro maravilhosamente úteis e que é uma linguagem funcional (em oposição à Orientada a Objetos).

Minha apresentação veio através de uma palestra dada por Evan Czaplicki, o criador do Elm, sobre sua visão para a experiência do desenvolvedor front-end e, por sua vez, a visão para o Elm. Como alguém também se concentrou na manutenção e usabilidade do desenvolvimento front-end, sua conversa realmente mexeu comigo. Experimentei o Elm em um projeto paralelo há um ano e continuo aproveitando seus recursos e desafios de uma maneira que não fazia desde que comecei a programar; Eu sou um novato tudo de novo. Além disso, consigo aplicar muitas das práticas de Elm em outros idiomas.

Desenvolvendo a Consciência da Dependência

As dependências estão por toda parte. Ao reduzi-los, você pode aumentar a probabilidade de seu site ser usado pelo maior número de pessoas na mais ampla variedade de cenários.Leia um artigo relacionado →

Neste artigo de duas partes, construiremos um sequenciador de passos para programar batidas de bateria no Elm, enquanto apresentamos alguns dos melhores recursos da linguagem. Hoje, vamos percorrer os conceitos fundamentais do Elm, ou seja, como começar, usando tipos, exibições de renderização e estado de atualização. A segunda parte deste artigo mergulhará em tópicos mais avançados, como lidar com grandes refatorações facilmente, configurar eventos recorrentes e interagir com JavaScript.

Jogue com o projeto final aqui, e confira seu código aqui.

sequenciador de passos em ação
Veja como será o seqüenciador de etapas concluído em ação.

Introdução ao Elm

Para acompanhar este artigo, recomendo usar Ellie, uma experiência de desenvolvedor Elm no navegador. Você não precisa instalar nada para executar o Ellie e pode desenvolver aplicativos totalmente funcionais nele. Se você preferir instalar o Elm em seu computador, a melhor maneira de configurar é seguindo o guia oficial de introdução.

Mais depois do salto! Continue lendo abaixo ↓

Ao longo deste artigo, vou linkar para as versões de trabalho em andamento de Ellie, embora eu tenha desenvolvido o sequenciador localmente. E enquanto CSS pode ser escrito inteiramente em Elm, eu escrevi este projeto em PostCSS. Isso requer um pouco de configuração no Elm Reactor para desenvolvimento local para que os estilos sejam carregados. Por uma questão de brevidade, não vou tocar em estilos neste artigo, mas os links Ellie incluem todos os estilos CSS reduzidos.

Elm é um ecossistema independente que inclui:

  • Olmo Make
    Para compilar seu código Elm. Embora o Webpack ainda seja popular para a produção de projetos Elm juntamente com outros ativos, ele não é obrigatório. Neste projeto, optei por excluir o Webpack e confiar no elm make para compilar o código.
  • Pacote Olmo
    Um gerenciador de pacotes comparável ao NPM para usar pacotes/módulos criados pela comunidade.
  • Reator Elm
    Para executar um servidor de desenvolvimento de compilação automática. Mais notável, ele inclui o Depurador de Viagem no Tempo, tornando mais fácil percorrer os estados do seu aplicativo e reproduzir bugs.
  • Olmo Repl
    Para escrever ou testar expressões Elm simples no terminal.

Todos os arquivos Elm são considerados modules . As linhas iniciais de qualquer arquivo incluirão o module FileName exposing (functions) onde FileName é o nome do arquivo literal e as functions são as funções públicas que você deseja tornar acessíveis a outros módulos. Imediatamente após a definição do módulo são importadas de módulos externos. As demais funções seguem.

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

Este módulo, chamado Main.elm , expõe uma única função, main , e importa Html e text do módulo/pacote Html . A função main consiste em duas partes: a anotação de tipo e a função real. As anotações de tipo podem ser consideradas como definições de função. Eles indicam os tipos de argumento e o tipo de retorno. Nesse caso, a nossa afirma que a função main não recebe argumentos e retorna Html msg . A própria função renderiza um nó de texto contendo “Hello, World”. Para passar argumentos para uma função, adicionamos nomes separados por espaços antes do sinal de igual na função. Também adicionamos os tipos de argumento à anotação de tipo, na ordem dos argumentos, seguidos por uma seta.

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

Em JavaScript, uma função como esta é comparável:

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

E em uma linguagem Typed, como TypeScript, fica assim:

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

add2Numbers recebe dois inteiros e retorna um inteiro. O último valor na anotação é sempre o valor de retorno porque toda função deve retornar um valor. Chamamos add2Numbers com 2 e 3 para obter 5 como add2Numbers 2 3 .

Assim como você vincula os componentes do React, precisamos vincular o código Elm compilado ao DOM. A maneira padrão de vincular é chamar embed() em nosso módulo e passar o elemento DOM para ele.

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

Embora nosso aplicativo não faça nada, temos o suficiente para compilar nosso código Elm e renderizar texto. Confira em Ellie e tente alterar os argumentos para add2Numbers na linha 26.

O aplicativo Elm renderiza números adicionados
Nosso aplicativo Elm básico que renderiza números adicionados na tela.

Modelagem de dados com tipos

Vindo de uma linguagem tipada dinamicamente como JavaScript ou Ruby, os tipos podem parecer supérfluos. Essas linguagens determinam quais funções de tipo obtêm do valor que está sendo passado durante o tempo de execução. Escrever funções geralmente é considerado mais rápido, mas você perde a segurança de garantir que suas funções possam interagir adequadamente umas com as outras.

Em contraste, Elm é tipado estaticamente. Ele depende de seu compilador para garantir que os valores passados ​​para as funções sejam compatíveis antes do tempo de execução. Isso significa que não há exceções de tempo de execução para seus usuários e é assim que o Elm pode garantir “sem exceções de tempo de execução”. Onde os erros de tipo em muitos compiladores podem ser especialmente enigmáticos, o Elm se concentra em torná-los fáceis de entender e corrigir.

Elm torna a introdução aos tipos muito amigável. Na verdade, a inferência de tipos do Elm é tão boa que você pode pular as anotações até se sentir mais confortável com elas. Se você é novo em tipos, recomendo confiar nas sugestões do compilador em vez de tentar escrevê-las você mesmo.

Vamos começar a modelar nossos dados usando tipos. Nosso sequenciador de passos é uma linha do tempo visual de quando uma amostra de bateria específica deve tocar. A linha do tempo consiste em faixas , cada uma atribuída com uma amostra de bateria específica e a seqüência de etapas . Um passo pode ser considerado um momento no tempo ou uma batida. Se uma etapa estiver ativa , a amostra deverá ser acionada durante a reprodução e, se a etapa estiver inativa , a amostra deverá permanecer em silêncio. Durante a reprodução, o seqüenciador se moverá em cada etapa reproduzindo as amostras das etapas ativas. A velocidade de reprodução é definida pelo Beats Per Minute (BPM) .

aplicação final composta por trilhas com sequências de passos
Uma captura de tela do nosso aplicativo final, composto por faixas com sequências de etapas.

Modelando nosso aplicativo em JavaScript

Para ter uma ideia melhor de nossos tipos, vamos considerar como modelar esse sequenciador de bateria em JavaScript. Há uma série de faixas. Cada objeto de faixa contém informações sobre si mesmo: o nome da faixa, a amostra/clipe que será acionado e a sequência de valores de etapa.

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

Precisamos gerenciar o estado de reprodução entre a reprodução e a parada.

 playback: "playing" || "stopped"

Durante a reprodução, precisamos determinar qual etapa deve ser reproduzida. Devemos também considerar o desempenho de reprodução e, em vez de percorrer cada sequência em cada faixa, cada vez que um passo é incrementado; devemos reduzir todas as etapas ativas em uma única sequência de reprodução. Cada coleção dentro da sequência de reprodução representa todas as amostras que devem ser reproduzidas. Por exemplo, ["kick", "hat"] significa que os samples de kick e hi-hat devem tocar, enquanto ["hat"] significa que apenas o chimbal deve tocar. Também precisamos que cada coleção restrinja a exclusividade à amostra, para que não tenhamos algo como ["hat", "hat", "hat"] .

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

E precisamos definir o ritmo de reprodução, ou o BPM.

 bpm: 120

Modelagem com tipos em Elm

Transcrever esses dados em tipos Elm é essencialmente descrever do que esperamos que nossos dados sejam feitos. Por exemplo, já estamos nos referindo ao nosso modelo de dados como model , então o chamamos com um alias de tipo. Os aliases de tipo são usados ​​para tornar o código mais fácil de ler. Eles não são um tipo primitivo como um booleano ou inteiro; são simplesmente nomes que damos a um tipo primitivo ou estrutura de dados. Usando um, definimos todos os dados que seguem nossa estrutura de modelo como um modelo e não como uma estrutura anônima. Em muitos projetos Elm, a estrutura principal é denominada Model.

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

Embora nosso modelo se pareça um pouco com um objeto JavaScript, ele está descrevendo um registro Elm. Os registros são usados ​​para organizar dados relacionados em vários campos que possuem suas próprias anotações de tipo. Eles são fáceis de acessar usando field.attribute e fáceis de atualizar, o que veremos mais tarde. Objetos e registros são muito semelhantes, com algumas diferenças importantes:

  • Campos inexistentes não podem ser chamados
  • Os campos nunca serão null ou undefined
  • this e self não podem ser usados

Nossa coleção de faixas pode ser composta por um dos três tipos possíveis: Listas, Arrays e Conjuntos. Em resumo, Lists são coleções de uso geral não indexadas, Arrays são indexados e Sets contêm apenas valores exclusivos. Precisamos de um índice para saber qual etapa da trilha foi alternada e, como as matrizes são indexadas, é nossa melhor escolha. Alternativamente, podemos adicionar um id à faixa e filtrar de uma lista.

Em nosso modelo, nós digitamos tracks para um array de track , outro registro: tracks : Array Track . Track contém as informações sobre si mesmo. Tanto name quanto clip são strings, mas digitamos clip com alias porque sabemos que ele será referenciado em outro lugar no código por outras funções. Ao criar um alias, começamos a criar um código autodocumentado. A criação de tipos e aliases de tipo permite que os desenvolvedores modelem o modelo de dados para o modelo de negócios, criando uma linguagem onipresente.

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

Sabemos que a sequência será uma matriz de valores ativados/desativados. Poderíamos defini-lo como um array de booleanos, como sequence : Array Bool , mas perderíamos a oportunidade de expressar nosso modelo de negócios! Considerando que os seqüenciadores de passos são feitos de passos , definimos um novo tipo chamado Step . Um Step pode ser um alias de tipo para um boolean , mas podemos dar um passo adiante: Steps têm dois valores possíveis, on e off, então é assim que definimos o tipo de união. Agora as etapas só podem estar Ligadas ou Desligadas, tornando todos os outros estados impossíveis.

Definimos outro tipo para Playback , um alias para PlaybackPosition e usamos Clip ao definir playbackSequence como um Array contendo conjuntos de Clips. BPM é atribuído como um padrão Int .

 type Playback = Playing | Stopped type alias PlaybackPosition = Int

Embora haja um pouco mais de sobrecarga na introdução aos tipos, nosso código é muito mais fácil de manter. É autodocumentado e usa linguagem onipresente com nosso modelo de negócios. A confiança que ganhamos em saber que nossas funções futuras irão interagir com nossos dados da maneira que esperamos, sem exigir testes, vale o tempo que leva para escrever uma anotação. E podemos contar com a inferência de tipos do compilador para sugerir os tipos, de modo que escrevê-los é tão simples quanto copiar e colar. Aqui está a declaração de tipo completa.

Usando a arquitetura Elm

A arquitetura Elm é um padrão simples de gerenciamento de estado que surgiu naturalmente na linguagem. Ele cria foco em torno do modelo de negócios e é altamente escalável. Em contraste com outras estruturas de SPA, o Elm é opinativo sobre sua arquitetura — é a maneira como todos os aplicativos são estruturados, o que facilita muito a integração. A arquitetura consiste em três partes:

  • O modelo , contendo o estado do aplicativo e a estrutura que digitamos modelo alias
  • A função de atualização , que atualiza o estado
  • E a função view , que renderiza o estado visualmente

Vamos começar a construir nosso sequenciador de bateria aprendendo a arquitetura Elm na prática à medida que avançamos. Começaremos inicializando nosso aplicativo, renderizando a exibição e atualizando o estado do aplicativo. Vindo de um background Ruby, costumo preferir arquivos mais curtos e dividir minhas funções Elm em módulos, embora seja muito normal ter arquivos Elm grandes. Criei um ponto de partida na Ellie, mas localmente criei os seguintes arquivos:

  • Types.elm, contendo todas as definições de tipo
  • Main.elm, que inicializa e executa o programa
  • Update.elm, contendo a função de atualização que gerencia o estado
  • View.elm, contendo código Elm para renderizar em HTML

Inicializando nosso aplicativo

É melhor começar pequeno, então reduzimos o modelo para focar na construção de uma única trilha contendo etapas que podem ser ativadas e desativadas. Embora já pensemos que conhecemos toda a estrutura de dados, começar pequeno nos permite focar na renderização de faixas como HTML. Reduz a complexidade e o código You Ain't Gonna Need It. Mais tarde, o compilador nos guiará pela refatoração de nosso modelo. No arquivo Types.elm, mantemos nossos tipos Step e Clip, mas alteramos o modelo e a trilha.

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

Para renderizar Elm como HTML, usamos o pacote Elm Html. Tem opções para criar três tipos de programas que se complementam:

  • Programa Iniciante
    Um programa reduzido que exclui efeitos colaterais e é especialmente útil para aprender a Arquitetura Elm.
  • Programa
    O programa padrão que lida com efeitos colaterais, útil para trabalhar com bancos de dados ou ferramentas que existem fora do Elm.
  • Programa com bandeiras
    Um programa estendido que pode se inicializar com dados reais em vez de dados padrão.

É uma boa prática usar o tipo de programa mais simples possível porque é fácil alterá-lo posteriormente com o compilador. Esta é uma prática comum ao programar em Elm; use apenas o que você precisa e altere-o mais tarde. Para nossos propósitos, sabemos que precisamos lidar com JavaScript, que é considerado um efeito colateral, então criamos um Html.program . Em Main.elm precisamos inicializar o programa passando funções para seus campos.

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

Cada campo do programa passa uma função para o Elm Runtime, que controla nossa aplicação. Em poucas palavras, o Elm Runtime:

  • Inicia o programa com nossos valores iniciais de init .
  • Renderiza a primeira view passando nosso modelo inicializado para view .
  • Rerenderiza continuamente a exibição quando as mensagens são passadas para update de exibições, comandos ou assinaturas.

Localmente, nossas funções de view e update serão importadas de View.elm e Update.elm , respectivamente, e as criaremos em um momento. subscriptions escutam mensagens para causar atualizações, mas por enquanto, nós as ignoramos atribuindo always Sub.none . Nossa primeira função, init , inicializa o modelo. Pense no init como os valores padrão para o primeiro carregamento. Nós o definimos com uma única faixa chamada “kick” e uma sequência de passos Off. Como não estamos obtendo dados assíncronos, ignoramos explicitamente os comandos com Cmd.none para inicializar sem efeitos colaterais.

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

Nossa anotação de tipo de inicialização corresponde ao nosso programa. É uma estrutura de dados chamada tupla, que contém um número fixo de valores. No nosso caso, o Model e os comandos. Por enquanto, sempre ignoramos comandos usando Cmd.none até que estejamos prontos para lidar com os efeitos colaterais mais tarde. Nosso aplicativo não renderiza nada, mas compila!

Renderizando nosso aplicativo

Vamos construir nossos pontos de vista. Neste ponto, nosso modelo tem uma única trilha, então essa é a única coisa que precisamos renderizar. A estrutura HTML deve se parecer com:

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

Construiremos três funções para renderizar nossas visualizações:

  1. Um para renderizar uma única faixa, que contém o nome da faixa e a sequência
  2. Outro para renderizar a própria sequência
  3. E mais um para renderizar cada botão de passo individual dentro da sequência

Nossa primeira função de visualização renderizará uma única faixa. Contamos com nossa anotação de tipo, renderTrack : Track -> Html Msg , para impor uma única faixa passada. Usar tipos significa que sempre sabemos que renderTrack terá uma faixa. Não precisamos verificar se o campo de name existe no registro ou se passamos uma string em vez de um registro. Elm não irá compilar se tentarmos passar algo diferente de Track para renderTrack . Melhor ainda, se cometermos um erro e acidentalmente tentarmos passar qualquer coisa além de uma trilha para a função, o compilador nos dará mensagens amigáveis ​​para nos apontar na direção certa.

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

Pode parecer óbvio, mas tudo Elm é Elm, incluindo escrever HTML. Não há uma linguagem de modelagem ou abstração para escrever HTML - é tudo Elm. Os elementos HTML são funções Elm, que recebem o nome, uma lista de atributos e uma lista de filhos. Então div [ class "track" ] [] gera <div class="track"></div> . As listas são separadas por vírgulas no Elm, portanto, adicionar um id ao div ficaria como div [ class "track", id "my-id" ] [] .

O div wrap track-sequence passa a sequência da trilha para nossa segunda função, renderSequence . Leva uma sequência e retorna uma lista de botões HTML. Poderíamos manter renderSequence em renderTrack para pular a função adicional, mas acho muito mais fácil raciocinar sobre a quebra de funções em partes menores. Além disso, temos outra oportunidade de definir uma anotação de tipo mais rigorosa.

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

Mapeamos cada etapa na sequência e a passamos para a função renderStep . No mapeamento JavaScript com um índice seria escrito assim:

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

Comparado ao JavaScript, o mapeamento no Elm é quase invertido. Chamamos Array.indexedMap , que recebe dois argumentos: a função a ser aplicada no mapa ( renderStep ) e o array a ser mapeado ( sequence ). renderStep é nossa última função e determina se um botão está ativo ou inativo. Usamos indexedMap porque precisamos passar o índice da etapa (que usamos como ID) para a própria etapa para passá-lo para a função de atualização.

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

renderStep aceita o índice como seu primeiro argumento, o passo como o segundo e retorna o HTML renderizado. Usando um bloco let...in para definir funções locais, atribuímos a classe _active a On Steps e chamamos nossa função de classes na lista de atributos do botão.

A faixa de chute contendo uma sequência de passos
A faixa de chute contendo uma sequência de passos

Atualizando o estado do aplicativo

Neste ponto, nosso aplicativo renderiza as 16 etapas na sequência de kick, mas clicar não ativa a etapa. Para atualizar o estado da etapa, precisamos passar uma mensagem ( Msg ) para a função de atualização. Fazemos isso definindo uma mensagem e anexando-a a um manipulador de eventos para nosso botão.

Em Types.elm, precisamos definir nossa primeira mensagem, ToggleStep . Levará um Int para o índice de sequência e um Step . Em seguida, em renderStep , anexamos a mensagem ToggleStep ao evento on click do botão, juntamente com o índice de sequência e o passo como argumentos. Isso enviará a mensagem para nossa função de atualização, mas, neste ponto, a atualização não fará nada.

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

As mensagens são tipos regulares, mas nós as definimos como o tipo para causar atualizações, que é a convenção em Elm. Em Update.elm, seguimos a Arquitetura Elm para lidar com as mudanças de estado do modelo. Nossa função de atualização pegará uma Msg e o Model atual e retornará um novo modelo e potencialmente um comando. Comandos lidam com efeitos colaterais, que veremos na parte dois. Sabemos que teremos vários tipos Msg , então configuramos um bloco case de correspondência de padrões. Isso nos força a lidar com todos os nossos casos enquanto também separamos o fluxo de estado. E o compilador terá certeza de que não perderemos nenhum caso que possa alterar nosso modelo.

A atualização de um registro no Elm é um pouco diferente da atualização de um objeto em JavaScript. Não podemos alterar diretamente um campo no registro como record.field = * porque não podemos usar this ou self , mas Elm possui ajudantes embutidos. Dado um registro como brian = { name = "brian" } , podemos atualizar o campo name como { brian | name = "BRIAN" } { brian | name = "BRIAN" } . O formato segue { record | field = newValue } { record | field = newValue } .

É assim que se atualiza os campos de nível superior, mas os campos aninhados são mais complicados no Elm. Precisamos definir nossas próprias funções auxiliares, então definiremos quatro funções auxiliares para mergulhar nos registros aninhados:

  1. Um para alternar o valor do passo
  2. Um para retornar uma nova sequência, contendo o valor da etapa atualizado
  3. Outro para escolher a qual faixa a sequência pertence
  4. E uma última função para retornar uma nova faixa, contendo a sequência atualizada que contém o valor do passo atualizado

Começamos com ToggleStep para alternar o valor do passo da sequência de trilhas entre On e Off. Usamos um bloco let...in novamente para criar funções menores dentro da instrução case. Se o passo já estiver Off, nós o tornamos On, e vice-versa.

 toggleStep = if step == Off then On else Off

toggleStep será chamado de newSequence . Os dados são imutáveis ​​em linguagens funcionais, portanto, em vez de modificar a sequência, estamos criando uma nova sequência com um valor de etapa atualizado para substituir o antigo.

 newSequence = Array.set index toggleStep selectedTrack.sequence

newSequence usa Array.set para encontrar o índice que queremos alternar e, em seguida, cria a nova sequência. Se set não encontrar o índice, ele retornará a mesma sequência. Ele se baseia em selectedTrack.sequence para saber qual sequência modificar. selectedTrack é nossa função auxiliar chave usada para que possamos acessar nosso registro aninhado. Neste ponto, é surpreendentemente simples porque nosso modelo tem apenas uma única pista.

 selectedTrack = model.track

Nossa última função auxiliar conecta todo o resto. Novamente, como os dados são imutáveis, substituímos toda a nossa trilha por uma nova trilha que contém uma nova sequência.

 newTrack = { selectedTrack | sequence = newSequence }

newTrack é chamado fora do bloco let...in , onde retornamos um novo modelo, contendo a nova trilha, que renderiza novamente a visualização. Não estamos passando efeitos colaterais, então usamos Cmd.none novamente. Nossa função de update inteira se parece com:

 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 executamos nosso programa, vemos uma trilha renderizada com uma série de etapas. Clicar em qualquer um dos botões de etapa aciona ToggleStep , que atinge nossa função de atualização para substituir o estado do modelo.

A faixa de kick contendo uma sequência de passos ativos
A faixa de kick contendo uma sequência de passos ativos

À medida que nosso aplicativo é dimensionado, veremos como o padrão repetível da arquitetura Elm simplifica o manuseio do estado. A familiaridade em suas funções de modelo, atualização e visualização nos ajuda a focar em nosso domínio de negócios e facilita o acesso ao aplicativo Elm de outra pessoa.

Tire uma folga

Escrever em um novo idioma requer tempo e prática. Os primeiros projetos em que trabalhei foram clones simples do TypeForm que usei para aprender a sintaxe do Elm, a arquitetura e os paradigmas de programação funcional. Neste ponto, você já aprendeu o suficiente para fazer algo semelhante. Se você estiver ansioso, recomendo trabalhar com o Guia Oficial de Introdução. Evan, o criador do Elm, orienta você pelas motivações do Elm, sintaxe, tipos, arquitetura do Elm, dimensionamento e muito mais, usando exemplos práticos.

Na parte dois, vamos mergulhar em um dos melhores recursos do Elm: usar o compilador para refatorar nosso sequenciador de passos. Além disso, aprenderemos a lidar com eventos recorrentes, usando comandos para efeitos colaterais e interagindo com JavaScript. Fique ligado!