Introdução à linguagem de programação Elm

Publicados: 2022-03-11

Quando o desenvolvedor líder de um projeto muito interessante e inovador sugeriu mudar de AngularJS para Elm, meu primeiro pensamento foi: Por quê?

Já tínhamos um aplicativo AngularJS bem escrito que estava em estado sólido, bem testado e comprovado em produção. E o Angular 4, sendo uma atualização digna do AngularJS, poderia ter sido uma escolha natural para a reescrita – assim como o React ou o Vue. Elm parecia uma estranha linguagem específica de domínio da qual as pessoas mal ouviram falar.

Linguagem de programação Elm

Bem, isso foi antes de eu saber qualquer coisa sobre Elm. Agora, com alguma experiência com isso, especialmente depois de fazer a transição do AngularJS, acho que posso dar uma resposta a esse “porquê”.

Neste artigo, veremos os prós e contras do Elm e como alguns de seus conceitos exóticos atendem perfeitamente às necessidades de um desenvolvedor web front-end. Para um artigo de linguagem Elm mais semelhante a um tutorial, você pode dar uma olhada nesta postagem do blog.

Elm: uma linguagem de programação puramente funcional

Se você está acostumado a programar em Java ou JavaScript e sente que é a maneira natural de escrever código, aprender Elm pode ser como cair na toca do coelho.

A primeira coisa que você notará é a sintaxe estranha: sem chaves, muitas setas e triângulos.

Você pode aprender a viver sem chaves, mas como você define e aninha blocos de código? Ou, onde está o loop for , ou qualquer outro loop? Embora o escopo explícito possa ser definido com um bloco let , não há blocos no sentido clássico e nenhum loop.

Elm é uma linguagem de cliente web puramente funcional, fortemente tipada, reativa e orientada a eventos.

Você pode começar a se perguntar se a programação é possível dessa maneira.

Na realidade, essas qualidades se somam para lhe dar um paradigma incrível de programação e desenvolvimento de um bom software.

Puramente Funcional

Você pode pensar que usando a versão mais recente do Java ou ECMAScript 6, você pode fazer programação funcional. Mas, isso é apenas a superfície disso.

Nessas linguagens de programação, você ainda tem acesso a um arsenal de construções de linguagem e a tentação de recorrer às partes não funcionais dele. Onde você realmente percebe a diferença é quando você não pode fazer nada além de programação funcional. É nesse ponto que você eventualmente começa a sentir como a programação funcional pode ser natural.

Em Elm, quase tudo é uma função. O nome do registro é uma função, o valor do tipo de união é uma função — cada função é composta de funções parcialmente aplicadas a seus argumentos. Mesmo os operadores como mais (+) e menos (-) são funções.

Para declarar uma linguagem de programação como puramente funcional, ao invés da existência de tais construções, a ausência de todo o resto é de suma importância. Só então você pode começar a pensar de forma puramente funcional.

Elm é modelado em conceitos maduros de programação funcional e se assemelha a outras linguagens funcionais como Haskell e OCaml.

Fortemente tipado

Se você programa em Java ou TypeScript, sabe o que isso significa. Cada variável deve ter exatamente um tipo.

Algumas diferenças existem, é claro. Assim como no TypeScript, a declaração de tipo é opcional. Se não estiver presente, será inferido. Mas não existe um tipo “qualquer”.

Java suporta tipos genéricos, mas de uma maneira melhor. Os genéricos em Java foram adicionados posteriormente, portanto, os tipos não são genéricos, a menos que especificado de outra forma. E, para usá-los, precisamos da feia sintaxe <> .

Em Elm, os tipos são genéricos, a menos que especificado de outra forma. Vejamos um exemplo. Suponha que precisamos de um método que receba uma lista de um tipo específico e retorne um número. Em Java seria:

 public static <T> int numFromList(List<T> list){ return list.size(); }

E, na linguagem Elm:

 numFromList list = List.length list

Embora opcional, sugiro fortemente que você sempre adicione declarações de tipo. O compilador Elm nunca permitiria operações em tipos errados. Para um humano, é muito mais fácil cometer esse erro, especialmente enquanto aprende o idioma. Portanto, o programa acima com anotações de tipo seria:

 numFromList: List a -> Int numFromList list = List.length list

