Torne seu front-end da Web confiável com o Elm
Publicados: 2022-03-11Quantas vezes você tentou depurar seu front-end da Web e se viu emaranhado no código lidando com cadeias complexas de eventos?
Você já tentou refatorar o código para uma interface do usuário que lida com muitos componentes criados com jQuery, Backbone.js ou outros frameworks JavaScript populares?
Uma das coisas mais dolorosas sobre esses cenários é tentar seguir as múltiplas sequências indeterminadas de eventos e antecipar e corrigir todos esses comportamentos. Simplesmente um pesadelo!
Eu sempre procurei maneiras de escapar desse aspecto infernal do desenvolvimento web front-end. O Backbone.js funcionou bem para mim nesse sentido, dando aos front-ends da web a estrutura que eles estavam perdendo há muito tempo. Mas, dada a verbosidade necessária para fazer algumas das coisas mais triviais, não ficou muito melhor.
Então eu conheci Elm.
Elm é uma linguagem funcional de tipagem estática baseada na linguagem de programação Haskell, mas com uma especificação mais simples. O compilador (também construído usando Haskell) analisa o código Elm e o compila em JavaScript.
O Elm foi originalmente construído para desenvolvimento front-end, mas os engenheiros de software encontraram maneiras de usá-lo também para a programação do lado do servidor.
Este artigo fornece uma visão geral de como o Elm pode mudar a maneira como pensamos no desenvolvimento de front-end da Web e uma introdução aos conceitos básicos dessa linguagem de programação funcional. Neste tutorial, desenvolveremos um aplicativo simples semelhante a um carrinho de compras usando o Elm.
Por que Elmo?
Elm promete muitas vantagens, a maioria das quais é extremamente útil para obter uma arquitetura de front-end da Web limpa. Elm oferece melhores vantagens de desempenho de renderização de HTML em relação a outros frameworks populares (até mesmo React.js). Além disso, o Elm permite que os desenvolvedores escrevam código que, na prática, não produz a maioria das exceções de tempo de execução que afligem linguagens de tipagem dinâmica como JavaScript.
O compilador infere tipos automaticamente e emite erros amigáveis, alertando o desenvolvedor sobre qualquer possível problema antes do tempo de execução.
NoRedInk tem 36.000 linhas de Elm e, depois de mais de um ano em produção, ainda não produziu uma única exceção de tempo de execução. [Fonte]
Você não precisa converter todo o seu aplicativo JavaScript existente apenas para poder experimentar o Elm. Por meio de sua excelente interoperabilidade com JavaScript, você pode pegar apenas uma pequena parte de seu aplicativo existente e reescrevê-lo no Elm.
O Elm também possui uma excelente documentação que não apenas fornece uma descrição completa do que ele tem a oferecer, mas também fornece um guia adequado para a criação de front-end da Web seguindo a arquitetura Elm - algo ótimo para modularidade, reutilização de código e testes .
Vamos fazer um carrinho simples
Vamos começar com um pequeno trecho de código Elm:
import List exposing (..) cart = [] item product quantity = { product = product, qty = quantity } product name price = { name = name, price = price } add cart product = if isEmpty (filter (\item -> item.product == product) cart) then append cart [item product 1] else cart subtotal cart = -- we want to calculate cart subtotal sum (map (\item -> item.product.price * toFloat item.qty) cart)
Qualquer texto precedido por
--
é um comentário em Elm.
Aqui estamos definindo um carrinho como uma lista de itens, onde cada item é um registro com dois valores (o produto a que corresponde e a quantidade). Cada produto é um registro com um nome e um preço.
Adicionar um produto ao carrinho envolve verificar se o item já existe no carrinho.
Se isso acontecer, não fazemos nada; caso contrário, adicionamos o produto ao carrinho como um novo item. Verificamos se o produto já existe no carrinho filtrando a lista, combinando cada item com o produto e verificando se a lista filtrada resultante está vazia.
Para calcular o subtotal, iteramos os itens no carrinho, encontrando a quantidade e o preço do produto correspondente e somando tudo.
Isso é tão minimalista quanto um carrinho e suas funções relacionadas podem ser. Vamos começar com este código e melhorá-lo passo a passo, tornando-o um componente web completo, ou um programa nos termos de Elm.
Vamos começar adicionando tipos aos vários identificadores em nosso programa. Elm é capaz de inferir tipos por si só, mas para tirar o máximo proveito do Elm e seu compilador, é recomendado que os tipos sejam indicados explicitamente.
module Cart1 exposing ( Cart, Item, Product , add, subtotal , itemSubtotal ) -- This is module and its API definition {-| We build an easy shopping cart. @docs Cart, Item, Product, add, subtotal, itemSubtotal -} import List exposing (..) -- we need list manipulation functions {-| Cart is a list of items. -} type alias Cart = List Item {-| Item is a record of product and quantity. -} type alias Item = { product : Product, qty : Int } {-| Product is a record with name and price -} type alias Product = { name : String, price : Float } {-| We want to add stuff to a cart. This is a function definition, it takes a cart, a product to add and returns new cart -} add : Cart -> Product -> Cart {-| This is an implementation of the 'add' function. Just append product item to the cart if there is no such product in the cart listed. Do nothing if the product exists. -} add cart product = if isEmpty (filter (\item -> item.product == product) cart) then append cart [Item product 1] else cart {-| I need to calculate cart subtotal. The function takes a cart and returns float. -} subtotal : Cart -> Float {-| It's easy -- just sum subtotal of all items. -} subtotal cart = sum (map itemSubtotal cart) {-| Item subtotal takes item and return the subtotal float. -} itemSubtotal : Item -> Float {-| Subtotal is based on product's price and quantity. -} itemSubtotal item = item.product.price * toFloat item.qty
Com anotações de tipo, o compilador agora pode detectar problemas que, de outra forma, resultariam em exceções de tempo de execução.
No entanto, Elm não para por aí. A Elm Architecture orienta os desenvolvedores por meio de um padrão simples para estruturar seus front-ends da Web, e o faz por meio de conceitos com os quais a maioria dos desenvolvedores já está familiarizada:
- Modelo: Os modelos mantêm o estado do programa.
- View: View é uma representação visual do estado.
- Update: Update é uma maneira de alterar o estado.
Se você pensar na parte do seu código que lida com atualizações como o controlador, então você tem algo muito semelhante ao bom e velho paradigma Model-View-Controller (MVC).
Como Elm é uma linguagem de programação funcional pura, todos os dados são imutáveis, o que significa que o modelo não pode ser alterado. Em vez disso, podemos criar um novo modelo baseado no anterior, o que fazemos por meio de funções de atualização.
Por que isso é tão grande?
Com dados imutáveis, as funções não podem mais ter efeitos colaterais. Isso abre um mundo de possibilidades, incluindo o depurador de viagem no tempo de Elm, que discutiremos em breve.
As visualizações são renderizadas toda vez que uma mudança no modelo requer uma mudança na visualização, e sempre teremos o mesmo resultado para os mesmos dados no modelo - da mesma forma que uma função pura sempre retorna o mesmo resultado para o mesmos argumentos de entrada.
Comece com a função principal
Vamos implementar a visualização HTML para nosso aplicativo de carrinho.
Se você está familiarizado com o React, isso é algo que tenho certeza que você vai gostar: Elm tem um pacote apenas para definir elementos HTML. Isso permite que você implemente sua visão usando o Elm, sem precisar depender de linguagens de modelagem externas.
Os wrappers para os elementos HTML estão disponíveis no pacote Html
:
import Html exposing (Html, button, table, caption, thead, tbody, tfoot, tr, td, th, text, section, p, h1)
Todos os programas Elm começam executando a função main:
type alias Stock = List Product type alias Model = { cart : Cart, stock : Stock } main = Html.beginnerProgram { model = Model [] [ Product "Bicycle" 100.50 , Product "Rocket" 15.36 , Product "Biscuit" 21.15 ] , view = view , update = update }
Aqui, a função main inicializa um programa Elm com alguns modelos, uma visão e uma função de atualização. Definimos alguns tipos de produtos e seus preços. Para simplificar, estamos assumindo que temos um número ilimitado de produtos.
Uma função de atualização simples
A função de atualização é onde nosso aplicativo ganha vida.
Ele pega uma mensagem e atualiza o estado com base no conteúdo da mensagem. Definimos como uma função que recebe dois parâmetros (uma mensagem e o modelo atual) e retorna um novo modelo:
type Msg = Add Product update : Msg -> Model -> Model update msg model = case msg of Add product -> { model | cart = add model.cart product }
Por enquanto, estamos tratando de um único caso quando a mensagem é Add product
, onde chamamos o método add
no cart
com o product
.
A função de atualização crescerá à medida que a complexidade do programa Elm aumentar.
Implementando a função de visualização
Em seguida, definimos a visualização do nosso carrinho.
A visão é uma função que traduz o modelo em sua representação HTML. No entanto, não é apenas HTML estático. O gerador de HTML é capaz de emitir mensagens de volta para o aplicativo com base em várias interações e eventos do usuário.
view : Model -> Html Msg view model = section [style [("margin", "10px")]] [ stockView model.stock , cartView model.cart ] stockView : Stock -> Html Msg stockView stock = table [] [ caption [] [ h1 [] [ text "Stock" ] ] , thead [] [ tr [] [ th [align "left", width 100] [ text "Name" ] , th [align "right", width 100] [ text "Price" ] , th [width 100] [] ] ] , tbody [] (map stockProductView stock) ] stockProductView : Product -> Html Msg stockProductView product = tr [] [ td [] [ text product.name ] , td [align "right"] [ text ("\t$" ++ toString product.price) ] , td [] [ button [ onClick (Add product) ] [ text "Add to Cart" ] ] ]
O pacote Html
fornece wrappers para todos os elementos comumente usados como funções com nomes familiares (por exemplo, a section
de função gera um elemento <section>
).
A função de style
, parte do pacote Html.Attributes
, gera um objeto que pode ser passado para a função de section
para definir o atributo de estilo no elemento resultante.
É melhor dividir a visualização em funções separadas para melhor reutilização.
Para manter as coisas simples, incorporamos CSS e alguns atributos de layout diretamente em nosso código de visualização. No entanto, existem bibliotecas que simplificam o processo de estilizar seus elementos HTML a partir do código Elm.
Observe o button
próximo ao final do snippet e como associamos a mensagem Add product
ao evento de clique do botão.
Elm cuida de gerar todo o código necessário para vincular uma função de retorno de chamada com o evento real e gerar e chamar a função de atualização com parâmetros relevantes.
Finalmente, precisamos implementar a última parte da nossa visão:
cartView : Cart -> Html Msg cartView cart = if isEmpty cart then p [] [ text "Cart is empty" ] else table [] [ caption [] [ h1 [] [ text "Cart" ]] , thead [] [ tr [] [ th [ align "left", width 100 ] [ text "Name" ] , th [ align "right", width 100 ] [ text "Price" ] , th [ align "center", width 50 ] [ text "Qty" ] , th [ align "right", width 100 ] [ text "Subtotal" ] ] ] , tbody [] (map cartProductView cart) , tfoot [] [ tr [style [("font-weight", "bold")]] [ td [ align "right", colspan 4 ] [ text ("$" ++ toString (subtotal cart)) ] ] ] ] cartProductView : Item -> Html Msg cartProductView item = tr [] [ td [] [ text item.product.name ] , td [ align "right" ] [ text ("$" ++ toString item.product.price) ] , td [ align "center" ] [ text (toString item.qty) ] , td [ align "right" ] [ text ("$" ++ toString (itemSubtotal item)) ] ]
Aqui definimos a outra parte da nossa visão onde renderizamos o conteúdo do nosso carrinho. Embora a função de exibição não emita nenhuma mensagem, ela ainda precisa ter o tipo de retorno Html Msg
para se qualificar como uma exibição.
A exibição não apenas lista o conteúdo do carrinho, mas também calcula e renderiza o subtotal com base no conteúdo do carrinho.
Você pode encontrar o código completo para este programa Elm aqui.
Se você executasse o programa Elm agora, veria algo assim:
Como tudo isso funciona?
Nosso programa começa com um estado bastante vazio da função main
- um carrinho vazio com alguns produtos codificados.
Cada vez que o botão "Adicionar ao carrinho" é clicado, uma mensagem é enviada para a função de atualização, que atualiza o carrinho de acordo e cria um novo modelo. Sempre que o modelo é atualizado, as funções de visualização são invocadas pelo Elm para gerar novamente a árvore HTML.

