Aprendiendo Elm de un secuenciador de batería (Parte 1)

Publicado: 2022-03-10
Resumen rápido ↬ El desarrollador front-end Brian Holt guía a los lectores a través de la construcción de un secuenciador de batería en Elm. En la primera parte de esta serie de dos partes, presenta la sintaxis, la configuración y los conceptos básicos de Elm. Aprenderás a trabajar con la arquitectura Elm para crear aplicaciones sencillas.

Si es un desarrollador front-end que sigue la evolución de las aplicaciones de una sola página (SPA), es probable que haya oído hablar de Elm, el lenguaje funcional que inspiró a Redux. Si no lo ha hecho, es un lenguaje de compilación a JavaScript comparable con proyectos SPA como React, Angular y Vue.

Al igual que esos, administra los cambios de estado a través de su dominio virtual con el objetivo de hacer que el código sea más fácil de mantener y de mayor rendimiento. Se enfoca en la felicidad del desarrollador, herramientas de alta calidad y patrones simples y repetibles. Algunas de sus diferencias clave incluyen mensajes de error maravillosamente útiles y de tipo estático, y que es un lenguaje funcional (a diferencia de Orientado a Objetos).

Mi introducción se produjo a través de una charla dada por Evan Czaplicki, el creador de Elm, sobre su visión de la experiencia de los desarrolladores front-end y, a su vez, la visión de Elm. Como alguien que también se centró en la mantenibilidad y la usabilidad del desarrollo front-end, su charla realmente resonó conmigo. Probé Elm en un proyecto paralelo hace un año y continúo disfrutando tanto de sus características como de sus desafíos como no lo había hecho desde que comencé a programar; Soy un principiante de nuevo. Además, me encuentro capaz de aplicar muchas de las prácticas de Elm en otros idiomas.

Desarrollar la conciencia de la dependencia

Las dependencias están en todas partes. Al reducirlos, puede mejorar la probabilidad de que su sitio sea utilizable por la mayor cantidad de personas en la más amplia variedad de escenarios. Lea un artículo relacionado →

En este artículo de dos partes, construiremos un secuenciador por pasos para programar ritmos de batería en Elm, mientras mostramos algunas de las mejores características del lenguaje. Hoy, repasaremos los conceptos básicos de Elm, es decir, cómo empezar, usar tipos, renderizar vistas y actualizar el estado. Luego, la segunda parte de este artículo se sumergirá en temas más avanzados, como el manejo fácil de refactores grandes, la configuración de eventos recurrentes y la interacción con JavaScript.

Juega con el proyecto final aquí y mira su código aquí.

secuenciador por pasos en acción
Así es como se verá el secuenciador de pasos completo en acción.

Primeros pasos con Elm

Para seguir este artículo, recomiendo usar Ellie, una experiencia de desarrollador de Elm en el navegador. No necesita instalar nada para ejecutar Ellie, y puede desarrollar aplicaciones completamente funcionales en él. Si prefiere instalar Elm en su computadora, la mejor manera de configurarlo es siguiendo la guía oficial de inicio.

¡Más después del salto! Continúe leyendo a continuación ↓

A lo largo de este artículo, incluiré enlaces a las versiones de Ellie que están en progreso, aunque desarrollé el secuenciador localmente. Y aunque CSS se puede escribir completamente en Elm, he escrito este proyecto en PostCSS. Esto requiere un poco de configuración en Elm Reactor para el desarrollo local a fin de tener estilos cargados. En aras de la brevedad, no tocaré los estilos en este artículo, pero los enlaces de Ellie incluyen todos los estilos CSS minimizados.

Elm es un ecosistema autónomo que incluye:

  • olmo hacer
    Para compilar su código Elm. Si bien Webpack sigue siendo popular para la producción de proyectos de Elm junto con otros activos, no es obligatorio. En este proyecto, he optado por excluir Webpack y confiar en elm make para compilar el código.
  • Paquete de olmo
    Un administrador de paquetes comparable a NPM para usar paquetes/módulos creados por la comunidad.
  • reactor de olmo
    Para ejecutar un servidor de desarrollo de compilación automática. Más notable, incluye el depurador de viaje en el tiempo, lo que facilita el paso a través de los estados de su aplicación y la reproducción de errores.
  • Reemplazo de olmo
    Para escribir o probar expresiones simples de Elm en la terminal.