Pode parecer incomum no início declarar os tipos em uma linha separada, mas depois de algum tempo começa a parecer natural.

Idioma do cliente da Web

O que isso significa simplesmente é que o Elm compila para JavaScript, para que os navegadores possam executá-lo em uma página da web.

Dado isso, não é uma linguagem de propósito geral como Java ou JavaScript com Node.js, mas uma linguagem específica de domínio para escrever a parte cliente de aplicativos da web. Ainda mais do que isso, o Elm inclui escrever a lógica de negócios (o que o JavaScript faz) e a parte de apresentação (o que o HTML faz) – tudo em uma linguagem funcional.

Tudo isso é feito de uma maneira muito específica, semelhante a uma estrutura, chamada The Elm Architecture.

Reativo

A Elm Architecture é um framework web reativo. Quaisquer alterações nos modelos são renderizadas imediatamente na página, sem manipulação explícita do DOM.

Dessa forma, é semelhante a Angular ou React. Mas, Elm também faz isso à sua maneira. A chave para entender seus fundamentos está na assinatura das funções de view e update :

 view : Model -> Html Msg update : Msg -> Model -> Model

Uma visualização Elm não é apenas a visualização HTML do modelo. É HTML que pode produzir mensagens do tipo Msg , onde Msg é um tipo de união exato que você define.

Qualquer evento de página padrão pode produzir uma mensagem. E, quando uma mensagem é produzida, Elm chama internamente a função de atualização com essa mensagem, que então atualiza o modelo com base na mensagem e no modelo atual, e o modelo atualizado é novamente renderizado internamente na exibição.

Orientado a eventos

Muito parecido com JavaScript, Elm é orientado a eventos. Mas, diferentemente, por exemplo, do Node.js, onde retornos de chamada individuais são fornecidos para as ações assíncronas, os eventos Elm são agrupados em conjuntos discretos de mensagens, definidos em um tipo de mensagem. E, como em qualquer tipo de união, as informações que os valores de tipo separados carregam podem ser qualquer coisa.

Existem três fontes de eventos que podem produzir mensagens: ações do usuário na visualização Html , execução de comandos e eventos externos aos quais assinamos. É por isso que todos os três tipos, Html , Cmd e Sub contêm msg como argumento. E, o tipo genérico de msg deve ser o mesmo em todas as três definições - o mesmo fornecido para a função de atualização (no exemplo anterior, era o tipo Msg , com um M maiúsculo), onde todo o processamento de mensagens é centralizado.

Código fonte de um exemplo realista

Você pode encontrar um exemplo completo de aplicativo da web Elm neste repositório do GitHub. Embora simples, ele mostra a maioria das funcionalidades usadas na programação diária do cliente: recuperação de dados de um endpoint REST, decodificação e codificação de dados JSON, uso de visualizações, mensagens e outras estruturas, comunicação com JavaScript e tudo o que é necessário para compilar e empacotar Código Elm com Webpack.

Exemplo da Web do ELM

O aplicativo exibe uma lista de usuários recuperados de um servidor.

Para um processo de configuração/demonstração mais fácil, o servidor dev do Webpack é usado tanto para empacotar tudo, incluindo Elm, quanto para servir a lista de usuários.

Algumas funcionalidades estão em Elm e outras em JavaScript. Isso é feito intencionalmente por um motivo importante: para mostrar a interoperabilidade. Você provavelmente deseja experimentar o Elm para iniciar ou alternar gradualmente o código JavaScript existente para ele ou adicionar novas funcionalidades na linguagem Elm. Com a interoperabilidade, seu aplicativo continua a funcionar com código Elm e JavaScript. Esta é provavelmente uma abordagem melhor do que iniciar todo o aplicativo do zero no Elm.

A parte Elm no código de exemplo é inicializada primeiro com dados de configuração do JavaScript e, em seguida, a lista de usuários é recuperada e exibida na linguagem Elm. Digamos que temos algumas ações de usuário já implementadas em JavaScript, portanto, invocar uma ação de usuário em Elm apenas despacha a chamada de volta para ela.

