Baseado na palestra que ofereci no encontro do Grupy-SP, em 17 de setembro de 2016. O código dessa atividade está disponível no GitHub.
O problema que o Elm resolve
Para entender o porquê eu gosto do Elm precisamos falar sobre duas coisas: JavaScript e DOM.
Precisamos falar sobre JavaScript
Alguém aqui gosta de JavaScript? Eu confesso que dou risada com algumas coisas do JavaScript:
> {} + {}
NaN
> {} + []
0
> [] + {}
"[object Object]"
> [] + []
""
Mas deixemos os memes de canto. Arrisco dizer que quem gosta de Python não gosta de JavaScript por três motivos:
- Existem muitas formas de fazer a mesma coisa, nem todas são óbvias e nem todas funcionam em todos os navegadores. Por exemplo, aqui temos uma lista de 535 formas de recarregar uma página. Bateu aquela saudades do there should be one – and preferably only one – obvious way to do it, né?
- Debugar JavaScript é difícil pois as mensagens de erro padrão são péssimas. Por exemplo, tentar pegar o primeiro elemento de uma lista vazia, no JavaScript, vai te retornar apenas
undefined
. Bateu saudades doIndexError: list index out of range
, né? - O código é verboso demais no JavaScript — mas reconheço que isso é muito subjetivo. De qualquer forma, quem está acostuamdo com as list comprehensions do Python acha um absurdo usar
for (var i = 0; i < myList.length; i++) { … }
.
“Maybe this new JavaScript framework will compensate for the fact I haven’t actually learned JavaScript properly” - every front-end dev.
— I Am Devloper (@iamdevloper) September 22, 2015
Mas não podemos nos livrar do JavaScript – pelo menos não tão cedo. Ele roda em todos os navegadores, assim é a linguagem padrão disponível para UI e UX na web, seja em computadores, tablets ou celulares.
E, por sorte, existem coisas boas no JavaScript!
Esse livro da foto, e essa palestra do mesmo autor, desmisitificam uma crítica muito comum: JavaScript é lento. Não é. O que é lento é o DOM, ou, mais precisamente, alterar o DOM. Então vamos falar sobre o DOM.
Precisamos falar sobre DOM
O problema com o DOM é que a cada alteração mínima na página, o JavaScript tem que processar a nova entrada, pedir permissão ao navegador para alterar o DOM, destruir o nó que vai ser substituído no DOM, criar o novo nó, recalcular como renderizar a página atualizada (incluindo processar todos os estilos CSS), avisar o navegador que a mudança foi feita e atualizar as referências ao DOM no JavaScript. Tudo isso, digamos, a cada mudança que ocorre na tela (sem que a página toda seja recarregada).
Imagine você no Gmail ou Facebook: uma mensagem nova chega no chat, tem uma notificação com o número de mensagens que aparece, a janela de chat aparece e pisca, o nome da pessoa fica diferente na lista de amigos. E ao mesmo tempo a página segue mudando com mais um email, mais um comentário, etc. Cada coisinha dessa requereria uma sequência como a do parárafo anterior. É muita alteração no DOM e isso tende a ser custoso.
Para resolver esse problema a ideia foi criar um DOM virtual. A cada mensagem nova no chat, ao invés do JavaScript sair alterando cada coisinha no DOM, primeiro ele processa todas as alterações em um DOM virtual. Depois ele compara o DOM virtual com o DOM real e altera-o uma única vez. Essa é a estratégia de vários frameworks como o React e o Vue, ou mesmo a estratégia de front-end do AngularJS ou do Ember.js.
“The Top 100 JavaScript Frameworks of 2015″
— I Am Devloper (@iamdevloper) November 2, 2015
ಠ_ಠ this is an issue.
Isso resolveu o problema do custo da alteração do DOM. Mas não resolveu a nossa dependência do JavaScript para isso. Até que chegou o Elm.
Bem vindo, Elm
O Elm chegou com várias promessas ótimas. Ele promete ser mais rápido que as alternativas mencionadas anteriormente:
E, entre outras coisas, promete no runtime exceptions, ou seja, sem erros na hora do usuário executar a aplicação. A NoRedInk, pioneira na adoção do Elm, tem uma aplicação em Elm de mais ou menos 36 mil linhas, rodando há 1 ano. Zero erros reportados.
Mas… como o Elm consegue? Esse é o foco da palestra.
Primeiros passos
O primeiro passo para entender como o Elm consegue resolver esse problema é entender que ele não é só uma linguagem que compila para JavaScript. Ele oferece um ambiente de desenvolvimento único, sem paralelos com JavaScript. No final, por acaso, ele vira um .js
para você integrar na aplicação. Por acaso pois ele não depende do JavaScript e, se um dia os navegadores suportarem outra linguagem, ou mesmo Elm, o JavaScript some de cena sem deixar vestígios no ambiente de desenvolvimento Elm.
Elm é uma outra linguagem, com outra lógica, e com compilador próprio. Ela é uma linguagem funcional, trabalha com constantes e expressões — sempre.
Conhecendo a sintaxe e as mensagens de erro
Depois de instalar o Elm, vamos brincar um pouco para sentir como ele é. Faremos isso abrindo o console, o read–eval–print loop, com $ elm-repl
:
> "Ahoy"
"Ahoy" : String
> 3.1415
3.1415 : Float
> ["Ahoy", "Cap'n"]
["Ahoy","Cap'n"] : List String
No Elm os tipos importam muito. E ele usa isso para verificar várias possibilidades de erro no código — e não compila enquanto você não resolver esses problemas em potencial. Vamos a alguns exemplos:
Que tal tentar juntar um número inteiro com uma string?
> 42 + "Ahoy"
-- TYPE MISMATCH --------------------------------------------- repl-temp-000.elm
The right argument of (+) is causing a type mismatch.
3| 42 + "Ahoy"
^^^^^^
(+) is expecting the right argument to be a:
number
But the right argument is:
String
Hint: To append strings in Elm, you need to use the (++) operator, not (+).
<http://package.elm-lang.org/packages/elm-lang/core/latest/Basics#++>
Hint: I always figure out the type of the left argument first and if it is
acceptable on its own, I assume it is "correct" in subsequent checks. So the
problem may actually be in how the left and right arguments interact.
Reparem como a mensagem de erro é clara: TYPE MISMATCH
te diz qual o tipo do erro (o tipo de um valor não é o que o compilador espera), tem um ^^^^^^
sublinhando, na devida linha, qual valor é esse. Ainda tem uma dica: para concatenar texto, use ++
ao invés de +
. E, por fim, um exclarecimento: ele diz que é o texto, e não o número, que ele acredita ser o problema pois ele começa avaliando o valor da esquerda, sempre. Se a gente tivesse colocado "Ahoy" ++ 42
, ele reclamaria que o 42
não é texto.
Outro exemplo: o Maybe
. Imagine uma cenário onde, por algum motivo, você precisa do primeiro elemento de uma lista. Mas imagine que por algum motivo (talvez ainda desconhecido) aquela lista chegou ali vazia. No Python temos um IndexError
, como já vimos. E no Elm?
> import List
> List.head [1, 2, 3]
Just 1 : Maybe.Maybe number
Faríamos assim com uma lista contendo três elementos. Mas repare no detalhe do tipo que essa função retornou, Just 1
, e no tipo que essa mesma função retorna quando passamos uma lista vazia, o Nothing
:
> List.head []
Nothing : Maybe.Maybe a
O Elm sabe que uma lista pode ser vazia. E não te deixa esquecer disso. Quando você for usar um valor que vem de uma lista, você tem que prever esse cenário. Por isso ele não retorna um número, no nosso caso, logo de cara. Ele retorna Just 1
ou Nothing
— sendo que 1
é o primeiro item da nossa lista.
Se quisermos somente o número, sem o tipo Just <número>
, podemos dizer qual é o valor padrão:
> Maybe.withDefault 0 (List.head [])
0 : number
É com estruturas e lógicas como essa que o Elm consegue prometer – e cumprir — a promessa de não deixar passar erros.
Compilando e colocando a mão na massa
Mas e como desenvolvemos algo de verdade? Bom, vamos por partes.
Comecemos com um arquivo simples, Main.elm
:
module Main exposing (..)
import Html exposing (text)
main =
text "Ahoy"
Todo arquivo Elm que vai ser compilado espera uma função main
. E essa função main
tem que retornar um Html
— afinal, o Elm é pensando para produzir interfaces para navegadores.
Então importamos uma função, text
, que retorna um Html
. Depois definimos que a função main
: ela retorna seja lá o que for que aquela função Html.text
produzir quando passarmos a ela um texto "Ahoy"
.
A primeira linha é padrão em todo arquivo Elm: você dá um nome ao módulo que está criando ali naquele arquivo (e esse nome tem que bater com o nome do arquivo). O (..)
define o que desse módulo é acessível externamente — algo com o que não precisamos nos preocupar agora (..
define que tudo é acessível externamente).
Feito isso, é só compilar: $ elm-make Main.elm
. Inspecionando o diretório, vamos ver quatro arquivos:
Main.elm
é o nosso código fonte.index.html
é o nosso código compilado em HTML, com o JavaScript embutido, pronto para rodar no navedagor — não tenha medo, abra para ver como ficou!elm-package.json
eelm-stuff
são criados pelo próprio Elm para controlar teu projeto.
Se quisermos compilar somente o JavaScript, para incluí-lo no HTML separadamente, podemos: $ elm-make Main.elm --output app.js
gera o arquivo app.js
, que pode ser incluído em qualquer HTML posteriormente.
Para desenvolvermos — e brincarmos — temos ainda o $ elm-reactor
, uma ferramenta que faz tudo isso automaticamente para você poder se focar no que importa: escrever código. Vamos usar o Reactor logo logo.
A chave de ouro: a arquitetura do Elm
Já vimos que o compilador ajuda o Elm a não deixar passar chances de erro. Já vimos que o Elm controla o tipo de cada valor para ter certeza que as funções recebem os argumentos do tipo correto e sempre retornam algo esperado. Mas ainda não falamos do principal: a arquitetura do Elm.
Essa arquitetura foi tão bem aceita que até quem não programa em Elm se inspirou nela: o Redux, muito usado no mundo do ReactJS, se inspirou na arquitetura do Elm (eles contam isso no GitHub, e na documentação).
Nos termos do Elm isso quer dizer que toda aplicação é organizada em torno de três objetos principais:
- Um modelo (normalmente um tipo
Model
) com os dados necessários para renderizar uma interface. - Uma função (
view
) que recebe como argumento um modelo e retorna um HTML de como aqueles dados são renderizados (na verdade aqui estamos falando do móduloHtml
do Elm, e não de um arquivo.html
). - Uma função (normalmente
update
) que recebe como argumento uma mensagem e um modelo, e retorna um novo modelo com as informações atualizadas de acordo com a mensagem recebida (uma mensagem pode ser, por exemplo, um clique no botão Enviar em um espaço de comentários).
Vamos ver como isso funciona abrindo o $ elm-reactor
no terminal e abrindo a URL do servidor que ele cria (localhost:8000
) no navegador.
Clicando em Main.elm
(ou em qualquer arquivo Elm) o Reactor já compila e mostra o resultado no navegador. Então vamos começar a aprender um pouco mais de Elm e testar sua arquitetura criando uma caixa de comentários.
Nosso modelo de dados
Vamos pensar quais dados precisamos para ter uma caixa de comentário:
- Cada comentário tem que ter dois campos de dados, um para armazenar conteúdo do comentário e outro para armazenar o nome de quem fez esse comentário.
- Precisamos de um lugar para guardar cada um dos comentários; uma sequência de elementos que tem a mesma estrutura.
- E ainda precisamos de um lugar para guardar um comentário novo enquanto ele é digitado. Na UI vai ser o
textarea
do nosso HTML, mas precismaos trazer ele para o nosso modelo.
Com isso, vamos começar a editar nosso Main.elm
criando um tipo específico para nossos comentários:
type alias Comment =
{ author : String
, contents : String
}
O Elm já tem vários tipos de dados como Integer
, Float
, String
, List
, etc. Ele também nos permite criar nossos próprios tipos. Para isso usamos type alias
, estamos combinando tipos existentes para formar um novo tipo que batizamos de Comment
. Assim, quando passarmos um comentário como argumento para alguma função, o compilador sabe que a descrição desse tipo é essa: um Record (tecnicamente é isso que criamos utilizando as chaves) que tem dois campos nomeados, um chamado author
e que será do tipo texto (String
), e outro, também do tipo texto, que é o contents
.
E vamos criar também um modelo nosso:
type alias Model =
{ new = Comment
, comments = List Comment
}
Temos duas novidades aqui: primeiro, uma vez que já temos um tipo Comment
, criado no passo anterior, já podemos utilizá-lo para criar novos tipos; segundo, quando definimos uma lista, temos que informar qual o tipo dos elementos dessa lista — logo, ao contrário do Python, uma lista em Elm não pode ter tipos misturados. Por exemplo, ao tentarmos criar umas lista com "Ahoy"
(texto) e 42
(número inteiro), temos um erro que diz que o primeiro e o segundo elementos da lista não são do mesmo tipo:
> ["Ahoy", 42]
-- TYPE MISMATCH --------------------------------------------- repl-temp-000.elm
The 1st and 2nd elements are different types of values.
3| ["Ahoy", 42]
^^
The 1st element has this type:
String
But the 2nd is:
number
Hint: All elements should be the same type of value so that we can iterate
through the list without running into unexpected values.
Por isso definimos que nosso modelo tem dois campos: um new
onde guardamos nome do autor do comentário e o conteúdo do comentário; e uma lista de comentários na qual cada um tem seu próprio autor e conteúdo.
Podemos declarar uma constante que seria o equivalente a uma caixa de comentários vazia:
initialModel =
{ new =
{ author = ""
, contents = ""
}
, comments = []
}
Ou, por exemplo, uma em que já teríamos dois comentários postados, e um sendo digitado:
initialModel =
{ new =
{ author = "John Doe"
, contents = "Ahoy, cap'n"
}
, comments =
[ { author = "Joane Doe"
, contents = "What be happenin', matey?"
}
, { author = "Buccaneer"
, contents = "What say ye, ya scurvy dog?"
}
]
}
Ainda podemos, antes de declarar a constante, especificar que ela deve seguir os tipos do nosso Model
criado anteriormente:
initialModel : Model
initialModel =
{ new =
{ author = ""
, contents = ""
}
, comments = []
}
A vantagem de fazer isso é que se errarmos na hora de criar nosso initialModel
, o Elm nos avisa e não deixa o erro passar. Se, por engano, digitarmos name
ao invés de author
, o Elm pega:
Detected errors in 1 module.
-- TYPE MISMATCH ------------------------------------------------------ Main.elm
The type annotation for `initialModel` does not match its definition.
18| initialModel : Model
^^^^^
The type annotation is saying:
{ ..., new : { ..., author : ... } }
But I am inferring that the definition has this type:
{ ..., new : { ..., name : ... } }
Pronto, já temos nosso modelo declarado e um valor inicial para ele. Se você se perdeu, é isso que adicionamos ao nosso Main.elm
:
type alias Comment =
{ author : String
, contents : String
}
type alias Model =
{ new : Comment
, comments : List Comment
}
initialModel : Model
initialModel =
{ new =
{ author = ""
, contents = ""
}
, comments = []
}
Nossa função view
Agora vamos substituir aquele text "Ahoy"
por uma função view
. Essa função recebe um modelo e retorna um HTML. No Elm temos o módulo Html
com praticamente todos os tags do HTML nativo, assim podemos escrever nossa própria interface em Elm. No módulo de HTML do Elm toda tag é uma função que leva como argumento duas listas: uma com os atributo que essa tag leva e outro com os nós que ficam dentro dela.
Vamos a alguns exemplos:
import Html exposing (p, text)
p [] [ text "Ahoy" ]
Isso no final das contas é uma tag p
, sem nenhum atributo e tem um nó de texto com Ahoy
dentro. Ou seja, <p>Ahoy</p>
.
import Html exposing (p, text)
import Html.Attributes exposing (class)
p [ class "alert" ] [ text "Ahoy" ]
Agora esse código recebeu uma lista não-vazia de argumentos, logo equivale a <p class="alert">Ahoy</p>
.
import Html exposing (p, strong, text)
import Html.Attributes exposing (class)
p
[ class "alert" ]
[ text "Ahoy, "
, strong [] [ text "cap'n" ]
, text "!"
]
Agora temos uma tag dentro de outra: <p class="alert">Ahoy, <strong>cap'n</strong>!</p>
.
É essa mesma estrutura que vamos utilizar na nossa view
. A assinatura dela é receber um Model
e devolver esses elementos de HTML, o que em Elm dizemos assim:
view : Model -> Html.Html a
Esse Html.Html a
parece um pouco complicado. Basicamente o Html.Html
quer dizer o tipo Html
dentro do módulo Html
. Já o a
vamos usar quando criarmos nossa função update
— guarde ele aí. O importante é entender que essa função vai receber um modelo e devolver HTML.
Vamos, por enquanto, só renderizar um parágrafo dizendo Temos 0 comentários, Temos 1 comentário, Temos x comentários, de acordo com quanto elementos tivermos no nosso modelo.
O módulo de listas do Elm pode nos facilitar a vida. Ele tem a função List.length
que recebe uma lista e retorna um número inteiro (no formato da assinatura do Elm, List a -> Int
).
Então o texto que queremos mostrar vai ser a junção de duas coisas: o valor retornado por List.length
, transformado para texto, mais o texto comentários
(depois cuidamos do plural). Isso quer dizer algo como:
view : Model -> Html.Html a
view model =
p [] [ text ((toString (List.length model.comments)) ++ " comentários") ]
Esse código está péssimo. Muito fácil se perder nos parênteses. Vamos usar uma estrutura do Elm pra facilitar isso, depois explicamos mais sobre ela:
view : Model -> Html.Html a
view model =
let
count =
List.length model.comments
phrase =
(toString count) ++ " comentários"
in
p [] [ text phrase ]
Melhorou, né? A primeira linha é a assinatura que já falamos. A segunda linha define uma função view
que recebe um único argumento (model
). Para simplificar vamos dizer que tudo que está dentro do let
são constantes que só serão válidas dentro do in
subsequente.
Dentro do let
temos um número inteiro count
, que é transformado para texto quando o utilizamos na constante phrase
.
Se quiser ver nosso modelo e view funcionando, é só trocar main
por:
main =
view initialModel
No navegador já é possível ver 0 comentários, ou 2 comentários se usar nossa versão preenchida do initialModel
.
Antes de fechar nossa view
precisamos fazer três coisas:
- Cuidar do plural (comentários) e do singular (comentário) no código que já temos
- Incluir um formulário para receber novos comentários
- Incluir os comentários, caso existam
Plural e singular
Essa é a mais simples: o que precisamos é de uma função Int -> String
, ou seja, passamos um número inteiro (0
, ou 1
, ou 2
etc.) e ela nos devolve "comentário"
quando passamos 1
, ou "comentários"
quando passamos qualquer outro número:
pluralize : Int -> String
pluralize count =
if count == 1 then
"comentário"
else
"comentários"
E já podemos usá-la na nossa view
substituindo a antiga phrase
por:
phrase =
(toString count) ++ " " ++ pluralize count
Mostrando um formulário
Nosso formulário só vai ganhar vida quando tivermos nossa função de update
que vai pegar o que for digitado e salvar no modelo. Então por enquanto é só usar o HTML do Elm. Expomos mais algumas tags e atributos e ficamos com esse resultado:
import Html exposing (br, button, div, form, input, p, text, textarea)
import Html.Attributes exposing (value)
--
-- ...
--
view : Model -> Html.Html a
view model =
let
count =
List.length model.comments
phrase =
(toString count) ++ " " ++ pluralize count
in
div
[]
[ p [] [ text phrase ]
, form
[]
[ input [ value model.new.author ] []
, br [] []
, textarea [ value model.new.contents ] []
, br [] []
, button [] [ text "Enviar" ]
]
]
São muitas linhas novas, mas é só HTML escrito de uma forma um pouco diferente: usando função, lista de atributos, e lista de nós (de outros tags HTMLs). A única coisa a se atentar ali é que usamos o new
do nosso modelo para preencher o input
e o textarea
— se você usar a versão preenchida do initalModel
, já vai ver no navegador um pirata comentando algo ali.
Mostrando os comentários
Para começar vamos fazer uma nova função que recebe um comentário só (tipo Comment
que criamos) e retorna HTML (Comment -> Html.Html. a
):
viewComment : Comment -> Html.Html a
viewComment comment =
p
[]
[ text (comment.author ++ ":")
, br [] []
, text comment.contents
]
Isso vai gerar algo como <p>Buccaneer:<br>What say ye, ya scurvy dog?</p>
.
Agora vamos usar o List.map
para aplicar essa função em cada um dos comentários existentes no nosso modelo. Essa função recebe dois argumentos: o primeiro é uma outra função, o segundo é uma lista. O List.map
aplica esse função a cada um dos elementos da lista e retorna uma nova lista com os resultados.
Na prática a única novidade na nossa view
é o div [] (List.map viewComment model.comments)
:
view : Model -> Html.Html a
view model =
let
count =
List.length model.comments
phrase =
(toString count) ++ " " ++ pluralize count
in
div
[]
[ p [] [ text phrase ]
, div [] (List.map viewComment model.comments)
, form
[]
[ input [ value model.new.author ] []
, br [] []
, textarea [ value model.new.contents ] []
, br [] []
, button [] [ text "Enviar" ]
]
]
Dando vida aos comentários
A função update
é a mais complexa. Ela recebe um modelo e uma mensagem, para retornar uma versão atualizada do modelo. Por exemplo, nosso tutorial terá três mensagens:
- a ação de atualizar um novo nome em um novo comentário (ou seja, guardar o que for digitado no modelo no
Model.new.author
); - a ação de digitar um novo comentártio (ou seja, guardar o que for digitado no modelo, no
Model.new.contents
); - e a ação de postar um novo comentário (adicionar o conteúdo do
Model.new
à listaModel.comments
e limpar oModel.new
).
type Msg = UpdateAuthor String | UpdateContents String | PostComment
Criamos um novo tipo, o Msg
. Não é um type alias
como antes pois não estamos criando tipos com base nos que já existem. Estamos literalmente criando tipos novos. E o que estamos dizendo é que o tipo Msg
só pode ser três coisas:
- ou um
UpdateAuthor
acompanhando de um texto; - ou um
UpdateContents
acompanhando de um texto; - ou um `PostComment.
Essas são as mensagens que precisamos e é o nosso HTML (o tipo Html.Html
, no caso) que vai produzí-las. Esse é o significado do a
na assinatura da nossa view
: qual o tipo de mensagem que nosso HTML vai produzir? Agora podemos atualizar nossa view
e especificar que será uma dessas mensagens que definimos no tipo Msg
(e podemos fazer o mesmo com a viewComments
):
view : Model -> Html.Html Msg
Falando na view
, podemos importar alguns eventos do módulo Html
: o onInput
para quando tiver qualquer alteração nos dados do input
ou do textarea
, e o onSubmit
, que captura o momento que um formulário foi enviado.
import Html.Events exposing (onInput, onSubmit)
--
-- ...
--
view : Model -> Html.Html Msg
view model =
let
count =
List.length model.comments
phrase =
(toString count) ++ " " ++ pluralize count
in
div
[]
[ p [] [ text phrase ]
, div [] (List.map viewComment model.comments)
, form
[ onSubmit PostComment ]
[ input [ value model.new.author, onInput UpdateAuthor ] []
, br [] []
, textarea [ value model.new.contents, onInput UpdateContents ] []
, br [] []
, button [] [ text "Enviar" ]
]
]
Não se perca. Foram três alterações:
- Adicionamos
onSubmit PostComment
como um atributo doform
; - Adicionamos
onInput UpdateAuthor
como segundo atributo doinput
; - E adicionamos
onInput UpdateContents
como segundo atributo dotextarea
.
Assim, quando o usuário interagir com o formulário o aplicativo já dispara as mensagens para o update
. O que está faltando é escrever o que acontece em cada caso.
No caso do UpdateAuthor
ou UpdateContents
a ideia é pegar o texto que vier junto com a mensagem e atualizar, respectivamente, o new.author
e o new.comments
. Feito isso é só retornar uma nova versão do modelo. Vamos ver como começamos:
update : Msg -> Model -> Model
update msg model =
case msg of
UpdateAuthor value ->
let
new =
model.new
updated =
{ new | author = value }
in
{ model | new = updated }
UpdateContents value ->
model
PostComment ->
model
Temos a nossa função update
cuidando de somente um caso. Ela recebe a mensagem (msg
) e o modelo (model
), e verifica qual o tipo da mensagem (case msg of
). Caso a mensagem seja do tipo UpdateContents
ou PostComment
ela retorna o modelo sem mudar nada (implementaremos isso já já). Mas caso a mensagem seja do tipo UpdateAuthor
ela segue a expressão let
/in
declarada aí. A sintaxe é nova, então vamos com calma!
Imagine que você tenha esse Comment
:
myComment : Comment
myComment =
{ author = "Buccaneer"
, contents = "What say ye, ya scurvy dog?"
}
Se você quiser atualizar esse Comment
para obter um cujo o author
seja "Scurvy dog"
, você poderia trocar o author
e manter o contents
:
myNewComment : Comment -> Comment
myNewComment comment =
{ author = "Scurvy dog"
, contents = comment.contents
}
Isso é simples em um Record
que não tem muitos campos mas imagine como seria a repetição caso nosso Comment
tivesse mais de 25 campos? É para isso que a sintaxe que usamos serve, para manter tudo intacto, menos menos o que for declarado após o pipe (|
). Por exemplo, essa função abaixo faz a mesma coisa que a de cima: mantém todos os campos intactos, salvo o author
.
myNewComment : Comment -> Comment
myNewComment comment =
{ comment | author = "Scurvy dog" }
É isso que usamos na nossa update
:
- Dentro do
let
isolamos onew
(comnew = model.new
). - Criando a constante
updated
atualizamos somente o campoauthor
donew
(comupdated = { new | author = value }
, sendo quevalue
é o texto que veio junto com a mensagemUpdateAuthor String
). - E depois, no
in
atualizamos o próprio modelo com{ model | new = updated }
.
Para fazer o UpdateContents
, a lógica é a mesma. Para fazer PostComment
vamos adicionar o new
como um novo elemento da lista comment
, depois limpar os campos do new
. Juntando tudo, temos:
update : Msg -> Model -> Model
update msg model =
case msg of
UpdateAuthor value ->
let
new =
model.new
updated =
{ new | author = value }
in
{ model | new = updated }
UpdateContents value ->
let
new =
model.new
updated =
{ new | contents = value }
in
{ model | new = updated }
PostComment ->
let
comments =
List.append model.comments [ model.new ]
in
{ model | new = Comment "" "", comments = comments }
Alguns detalhes:
Comment "" ""
é um atalho para{ author = "", contents = "" }
— qualquer tipo criado também cria seu construtor, que por sua vez recebe os argumentos na ordem em que o tipo foi criado (por exemploComment "Scurvy dog" ""
cria{ author = "Scurvy dog", contents = "" }
, enquantoComment "" "Ahoy"
cria{ author = "", contents = "Ahoy" }
).List.append
espera duas listas como argumento, então envolvemosmodel.new
nos colchetes, ou seja, passamos ele para uma lista de um elemento só ([ model.new ]
) pois oList.append
não aceitariaComment
como segundo argumento (ou seja, o correto éList.append model.comments [ model.new ]
e nãoList.append model.comments model.new
).
Ligando os pontos
Depois dessa complicação toda da função update
você abriu o navegador e não funcionou ainda… que desastre de tutorial.
O que falta é criar uma aplicação que liga os fios entre o modelo, a update
e a view
. Vamos importar esse aplicativo atualizar nossa main
:
import Html.App
--
-- ...
--
main : Program Never
main =
Html.App.beginnerProgram
{ model = initialModel
, view = view
, update = update
}
Que tal brincar no navegador agora?