Todos los archivos Elm se consideran modules . Las líneas iniciales de cualquier archivo incluirán el module FileName exposing (functions) donde FileName es el nombre de archivo literal, y functions son las funciones públicas que desea que otros módulos puedan acceder. Inmediatamente después de la definición del módulo están las importaciones desde módulos externos. El resto de las funciones siguen.

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

Este módulo, llamado Main.elm , expone una sola función, main , e importa Html y text del módulo/paquete Html . La función main consta de dos partes: la anotación de tipo y la función real. Las anotaciones de tipo se pueden considerar como definiciones de funciones. Indican los tipos de argumentos y el tipo de retorno. En este caso, el nuestro indica que la función main no toma argumentos y devuelve Html msg . La función en sí representa un nodo de texto que contiene "Hola, mundo". Para pasar argumentos a una función, agregamos nombres separados por espacios antes del signo igual en la función. También agregamos los tipos de argumentos a la anotación de tipo, en el orden de los argumentos, seguidos de una flecha.

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

En JavaScript, una función como esta es comparable:

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

Y en un lenguaje escrito, como TypeScript, se ve así:

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

add2Numbers toma dos enteros y devuelve un entero. El último valor de la anotación es siempre el valor devuelto porque cada función debe devolver un valor. Llamamos a add2Numbers con 2 y 3 para obtener 5 como add2Numbers 2 3 .

Así como vinculas los componentes de React, necesitamos vincular el código Elm compilado al DOM. La forma estándar de enlazar es llamar a embed() en nuestro módulo y pasarle el elemento DOM.

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

Aunque nuestra aplicación en realidad no hace nada, tenemos suficiente para compilar nuestro código Elm y representar el texto. Compruébelo en Ellie e intente cambiar los argumentos a add2Numbers en la línea 26.

La aplicación Elm representa números agregados
Nuestra aplicación Elm básica que representa números agregados en la pantalla.

Modelado de datos con tipos

Viniendo de un lenguaje de tipos dinámicos como JavaScript o Ruby, los tipos pueden parecer superfluos. Esos lenguajes determinan qué tipo de funciones toman del valor que se pasa durante el tiempo de ejecución. La escritura de funciones generalmente se considera más rápida, pero pierde la seguridad de garantizar que sus funciones puedan interactuar correctamente entre sí.

Por el contrario, Elm tiene tipos estáticos. Se basa en su compilador para garantizar que los valores pasados ​​a las funciones sean compatibles antes del tiempo de ejecución. Esto significa que no hay excepciones de tiempo de ejecución para sus usuarios, y es cómo Elm puede garantizar su "sin excepciones de tiempo de ejecución". Donde los errores de tipo en muchos compiladores pueden ser especialmente crípticos, Elm se enfoca en hacerlos fáciles de entender y corregir.

Elm hace que empezar con los tipos sea muy amigable. De hecho, la inferencia de tipos de Elm es tan buena que puede omitir escribir anotaciones hasta que se sienta más cómodo con ellas. Si es nuevo en los tipos, le recomiendo confiar en las sugerencias del compilador en lugar de intentar escribirlas usted mismo.

Comencemos a modelar nuestros datos usando tipos. Nuestro secuenciador por pasos es una línea de tiempo visual de cuándo debe reproducirse una muestra de batería en particular. La línea de tiempo consta de pistas , cada una de las cuales tiene asignada una muestra de batería específica y la secuencia de pasos . Un paso puede considerarse un momento en el tiempo o un latido. Si un paso está activo , la muestra debe activarse durante la reproducción, y si el paso está inactivo , la muestra debe permanecer en silencio. Durante la reproducción, el secuenciador se moverá a través de cada paso reproduciendo las muestras de los pasos activos. La velocidad de reproducción se establece mediante Beats Per Minute (BPM) .

aplicación final formada por pistas con secuencias de pasos
Una captura de pantalla de nuestra aplicación final, compuesta por pistas con secuencias de pasos.

Modelando nuestra aplicación en JavaScript

Para tener una mejor idea de nuestros tipos, consideremos cómo modelar este secuenciador de batería en JavaScript. Hay una variedad de pistas. Cada objeto de pista contiene información sobre sí mismo: el nombre de la pista, la muestra/clip que se activará y la secuencia de valores de paso.

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