O código também usa alguns dos conceitos e técnicas explicados na próxima seção.

Aplicação de Conceitos Elm

Vamos passar por alguns dos conceitos exóticos da linguagem de programação Elm em cenários do mundo real.

Tipo de União

Este é o ouro puro da língua Elm. Você se lembra de todas aquelas situações em que dados estruturalmente diferentes precisavam ser usados ​​com o mesmo algoritmo? É sempre difícil modelar essas situações.

Aqui está um exemplo: Imagine que você está criando paginação para sua lista. No final de cada página, deve haver links para as páginas anterior, seguinte e todas as páginas por seus números. Como você o estrutura para manter as informações de qual link o usuário clicou?

Podemos usar vários retornos de chamada para cliques anteriores, próximos e de número de página, ou podemos usar um ou dois campos booleanos para indicar o que foi clicado ou dar um significado especial a valores inteiros específicos, como números negativos, zero etc. essas soluções podem modelar exatamente esse tipo de evento de usuário.

Em Elm, é muito simples. Definiríamos um tipo de união:

 type NextPage = Prev | Next | ExactPage Int

E usamos como parâmetro para uma das mensagens:

 type Msg = ... | ChangePage NextPage

Por fim, atualizamos a função para ter um case para verificar o tipo de nextPage :

 update msg model = case msg of ChangePage nextPage -> case nextPage of Prev -> ... Next -> ... ExactPage newPage -> ...

Isso torna as coisas muito elegantes.

Criando Múltiplas Funções de Mapa com <|

Muitos módulos incluem uma função map , com diversas variantes para aplicar a um número diferente de argumentos. Por exemplo, List tem map , map2 , … , até map5 . Mas, e se tivermos uma função que recebe seis argumentos? Não há map6 . Mas, existe uma técnica para superar isso. Ele usa o <| função como parâmetro e funções parciais, com alguns dos argumentos aplicados como resultados intermediários.

Para simplificar, suponha que uma List tenha apenas map e map2 e queremos aplicar uma função que receba três argumentos em três listas.

Veja como fica a implementação:

 map3 foo list1 list2 list3 = let partialResult = List.map2 foo list1 list2 in List.map2 (<|) partialResult list3

Suponha que queremos usar foo , que apenas multiplica seus argumentos numéricos, definidos como:

 foo abc = a * b * c

Então o resultado de map3 foo [1,2,3,4,5] [1,2,3,4,5] [1,2,3,4,5] é [1,8,27,64,125] : List number .

Vamos desconstruir o que está acontecendo aqui.

Primeiro, em partialResult = List.map2 foo list1 list2 , foo é parcialmente aplicado a cada par em list1 e list2 . O resultado é [foo 1 1, foo 2 2, foo 3 3, foo 4 4, foo 5 5] , uma lista de funções que recebe um parâmetro (já que os dois primeiros já estão aplicados) e retorna um número.

A seguir em List.map2 (<|) partialResult list3 , na verdade é List.map2 (<|) [foo 1 1, foo 2 2, foo 3 3, foo 4 4, foo 5 5] list3 . Para cada par dessas duas listas, estamos chamando a função (<|) . Por exemplo, para o primeiro par, é (<|) (foo 1 1) 1 , que é o mesmo que foo 1 1 <| 1 foo 1 1 <| 1 , que é o mesmo que foo 1 1 1 , que produz 1 . Para o segundo, será (<|) (foo 2 2) 2 , que é foo 2 2 2 , que resulta em 8 e assim por diante.

Esse método pode ser particularmente útil com funções mapN para decodificar objetos JSON com muitos campos, pois Json.Decode os fornece até map8 .

Remover todos os valores de nada de uma lista de talvez

Digamos que temos uma lista de valores Maybe e queremos extrair apenas os valores dos elementos que possuem um. Por exemplo, a lista é:

 list : List (Maybe Int) list = [ Just 1, Nothing, Just 3, Nothing, Nothing, Just 6, Just 7 ]