Como o Elm usa uma abordagem Virtual DOM, semelhante à do React, as alterações na interface do usuário são realizadas apenas quando necessário, garantindo um desempenho ágil.
Não apenas um verificador de tipos
Elm é tipado estaticamente, mas o compilador pode verificar muito mais do que apenas tipos.
Vamos fazer uma mudança no nosso tipo Msg
e ver como o compilador reage a isso:
type Msg = Add Product | ChangeQty Product String
Definimos outro tipo de mensagem - algo que alteraria a quantidade de um produto no carrinho. No entanto, tentar executar o programa novamente sem manipular esta mensagem na função de atualização emitirá o seguinte erro:
Rumo a um carrinho mais funcional
Observe que na seção anterior usamos uma string como o tipo para o valor da quantidade. Isso porque o valor virá de um elemento <input>
que será do tipo string.
Vamos adicionar uma nova função changeQty
ao módulo Cart
. É sempre melhor manter a implementação dentro do módulo para poder alterá-la posteriormente, se necessário, sem alterar a API do módulo.
{-| Change quantity of the product in the cart. Look at the result of the function. It uses Result type. The Result type has two parameters: for bad and for good result. So the result will be Error "msg" or a Cart with updated product quantity. -} changeQty : Cart -> Product -> Int -> Result String Cart {-| If the quantity parameter is zero the product will be removed completely from the cart. If the quantity parameter is greater then zero the quantity of the product will be updated. Otherwise (qty < 0) the error will be returned. -} changeQty cart product qty = if qty == 0 then Ok (filter (\item -> item.product /= product) cart) else if qty > 0 then Result.Ok (map (\item -> if item.product == product then { item | qty = qty } else item) cart) else Result.Err ("Wrong negative quantity used: " ++ (toString qty))
Não devemos fazer suposições sobre como a função será usada. Podemos ter certeza de que o parâmetro qty
conterá um Int
, mas o valor pode ser qualquer coisa. Portanto, verificamos o valor e informamos um erro quando ele é inválido.
Também atualizamos nossa função de update
de acordo:
update msg model = case msg of Add product -> { model | cart = add model.cart product } ChangeQty product str -> case toInt str of Ok qty -> case changeQty model.cart product qty of Ok cart -> { model | cart = cart } Err msg -> model -- do nothing, the wrong input Err msg -> model -- do nothing, the wrong quantity
Convertemos o parâmetro de quantity
de string da mensagem em um número antes de usá-lo. Caso a string contenha um número inválido, informamos como um erro.
Aqui, mantemos o modelo inalterado quando ocorre um erro. Alternativamente, poderíamos apenas atualizar o modelo de forma a relatar o erro como uma mensagem na visualização para o usuário ver:
type alias Model = { cart : Cart, stock : Stock, error : Maybe String } main = Html.beginnerProgram { model = Model [] -- empty cart [ Product "Bicycle" 100.50 -- stock , Product "Rocket" 15.36 , Product "Bisquit" 21.15 ] Nothing -- error (no error at beginning) , view = view , update = update } update msg model = case msg of Add product -> { model | cart = add model.cart product } ChangeQty product str -> case toInt str of Ok qty -> case changeQty model.cart product qty of Ok cart -> { model | cart = cart, error = Nothing } Err msg -> { model | error = Just msg } Err msg -> { model | error = Just msg }
Usamos o tipo Maybe String
para o atributo error em nosso modelo. Maybe é outro tipo que pode conter Nothing
ou um valor de um tipo específico.
Após atualizar a função de visualização da seguinte forma:
view model = section [style [("margin", "10px")]] [ stockView model.stock , cartView model.cart , errorView model.error ] errorView : Maybe String -> Html Msg errorView error = case error of Just msg -> p [style [("color", "red")]] [ text msg ] Nothing -> p [] []
Você deve ver isso:
A tentativa de inserir um valor não numérico (por exemplo, “1a”) resultaria em uma mensagem de erro, conforme mostrado na captura de tela acima.
Mundo dos Pacotes
Elm tem seu próprio repositório de pacotes de código aberto. Com o gerenciador de pacotes do Elm, fica muito fácil aproveitar esse conjunto de pacotes. Embora o tamanho do repositório não seja comparável a algumas outras linguagens de programação maduras como Python ou PHP, a comunidade Elm está trabalhando duro para implementar mais pacotes todos os dias.
Observe como as casas decimais nos preços renderizados em nossa visão são inconsistentes?
Vamos substituir nosso uso ingênuo de toString
por algo melhor do repositório: numeral-elm.
cartProductView item = tr [] [ td [] [ text item.product.name ] , td [ align "right" ] [ text (formatPrice item.product.price) ] , td [ align "center" ] [ input [ value (toString item.qty) , onInput (ChangeQty item.product) , size 3 --, type' "number" ] [] ] , td [ align "right" ] [ text (formatPrice (itemSubtotal item)) ] ] formatPrice : Float -> String formatPrice price = format "$0,0.00" price
Estamos usando a função de format
do pacote Numeral aqui. Isso formataria os números de uma maneira que normalmente formatamos moedas:
100.5 -> $100.50 15.36 -> $15.36 21.15 -> $21.15
Geração Automática de Documentação
Ao publicar um pacote no repositório Elm, a documentação é gerada automaticamente com base nos comentários no código. Você pode vê-lo em ação verificando a documentação do nosso módulo Carrinho aqui. Tudo isso foi gerado a partir dos comentários vistos neste arquivo: Cart.elm.
Um verdadeiro depurador para front-end
Os problemas mais óbvios são detectados e relatados pelo próprio compilador. No entanto, nenhum aplicativo está a salvo de erros lógicos.
Como todos os dados em Elm são imutáveis e tudo acontece por meio de mensagens passadas para a função de atualização, todo o fluxo de um programa Elm pode ser representado como uma série de mudanças de modelo. Para o depurador, Elm é como um jogo de estratégia baseado em turnos. Isso permite que o depurador realize alguns feitos realmente incríveis, como viajar no tempo. Ele pode se mover para frente e para trás no fluxo de um programa pulando entre várias mudanças de modelo que aconteceram durante a vida útil de um programa.
Você pode aprender mais sobre o depurador aqui.
Interagindo com um back-end
Então, você diz, nós construímos um brinquedo legal, mas Elm pode ser usado para algo sério? Absolutamente.
Vamos conectar nosso front-end de carrinho com algum back-end assíncrono. Para torná-lo interessante, vamos implementar algo especial. Digamos que queremos inspecionar todos os carrinhos e seus conteúdos em tempo real. Na vida real, poderíamos usar essa abordagem para trazer alguns recursos extras de marketing/vendas para nossa loja online ou mercado, ou fazer algumas sugestões ao usuário, ou estimar os recursos de estoque necessários e muito mais.
Assim, armazenamos o carrinho no lado do cliente e também informamos ao servidor sobre cada carrinho em tempo real.
Para manter as coisas simples, implementaremos nosso back-end usando Python. Você pode encontrar o código completo para o back-end aqui.
É um servidor web simples que usa um WebSocket e acompanha o conteúdo do carrinho na memória. Para simplificar, renderizamos o carrinho de todos os outros na mesma página. Isso pode ser facilmente implementado em uma página separada ou até mesmo como um programa Elm separado. Por enquanto, cada usuário poderá ver o resumo dos carrinhos de outros usuários.
Com o back-end instalado, agora precisaremos atualizar nosso aplicativo Elm para enviar e receber atualizações de carrinho para o servidor. Usaremos JSON para codificar nossas cargas úteis, para as quais o Elm tem excelente suporte.
CartEncoder.elm
Implementaremos um codificador para converter nosso modelo de dados Elm em uma representação de string JSON. Para isso, precisamos usar a biblioteca Json.Encode.
module CartEncoder exposing (cart) import Cart2 exposing (Cart, Item, Product) import List exposing (map) import Json.Encode exposing (..) product : Product -> Value product product = object [ ("name", string product.name) , ("price", float product.price) ] item : Item -> Value item item = object [ ("product", product item.product) , ("qty", int item.qty) ] cart : Cart -> Value cart cart = list (map item cart)
A biblioteca fornece algumas funções (como string
, int
, float
, object
, etc.) que pegam objetos Elm e os transformam em strings codificadas em JSON.
CartDecoder.elm
Implementar o decodificador é um pouco mais complicado, pois todos os dados do Elm têm tipos e precisamos definir qual valor JSON precisa ser convertido para qual tipo:
module CartDecoder exposing (cart) import Cart2 exposing (Cart, Item, Product) -- decoding for Cart import Json.Decode exposing (..) -- will decode cart from string cart : Decoder (Cart) cart = list item -- decoder for cart is a list of items item : Decoder (Item) item = object2 Item -- decoder for item is an object with two properties: ("product" := product) -- 1) "product" of product ("qty" := int) -- 2) "qty" of int product : Decoder (Product) product = object2 Product -- decoder for product also an object with two properties: ("name" := string) -- 1) "name" ("price" := float) -- 2) "price"
Aplicativo Elm atualizado
Como o código final do Elm é um pouco mais longo, você pode encontrá-lo aqui. Aqui está um resumo das alterações que foram feitas no aplicativo front-end:
Envolvemos nossa função de update
original com uma função que envia alterações no conteúdo do carrinho para o back-end toda vez que o carrinho é atualizado:
updateOnServer msg model = let (newModel, have_to_send) = update msg model in case have_to_send of True -> -- send updated cart to server (!) newModel [ WebSocket.send server (encode 0 (CartEncoder.cart newModel.cart)) ] False -> -- do nothing (newModel, Cmd.none)
Também adicionamos um tipo de mensagem adicional de ConsumerCarts String
para receber atualizações do servidor e atualizar o modelo local de acordo.
A visualização foi atualizada para renderizar o resumo dos carrinhos de outras pessoas usando a função consumersCartsView
.
Uma conexão WebSocket foi estabelecida para assinar o back-end para ouvir as alterações nos carrinhos de outras pessoas.
subscriptions : Model -> Sub Msg subscriptions model = WebSocket.listen server ConsumerCarts server = "ws://127.0.0.1:8765"
Também atualizamos nossa função principal. Agora usamos Html.program
com parâmetros adicionais de init
e subscriptions
. init
especifica o modelo inicial do programa e subscription
especifica uma lista de subscrições.
Uma assinatura é uma maneira de dizermos ao Elm para ouvir as alterações em canais específicos e encaminhar essas mensagens para a função de update
.
main = Html.program { init = init , view = view , update = updateOnServer , subscriptions = subscriptions } init = ( Model [] -- empty cart [ Product "Bicycle" 100.50 -- stock , Product "Rocket" 15.36 , Product "Bisquit" 21.15 ] Nothing -- error (no error at beginning) [] -- consumer carts list is empty , Cmd.none)
Finalmente, tratamos da maneira como decodificamos a mensagem ConsumerCarts que recebemos do servidor. Isso garante que os dados que recebemos de fonte externa não interromperão o aplicativo.
ConsumerCarts message -> case decodeString (Json.Decode.list CartDecoder.cart) message of Ok carts -> ({ model | consumer_carts = carts }, False) Err msg -> ({ model | error = Just msg, consumer_carts = [] }, False)
Mantenha seus front-ends sãos
Elmo é diferente. Exige que o desenvolvedor pense de forma diferente.
Qualquer pessoa que venha do domínio do JavaScript e linguagens semelhantes se encontrará tentando aprender a maneira de fazer as coisas do Elm.
Em última análise, porém, o Elm oferece algo que outros frameworks - mesmo os mais populares - geralmente lutam para fazer. Ou seja, ele fornece um meio de criar aplicativos front-end robustos sem se envolver em um enorme código detalhado.
Elm também abstrai muitas das dificuldades que o JavaScript apresenta ao combinar um compilador inteligente com um depurador poderoso.
Elm é o que os desenvolvedores front-end anseiam há tanto tempo. Agora que você o viu em ação, experimente você mesmo e colha os benefícios construindo seu próximo projeto da web no Elm.