Necesitamos administrar el estado de reproducción entre reproducir y detener.

 playback: "playing" || "stopped"

Durante la reproducción, necesitamos determinar qué paso debe reproducirse. También deberíamos considerar el rendimiento de la reproducción, y en lugar de recorrer cada secuencia en cada pista cada vez que se incrementa un paso; debemos reducir todos los pasos activos en una sola secuencia de reproducción. Cada colección dentro de la secuencia de reproducción representa todas las muestras que deben reproducirse. Por ejemplo, ["kick", "hat"] significa que deben reproducirse las muestras de bombo y charles, mientras que ["hat"] significa que solo debe reproducirse el charles. También necesitamos que cada colección restrinja la singularidad de la muestra, para que no terminemos con algo como ["hat", "hat", "hat"] .

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

Y necesitamos establecer el ritmo de reproducción, o el BPM.

 bpm: 120

Modelado con tipos en Elm

Transcribir estos datos en tipos de Elm es esencialmente describir de qué esperamos que estén hechos nuestros datos. Por ejemplo, ya nos estamos refiriendo a nuestro modelo de datos como modelo , por lo que lo llamamos así con un alias de tipo. Los alias de tipo se utilizan para facilitar la lectura del código. No son un tipo primitivo como un booleano o un entero; son simplemente nombres que le damos a un tipo primitivo o estructura de datos. Con uno, definimos cualquier dato que siga la estructura de nuestro modelo como un modelo en lugar de una estructura anónima. En muchos proyectos de Elm, la estructura principal se denomina Modelo.

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

Aunque nuestro modelo se parece un poco a un objeto de JavaScript, describe un Elm Record. Los registros se utilizan para organizar datos relacionados en varios campos que tienen sus propias anotaciones de tipo. Son fáciles de acceder mediante field.attribute y fáciles de actualizar, lo que veremos más adelante. Los objetos y los registros son muy similares, con algunas diferencias clave:

  • Los campos inexistentes no se pueden llamar
  • Los campos nunca serán null o undefined
  • this y self no se pueden usar

Nuestra colección de pistas se puede componer de uno de los tres tipos posibles: listas, matrices y conjuntos. En resumen, las listas son colecciones de uso general no indexadas, las matrices están indexadas y los conjuntos solo contienen valores únicos. Necesitamos un índice para saber qué paso de la pista se ha alternado y, dado que las matrices están indexadas, es nuestra mejor elección. Alternativamente, podríamos agregar una identificación a la pista y filtrar desde una Lista.

En nuestro modelo, hemos compuesto pistas en una matriz de track , otro registro: tracks : Array Track . La pista contiene la información sobre sí misma. Tanto el nombre como el clip son cadenas, pero hemos escrito clip con alias porque sabemos que otras funciones harán referencia a él en otra parte del código. Al asignarle un alias, comenzamos a crear un código autodocumentado. La creación de tipos y alias de tipo permite a los desarrolladores modelar el modelo de datos en el modelo de negocio, creando un lenguaje ubicuo.

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

Sabemos que la secuencia será una matriz de valores de encendido/apagado. Podríamos establecerlo como una matriz de valores booleanos, como sequence : Array Bool , ¡pero perderíamos la oportunidad de expresar nuestro modelo de negocio! Teniendo en cuenta que los secuenciadores por pasos están hechos de pasos , definimos un nuevo tipo llamado Step . Un paso podría ser un alias de tipo para un boolean , pero podemos ir un paso más allá: los pasos tienen dos valores posibles, activado y desactivado, así es como definimos el tipo de unión. Ahora los pasos solo pueden estar activados o desactivados, lo que hace que todos los demás estados sean imposibles.

Definimos otro tipo para Playback , un alias para PlaybackPosition , y usamos Clip cuando definimos playbackSequence como una matriz que contiene conjuntos de clips. BPM se asigna como un Int estándar.

 type Playback = Playing | Stopped type alias PlaybackPosition = Int

Si bien hay un poco más de sobrecarga al comenzar con los tipos, nuestro código es mucho más fácil de mantener. Es autodocumentado y utiliza un lenguaje ubicuo con nuestro modelo de negocio. La confianza que ganamos al saber que nuestras funciones futuras interactuarán con nuestros datos de la manera que esperamos, sin requerir pruebas, vale la pena el tiempo que lleva escribir una anotación. Y podríamos confiar en la inferencia de tipos del compilador para sugerir los tipos, de modo que escribirlos sea tan simple como copiar y pegar. Aquí está la declaración de tipo completa.