E queremos obter [1,3,6,7] : List Int . A solução é esta expressão de uma linha:

 List.filterMap identity list

Vamos ver por que isso funciona.

List.filterMap espera que o primeiro argumento seja uma função (a -> Maybe b) , que é aplicada em elementos de uma lista fornecida (o segundo argumento), e a lista resultante é filtrada para omitir todos os valores Nothing e, em seguida, o real os valores são extraídos de Maybe s.

No nosso caso, fornecemos identity , então a lista resultante é novamente [ Just 1, Nothing, Just 3, Nothing, Nothing, Just 6, Just 7 ] . Após filtrá-lo, obtemos [ Just 1, Just 3, Just 6, Just 7 ] , e após a extração do valor, fica [1,3,6,7] , como desejávamos.

Decodificação JSON personalizada

Como nossas necessidades em decodificação JSON (ou desserialização) começam a exceder o que é exposto no módulo Json.Decode , podemos ter problemas para criar novos decodificadores exóticos. Isso ocorre porque esses decodificadores são invocados no meio do processo de decodificação, por exemplo, dentro de métodos Http , e nem sempre é claro quais são suas entradas e saídas, especialmente se houver muitos campos no JSON fornecido.

Aqui estão dois exemplos para mostrar como lidar com esses casos.

No primeiro, temos dois campos no JSON de entrada, a e b , denotando lados de uma área retangular. Mas, em um objeto Elm, queremos apenas armazenar sua área.

 import Json.Decode exposing (..) areaDecoder = map2 (*) (field "a" int) (field "b" int) result = decodeString areaDecoder """{ "a":7,"b":4 }""" -- Ok 28 : Result.Result String Int

Os campos são decodificados individualmente com o decodificador field int e, em seguida, ambos os valores são fornecidos à função fornecida em map2 . Como a multiplicação ( * ) também é uma função, e leva dois parâmetros, podemos usá-la assim. O areaDecoder resultante é um decodificador que retorna o resultado da função quando aplicada, neste caso, a*b .

No segundo exemplo, estamos recebendo um campo de status confuso, que pode ser nulo, ou qualquer string incluindo vazio, mas sabemos que a operação foi bem-sucedida apenas se estiver “OK”. Nesse caso, queremos armazená-lo como True e para todos os outros casos como False . Nosso decodificador fica assim:

 okDecoder = nullable string |> andThen (\ms -> case ms of Nothing -> succeed False Just s -> if s == "OK" then succeed True else succeed False )

Vamos aplicá-lo a alguns JSONs:

 decodeString (field "status" okDecoder) """{ "a":7, "status":"OK" }""" -- Ok True decodeString (field "status" okDecoder) """{ "a":7, "status":"NOK" }""" -- Ok False decodeString (field "status" okDecoder) """{ "a":7, "status":null }""" -- Ok False

A chave aqui está na função fornecida para andThen , que pega o resultado de um decodificador de string anulável anterior (que é um Maybe String ), o transforma no que precisarmos e retorna o resultado como um decodificador com a ajuda de succeed .

Dica importante

Como pode ser visto nesses exemplos, programar de maneira funcional pode não ser muito intuitivo para desenvolvedores Java e JavaScript. Leva algum tempo para se acostumar com isso, com muita tentativa e erro. Para ajudar a entender, você pode usar elm-repl para exercitar e verificar os tipos de retorno de suas expressões.

O projeto de exemplo vinculado anteriormente neste artigo contém vários outros exemplos de decodificadores e codificadores personalizados que também podem ajudá-lo a entendê-los.

Mas ainda assim, por que escolher Elm?

Sendo tão diferente de outras estruturas cliente, a linguagem Elm certamente não é “mais uma biblioteca JavaScript”. Como tal, possui muitos traços que podem ser considerados positivos ou negativos quando comparados a eles.

Vamos começar com o lado positivo primeiro.

Programação de cliente sem HTML e JavaScript

Finalmente, você tem uma linguagem onde você pode fazer tudo. Não há mais separação e combinações desajeitadas de sua mistura. Sem gerar HTML em JavaScript e sem linguagens de modelagem personalizadas com algumas regras de lógica simplificadas.

Com Elm, você tem apenas uma sintaxe e um idioma, em toda a sua glória.

Uniformidade

Como quase todos os conceitos são baseados em funções e algumas estruturas, a sintaxe é bastante concisa. Você não precisa se preocupar se algum método está definido na instância ou no nível de classe, ou se é apenas uma função. Todas são funções definidas no nível do módulo. E não há centenas de maneiras diferentes de iterar listas.

Na maioria das linguagens, sempre há essa discussão sobre se o código está escrito da maneira da linguagem. Muitas expressões idiomáticas precisam ser dominadas.

No Elm, se compilar, provavelmente é o modo “Elm”. Se não, bem, certamente não é.

Expressividade

Embora concisa, a sintaxe do Elm é muito expressiva.

Isso é alcançado principalmente através do uso de tipos de união, declarações de tipos formais e estilo funcional. Tudo isso inspira o uso de funções menores. No final, você obtém um código que é praticamente autodocumentado.

Não Nulo

Quando você usa Java ou JavaScript por muito tempo, null se torna algo completamente natural para você – uma parte inevitável da programação. E, embora vejamos constantemente NullPointerException se vários TypeError s, ainda não achamos que o problema real seja a existência de null . É tão natural .

Fica claro rapidamente depois de algum tempo com Elm. Não ter null não apenas nos livra de ver erros de referência nula em tempo de execução repetidas vezes, mas também nos ajuda a escrever um código melhor, definindo e lidando claramente com todas as situações em que podemos não ter o valor real, reduzindo assim a dívida técnica por não adiar null manusear até que algo se quebre.

A confiança de que vai funcionar

A criação de programas JavaScript sintaticamente corretos pode ser feita muito rapidamente. Mas, será que realmente funcionará? Bem, vamos ver depois de recarregar a página e testá-la completamente.

Com Elm, é o oposto. Com verificação de tipo estático e verificações null impostas, ele precisa de algum tempo para compilar, especialmente quando um iniciante escreve um programa. Mas, uma vez compilado, há uma boa chance de que funcione corretamente.

Rápido

Isso pode ser um fator importante ao escolher uma estrutura de cliente. A capacidade de resposta de um aplicativo da Web extenso geralmente é crucial para a experiência do usuário e, portanto, para o sucesso de todo o produto. E, como mostram os testes, Elm é muito rápido.

Prós do Elm vs Frameworks Tradicionais

A maioria dos frameworks web tradicionais oferece ferramentas poderosas para criação de aplicativos web. Mas esse poder tem um preço: arquitetura supercomplicada com muitos conceitos e regras diferentes sobre como e quando usá-los. Leva muito tempo para dominar tudo. Existem controladores, componentes e diretivas. Em seguida, há as fases de compilação e configuração e a fase de execução. Além disso, há serviços, fábricas e toda a linguagem de template personalizada usada nas diretivas fornecidas - todas aquelas situações em que precisamos chamar $scope.$apply() diretamente para atualizar a página e muito mais.

A compilação do Elm para JavaScript certamente também é muito complicada, mas o desenvolvedor está protegido de ter que conhecer todos os detalhes. Apenas escreva um pouco de Elm e deixe o compilador fazer seu trabalho.

E, por que não escolher Elm?

Chega de elogiar Elm. Agora, vamos ver um pouco do seu lado não tão bom.

Documentação

Esta é realmente uma questão importante. A linguagem Elm carece de um manual detalhado.

Os tutoriais oficiais apenas percorrem o idioma e deixam muitas perguntas sem resposta.

A referência oficial da API é ainda pior. Muitas funções carecem de qualquer tipo de explicação ou exemplos. E, depois, há aqueles com a frase: “Se isso é confuso, trabalhe através do Tutorial de Arquitetura Elm. Realmente ajuda!” Não é a melhor linha que você deseja ver nos documentos oficiais da API.

Espero que isso mude em breve.

Não acredito que Elm possa ser amplamente adotado com documentação tão pobre, especialmente com pessoas vindas de Java ou JavaScript, onde tais conceitos e funções não são nada intuitivos. Para compreendê-los, é necessária uma documentação muito melhor com muitos exemplos.