Usando la arquitectura Elm

Elm Architecture es un patrón de administración de estado simple que surge naturalmente en el lenguaje. Crea un enfoque en torno al modelo de negocio y es altamente escalable. A diferencia de otros marcos SPA, Elm tiene opiniones sobre su arquitectura: es la forma en que se estructuran todas las aplicaciones, lo que hace que la incorporación sea muy sencilla. La arquitectura consta de tres partes:

  • El modelo , que contiene el estado de la aplicación, y la estructura que escribimos modelo con alias
  • La función de actualización , que actualiza el estado
  • Y la función de vista , que representa el estado visualmente

Comencemos a construir nuestro secuenciador de batería aprendiendo la Arquitectura Elm en la práctica a medida que avanzamos. Comenzaremos inicializando nuestra aplicación, representando la vista y luego actualizando el estado de la aplicación. Como vengo de Ruby, tiendo a preferir archivos más cortos y divido mis funciones de Elm en módulos, aunque es muy normal tener archivos grandes de Elm. Creé un punto de partida en Ellie, pero localmente creé los siguientes archivos:

  • Types.elm, que contiene todas las definiciones de tipo
  • Main.elm, que inicializa y ejecuta el programa
  • Update.elm, que contiene la función de actualización que administra el estado
  • View.elm, que contiene código Elm para renderizar en HTML

Inicializando nuestra aplicación

Es mejor comenzar poco a poco, por lo que reducimos el modelo para enfocarnos en construir una sola pista que contenga pasos que se activen y desactiven. Si bien ya creemos que conocemos toda la estructura de datos, comenzar poco a poco nos permite concentrarnos en representar las pistas como HTML. Reduce la complejidad y el código You Ain't Gonna Need It. Más tarde, el compilador nos guiará a través de la refactorización de nuestro modelo. En el archivo Types.elm, mantenemos nuestros tipos de Paso y Clip pero cambiamos el modelo y la pista.

 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 el paquete Elm Html. Tiene opciones para crear tres tipos de programas que se complementan entre sí:

  • Programa para principiantes
    Un programa reducido que excluye efectos secundarios y es especialmente útil para aprender la Arquitectura Elm.
  • Programa
    El programa estándar que maneja los efectos secundarios, útil para trabajar con bases de datos o herramientas que existen fuera de Elm.
  • Programa Con Banderas
    Un programa extendido que puede inicializarse con datos reales en lugar de datos predeterminados.

Es una buena práctica usar el tipo de programa más simple posible porque es fácil cambiarlo más tarde con el compilador. Esta es una práctica común cuando se programa en Elm; usa solo lo que necesitas y cámbialo más tarde. Para nuestros propósitos, sabemos que debemos lidiar con JavaScript, que se considera un efecto secundario, por lo que creamos un Html.program . En Main.elm necesitamos inicializar el programa pasando funciones a sus campos.

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

Cada campo en el programa pasa una función a Elm Runtime, que controla nuestra aplicación. En pocas palabras, Elm Runtime:

  • Inicia el programa con nuestros valores iniciales de init .
  • Renderiza la primera vista pasando nuestro modelo inicializado a view .
  • Vuelve a representar continuamente la vista cuando se pasan mensajes para update desde vistas, comandos o suscripciones.

Localmente, nuestras funciones de view y update se importarán desde View.elm y Update.elm respectivamente, y las crearemos en un momento. subscriptions escuchan los mensajes para generar actualizaciones, pero por ahora, los ignoramos asignando always Sub.none . Nuestra primera función, init , inicializa el modelo. Piense en init como los valores predeterminados para la primera carga. Lo definimos con una sola pista llamada “kick” y una secuencia de pasos Off. Dado que no obtenemos datos asíncronos, ignoramos explícitamente los comandos con Cmd.none para inicializar sin efectos secundarios.

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

Nuestra anotación de tipo init coincide con nuestro programa. Es una estructura de datos llamada tupla, que contiene un número fijo de valores. En nuestro caso, el Model y comandos. Por ahora, siempre ignoramos los comandos usando Cmd.none hasta que estemos listos para manejar los efectos secundarios más adelante. Nuestra aplicación no muestra nada, ¡pero compila!