Formato e espaço em branco

Livrar-se de chaves ou parênteses e usar espaço em branco para recuo pode parecer bom. Por exemplo, o código Python parece muito legal. Mas para os criadores do elm-format , não foi suficiente.

Com todos os espaços de linha dupla e expressões e atribuições divididas em várias linhas, o código Elm parece mais vertical do que horizontal. O que seria uma linha no bom e velho C pode facilmente se estender para mais de uma tela na linguagem Elm.

Isso pode soar bem se você for pago por linhas de código escritas. Mas, se você quiser alinhar algo com uma expressão iniciada 150 linhas antes, boa sorte em encontrar o recuo correto.

Manipulação de Registros

É difícil trabalhar com eles. A sintaxe para modificar o campo de um registro é feia. Não há uma maneira fácil de modificar campos aninhados ou fazer referência a campos arbitrariamente por nome. E se você estiver usando as funções de acesso de maneira genérica, haverá muitos problemas com a digitação adequada.

Em JavaScript, um registro ou objeto é a estrutura central que pode ser construída, acessada e modificada de várias maneiras. Mesmo JSON é apenas uma versão serializada de um registro. Os desenvolvedores estão acostumados a trabalhar com registros em programação web, então as dificuldades em seu manuseio no Elm podem se tornar perceptíveis se forem usados ​​como estrutura de dados primária.

Mais digitação

Elm requer mais código para ser escrito do que JavaScript.

Não há conversão de tipo implícita para operações de string e número, portanto, muitas conversões int-float e especialmente chamadas toString são necessárias, o que requer parênteses ou símbolos de aplicativo de função para corresponder ao número correto de argumentos. Além disso, a função Html.text requer uma string como argumento. Muitas expressões case são necessárias, para todos aqueles Maybe s, Results , types, etc.

A principal razão para isso é o sistema de tipo estrito, e esse pode ser um preço justo a pagar.

Decodificadores e codificadores JSON

Uma área em que mais digitação realmente se destaca é o manuseio de JSON. O que é simplesmente uma chamada JSON.parse() em JavaScript pode abranger centenas de linhas na linguagem Elm.

Claro, é necessário algum tipo de mapeamento entre as estruturas JSON e Elm. Mas a necessidade de escrever decodificadores e codificadores para a mesma parte do JSON é um problema sério. Se suas APIs REST transferirem objetos com centenas de campos, isso dará muito trabalho.

Embrulhar

Já vimos sobre o Elm, é hora de responder às perguntas conhecidas, provavelmente tão antigas quanto as próprias linguagens de programação: é melhor que a concorrência? Devemos usá-lo em nosso projeto?

A resposta para a primeira pergunta pode ser subjetiva, pois nem toda ferramenta é um martelo e nem tudo é um prego. O Elm pode brilhar e ser uma escolha melhor em relação a outras estruturas de cliente da Web em muitos casos, enquanto fica aquém em outros. Mas oferece um valor realmente único que pode tornar o desenvolvimento de front-end da Web muito mais seguro e fácil do que as alternativas.

Para a segunda pergunta, para evitar responder também com o antigo “depende”, a resposta simples é: sim. Mesmo com todas as desvantagens mencionadas, apenas a confiança que o Elm lhe dá sobre o seu programa estar correto é motivo suficiente para usá-lo.

Codificar em Elm também é divertido. É uma perspectiva totalmente nova para quem está acostumado com os paradigmas de programação web “convencionais”.

Em uso real, você não precisa mudar seu aplicativo inteiro para o Elm imediatamente ou iniciar um novo completamente nele. Você pode aproveitar sua interoperabilidade com JavaScript para experimentá-lo, começando com uma parte da interface ou alguma funcionalidade escrita na linguagem Elm. Você descobrirá rapidamente se ele atende às suas necessidades e, em seguida, ampliará seu uso ou o abandonará. E quem sabe, você também pode se apaixonar pelo mundo da programação web funcional.

Relacionado: Desvendando o ClojureScript para desenvolvimento front-end