Representación de nuestra aplicación

Construyamos nuestras vistas. En este punto, nuestro modelo tiene una sola pista, por lo que eso es lo único que necesitamos renderizar. La estructura HTML debería verse así:

 <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 tres funciones para representar nuestras vistas:

  1. Uno para renderizar una sola pista, que contiene el nombre de la pista y la secuencia
  2. Otro para renderizar la secuencia en sí.
  3. Y uno más para renderizar cada botón de paso individual dentro de la secuencia.

Nuestra función de primera vista renderizará una sola pista. Confiamos en nuestra anotación de tipo, renderTrack : Track -> Html Msg , para hacer cumplir el paso de una sola pista. Usar tipos significa que siempre sabemos que renderTrack tendrá una pista. No necesitamos verificar si el campo de name existe en el registro, o si hemos pasado una cadena en lugar de un registro. Elm no compilará si intentamos pasar algo que no sea Track a renderTrack . Aún mejor, si cometemos un error y accidentalmente tratamos de pasar algo que no sea una pista a la función, el compilador nos dará mensajes amistosos para indicarnos la dirección correcta.

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

Puede parecer obvio, pero todo Elm es Elm, incluida la escritura de HTML. No hay un lenguaje de plantillas o abstracción para escribir HTML, todo es Elm. Los elementos HTML son funciones de Elm, que toman el nombre, una lista de atributos y una lista de elementos secundarios. Entonces div [ class "track" ] [] genera <div class="track"></div> . Las listas están separadas por comas en Elm, por lo que agregar una identificación al div se vería como div [ class "track", id "my-id" ] [] .

La track-sequence de la pista a nuestra segunda función, renderSequence . Toma una secuencia y devuelve una lista de botones HTML. Podríamos mantener renderSequence en renderTrack para omitir la función adicional, pero creo que dividir las funciones en partes más pequeñas es mucho más fácil de razonar. Además, tenemos otra oportunidad de definir una anotación de tipo más estricta.

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

Mapeamos cada paso en la secuencia y lo pasamos a la función renderStep . En JavaScript, el mapeo con un índice se escribiría así:

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

Comparado con JavaScript, el mapeo en Elm es casi inverso. Llamamos a Array.indexedMap , que toma dos argumentos: la función que se aplicará en el mapa ( renderStep ) y la matriz para mapear ( sequence ). renderStep es nuestra última función y determina si un botón está activo o inactivo. Usamos indexedMap porque necesitamos pasar el índice del paso (que usamos como ID) al paso mismo para pasarlo a la función de actualización.

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

renderStep acepta el índice como su primer argumento, el paso como el segundo y devuelve HTML renderizado. Usando un bloque let...in para definir funciones locales, asignamos la clase _active a On Steps y llamamos a nuestra función de clases en la lista de atributos del botón.

La pista de bombo que contiene una secuencia de pasos
La pista de bombo que contiene una secuencia de pasos

Actualización del estado de la aplicación

En este punto, nuestra aplicación representa los 16 pasos de la secuencia de patada, pero al hacer clic no se activa el paso. Para actualizar el estado del paso, necesitamos pasar un mensaje ( Msg ) a la función de actualización. Hacemos esto definiendo un mensaje y adjuntándolo a un controlador de eventos para nuestro botón.

En Types.elm, necesitamos definir nuestro primer mensaje, ToggleStep . Tomará un Int para el índice de secuencia y un Step . A continuación, en renderStep , adjuntamos el mensaje ToggleStep al evento de clic del botón, junto con el índice de secuencia y el paso como argumentos. Esto enviará el mensaje a nuestra función de actualización, pero en este punto, la actualización en realidad no hará nada.

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

Los mensajes son tipos regulares, pero los definimos como el tipo para generar actualizaciones, que es la convención en Elm. En Update.elm seguimos la arquitectura Elm para manejar los cambios de estado del modelo. Nuestra función de actualización tomará un Msg y el Model actual, y devolverá un nuevo modelo y potencialmente un comando. Los comandos manejan los efectos secundarios, que veremos en la segunda parte. Sabemos que tendremos varios tipos de Msg , por lo que configuramos un bloque de casos de coincidencia de patrones. Esto nos obliga a manejar todos nuestros casos al mismo tiempo que separamos el flujo de estado. Y el compilador se asegurará de que no pasemos por alto ningún caso que pueda cambiar nuestro modelo.

La actualización de un registro en Elm se realiza de manera un poco diferente a la actualización de un objeto en JavaScript. No podemos cambiar directamente un campo en el registro como record.field = * porque no podemos usar this o self , pero Elm tiene asistentes incorporados. Dado un registro como brian = { name = "brian" } , podemos actualizar el campo de nombre como { brian | name = "BRIAN" } { brian | name = "BRIAN" } . El formato sigue { record | field = newValue } { record | field = newValue } .

Así es como se actualizan los campos de nivel superior, pero los campos anidados son más complicados en Elm. Necesitamos definir nuestras propias funciones auxiliares, por lo que definiremos cuatro funciones auxiliares para sumergirnos en los registros anidados:

  1. Uno para alternar el valor del paso
  2. Uno para devolver una nueva secuencia, que contiene el valor del paso actualizado
  3. Otro para elegir a qué pista pertenece la secuencia
  4. Y una última función para devolver una nueva pista, que contiene la secuencia actualizada que contiene el valor del paso actualizado.

Comenzamos con ToggleStep para alternar el valor de paso de la secuencia de pistas entre activado y desactivado. Usamos un bloque let...in nuevamente para hacer funciones más pequeñas dentro de la declaración del caso. Si el paso ya está desactivado, lo activamos y viceversa.

 toggleStep = if step == Off then On else Off

toggleStep se llamará desde newSequence . Los datos son inmutables en los lenguajes funcionales, por lo que en lugar de modificar la secuencia, en realidad estamos creando una nueva secuencia con un valor de paso actualizado para reemplazar la anterior.

 newSequence = Array.set index toggleStep selectedTrack.sequence

newSequence usa Array.set para encontrar el índice que queremos alternar, luego crea la nueva secuencia. Si set no encuentra el índice, devuelve la misma secuencia. Se basa en selectedTrack.sequence para saber qué secuencia modificar. selectedTrack es nuestra función de ayuda clave utilizada para que podamos acceder a nuestro registro anidado. En este punto, es sorprendentemente simple porque nuestro modelo solo tiene una pista.

 selectedTrack = model.track

Nuestra última función auxiliar conecta todo el resto. Nuevamente, dado que los datos son inmutables, reemplazamos toda nuestra pista con una nueva pista que contiene una nueva secuencia.

 newTrack = { selectedTrack | sequence = newSequence }

newTrack se llama fuera del bloque let...in , donde devolvemos un nuevo modelo, que contiene la nueva pista, que vuelve a representar la vista. No estamos pasando efectos secundarios, por lo que usamos Cmd.none nuevamente. Toda nuestra función de update se ve así:

 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 )

Cuando ejecutamos nuestro programa, vemos una pista renderizada con una serie de pasos. Al hacer clic en cualquiera de los botones de paso, se activa ToggleStep , que activa nuestra función de actualización para reemplazar el estado del modelo.

La pista de bombo que contiene una secuencia de pasos activos
La pista de bombo que contiene una secuencia de pasos activos

A medida que nuestra aplicación escala, veremos cómo el patrón repetible de Elm Architecture simplifica el manejo del estado. La familiaridad en sus funciones de modelo, actualización y vista nos ayuda a centrarnos en nuestro dominio comercial y facilita el salto a la aplicación Elm de otra persona.

Tomando un descanso

Escribir en un nuevo idioma requiere tiempo y práctica. Los primeros proyectos en los que trabajé fueron simples clones de TypeForm que usé para aprender la sintaxis, la arquitectura y los paradigmas de programación funcional de Elm. En este punto, ya has aprendido lo suficiente como para hacer algo similar. Si está ansioso, le recomiendo trabajar con la Guía oficial de inicio. Evan, el creador de Elm, lo guía a través de las motivaciones de Elm, la sintaxis, los tipos, la arquitectura de Elm, la escala y más, utilizando ejemplos prácticos.

En la segunda parte, nos sumergiremos en una de las mejores características de Elm: usar el compilador para refactorizar nuestro secuenciador de pasos. Además, aprenderemos cómo manejar eventos recurrentes, usar comandos para efectos secundarios e interactuar con JavaScript. ¡Manténganse al tanto!