Existem inúmeros frameworks Haskell para o desenvolvimento de aplicações Web e, como tudo nessa vida, todos eles possuem seus prós e contras. Dentre as muitas opções, o Scotty se destaca por ser um framework simples, prático e que faz pouco uso de Haskell magic — sendo uma espécie de Sinatra do Haskell.

Por isso, nós usaremos o Scotty neste tutorial, onde você aprenderá a implementar uma API REST que expõe quatro operações básicas (CRUD) sobre um tipo de dados e que usa o formato de dados JSON para a troca de informações entre cliente e servidor.

Inspirados pelo famigerado método Ivy Lee de produtividade, nós criaremos juntos uma aplicação chamada Mavins Lee, que nada mais é do que uma versão gourmetizada de uma “lista de tarefas”. Sério, se eu fosse você, nem perderia meu tempo lendo sobre o Ivy Lee, mas convenhamos que esse é um nome muito mais legal do que Task List App

Vá Direto ao Assunto…

Configurando o Ambiente

Para os apressadinhos, eu disponibilizei todo o código-fonte deste tutorial no GitHub junto com as instruções de build e execução. Continue nesta seção apenas se quiser aprender a criar o projeto do zero. Caso contrário, pode pular para a próxima.

Usaremos o template scotty-hello-world do Stack para inicializar nosso projeto:

1
2
3
$ mkdir -p ~/Development/haskell/mavins-lee/
$ cd ~/Development/haskell/mavins-lee/
$ stack --resolver lts-9.18 new mavins-lee --bare scotty-hello-world

O comando acima criará os seguintes arquivos:

1
2
3
4
5
6
7
$ tree
.
├── Main.hs
├── mavins-lee.cabal
└── stack.yaml

0 directories, 3 files

Agora vamos tentar fazer o build da aplicação e executá-la:

1
2
$ stack build
$ stack exec -- mavins-lee

Se tudo estiver certo, sua aplicação estará disponível no endereço http://localhost:3000.

Habilitando um Pseudo Hot-Deploy

Se você olhar, até então, o arquivo Main.hs tem apenas uma rota cadastrada:

1
2
3
get "/:word" $ do
    beam <- param "word"
    html $ mconcat ["<h1>Scotty, ", beam, " me up!</h1>"]

O funcionamento da rota /:word é fácil de entender: ela obtém a string imediatamente após o “/” na URL usando a função param; armazena o seu conteúdo na “variável” beam; concatena beam a duas outras strings; e retorna o resultado como HTML.

Experimente acessar os endereços /wake ou /lift e veja o que acontece…

Agora, com o servidor ainda em execução, vamos modificar um pouco essa rota:

1
2
3
get "/:name" $ do
    name <- param "name"
    html $ mconcat ["<h1>Olá, ", name, "!</h1>"]

E acessar /Alexandre, /Ingrid, /SeuNomeAqui

Ué… O resultado não foi bem o que esperávamos, né?

Diferentemente do Python, Haskell é uma linguagem compilada. Em teoria, isso significa que toda vez que nós mexermos no código será necessário refazer o build e reexecutar a aplicação (talvez você já tenha vivido isso no Java, que também é assim)…

Acontece que matar a aplicação e executar stack build && stack exec -- mavins-lee toda santa vez que alterarmos alguma coisa, além de tedioso, é extremamente improdutivo! Mas aqui vai a dica: carregue o código no GHCi.

1
2
3
$ stack ghci
...
ghci> main

Assim, quando você mudar o código, basta dar um Ctrl-C no GHCi, recarregar o código digitando :r e chamar a main novamente!

Resolvido! Agora nós podemos ver nossas modificações (quase) on-the-fly.

Obs. Eu procurei outras formas: o reserve funcionou de forma limitada, o wai-handler-devel está deprecated e esta alternativa pareceu desnecessariamente complicada para este tutorial.

Tutorial: Scotty em 5 Minutos!

5 minutos é muito… Corre que você aprende em 1 minuto!!

Roteamento

O Scotty suporta requisições GET, POST, PUT e DELETE:

1
2
3
4
5
6
7
8
9
10
11
get "/" $ do
    text "Recebi um GET."

post "/" $ do
    text "Recebi um POST."

put "/" $ do
    text "Recebi um PUT."

delete "/" $ do
    text "Recebi um DELETE."

Também é possível especificar rotas que casam com qualquer método HTTP:

1
2
matchAny "/any" $ do
    text "Posso ter recebido qualquer método HTTP."

Ou escrever um handler para quando nenhuma rota casar:

1
2
notFound $ do
    text "Não encontrei uma rota para sua requisição."

Além de parâmetros nomeados (como fizemos com /:word), você também pode especificar parâmetros não-nomeados, que são obtidos da query string (após a ?):

1
2
3
get "/hello" $ do
    name <- param "name"
    html $ mconcat ["<h1>Olá, ", name, "!</h1>"]

Implemente a rota acima e tente acessar /hello?name=”Alexandre”.

Cabeçalhos HTTP

Podemos recuperar o conteúdo de um cabeçalho:

1
2
3
get "/agent" $ do
    agent <- reqHeader "User-Agent"
    text agent

Ou setar um cabeçalho:

1
2
3
4
5
import Network.HTTP.Types -- Adicione "http-types" como dependência no ".cabal"

get "/mavins" $ do
    status status302 -- Redireciona permanentemente para "Location"
    header "Location" "https://mavins.com.br/blog"

Content Types

Podemos retornar respostas em HTML, plain text ou JSON:

1
2
3
4
html "Hello, world!"
text "Hello, world!"
json ("Hello, world!" :: String)
json ([0..10] :: [Int])

Observe que precisamos ser explícitos com os tipos quando usamos JSON.

Criando o “Model” da Aplicação

No Ivy Lee, selecionamos e priorizamos as tarefas que devemos executar em cada dia. Assim, o nosso modelo de dados pode ser representado da seguinte forma:

  • Cada tarefa é uma String que descreve o que deve ser feito.
  • As tarefas de um determinado dia são armazenadas em uma lista, cuja ordem dos elementos representa a prioridade de cada tarefa.
  • Utiliza-se um Map para relacionar dia (chave) e tarefas (valor).

Traduzindo para Haskell:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import qualified Data.Map as M -- Adicione "containers" como dependência no ".cabal"

type Day = String

type Task = String

allTasks :: M.Map Day [Task]
allTasks = M.fromList
    [ ("2018-01-01",
        [ "Lavar a louça" -- Tarefa de maior prioridade
        , "Pagar o contador"
        , "Fazer compras"
        , "Estudar Haskell"
        , "Ligar para o cliente"
        , "Ler um livro" -- Tarefa de menor prioridade
        ]
      )
    , ("2018-01-02",
        [ "Escrever um tutorial"
        , "Ligar para o Vinícius"
        , "Comprar um buquê"
        , "Tirar férias"
        , "Ir ao cinema"
        , "Visitar parentes"
        ]
      )
    , ("2018-01-03",
        [ "Ir a Igreja"
        , "Abastecer o carro"
        , "Varrer a casa"
        , "Tomar banho"
        , "Salvar o planeta"
        , "Estudar Haskell"
        ]
      )
    ]

API REST para Consulta (GET)

Dado esse modelo de dados, com apenas duas linhas de código, podemos criar um endpoint REST que retorna um JSON com as tarefas em allTasks:

1
2
get "/tasks" $ do
    json allTasks

E com mais três, outro endpoint que retorna as tarefas de um dia específico:

1
2
3
4
-- Ex: GET /tasks/2018-01-01
get "/tasks/:day" $ do
    day <- param "day"
    json $ M.lookup day allTasks

Antes de continuar, tente acessar essas rotas usando o navegador ou seu cliente HTTP favorito (curl, Postman, etc):

1
2
3
4
$ curl http://localhost:3000/tasks/2018-01-03
["Ir a Igreja","Abastecer o carro","Varrer a casa","Tomar banho","Salvar o planeta","Estudar Haskell"]
$ curl http://localhost:3000/tasks/2018-12-10
null

API REST para Atualização (POST, PUT, DELETE)

Diferentemente das operações de consulta, as operações de atualização modificam o model. No contexto do Mavins Lee, essas operações servirão para criar, editar ou remover tarefas.

Assim, seguindo os princípios do REST, implementaremos os seguintes endpoints:

1
2
3
4
5
6
7
8
9
10
11
-- Cadastra novas tarefas no dia especificado.
post "/tasks/:day" $ do
    undefined

-- Atualiza a lista de tarefas do dia especificado.
put "/tasks/:day" $ do
    undefined

-- Exclui todas as tarefas do dia especificado.
delete "/tasks/:day" $ do
    undefined

Agora é só fazer o parsing das requisições, validar os dados, modificar allTasks

Tela Azul do Windows

Houston, we’ve got a situation here…

Simulando um Banco de Dados

Você deve ter notado que usamos uma “variável” — allTasks — para armazenar os dados da aplicação. Fizemos isso porque, abstraindo os detalhes de persistência com uma “variável global”, podemos simplificar a implementação e focar no que interessa neste tutorial: no Scotty e no design de uma API REST.

Contudo, talvez você já tenha ouvido falar que as “variáveis” em Haskell são imutáveis… Isso significa que uma “variável”, uma vez definida, não pode ser modificada — pelo menos não da forma como fazemos em Python, PHP ou Java.

Em outras palavras, allTasks não é bem uma variável, mas sim uma constante!

Mas calma! Ainda podemos simular uma variável global mutável para ser o equivalente ao banco de dados da aplicação. Para isso, só precisamos encapsular nossas tarefas em uma MVar que, dentre outras, possui as seguintes operações:

1
2
3
4
newMVar :: a -> IO (MVar a)                      -- Cria uma nova variável.
readMVar :: MVar a -> IO a                       -- Lê o conteúdo da variável.
modifyMVar_ :: MVar a -> (a -> IO a) -> IO ()    -- Modifica a variável.
modifyMVar :: MVar a -> (a -> IO (a, b)) -> IO b -- Modifica e retorna um valor.

Já usou ponteiros? Pronto… Imagine, didaticamente, que uma MVar é um ponteiro:

1
2
3
4
mVar = malloc(...) // Em Haskell:
*mVar = x          // mVar <- newMVar x
y = *mVar;         // y <- readMVar mVar
*mVar = z;         // modifyMVar_ mVar (\_ -> return z)

Não se preocupe se estiver confuso… Vamos usar essas funções na prática e, logo, tudo ficará mais claro!

Voltando à Implementação da API

Antes de continuarmos, vamos atualizar o código da API de consultas para refletir as mudanças que acabamos de discutir sobre a MVar:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
main :: IO ()
main = do
    -- Cria e inicializa uma MVar "tasks'" com "allTasks"
    tasks' <- newMVar allTasks

    scotty 3000 $ do
        get "/tasks" $ do
            -- Lê o conteúdo em "tasks'" para "tasks"
            tasks <- liftIO $ readMVar tasks'
            json tasks

        get "/tasks/:day" $ do
            tasks <- liftIO $ readMVar tasks'
            day <- param "day"
            -- Consulta "tasks" usando "day" como chave
            json $ M.lookup day tasks

Agora, para montar a API de atualização, basta juntar as peças do quebra-cabeças… Por exemplo, vamos fazer juntos o post /tasks/:day:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
validateTasks :: [Task] -> Bool
validateTasks tasks =
    length tasks == 6 -- O Ivy Lee recomenda 6 tarefas por dia.

validateDay :: Day -> Bool
validateDay = ...

post "tasks/:day" $ do
    day <- param "day"
    -- Faz o "parsing" da string JSON no corpo do POST para uma lista.
    newTasks <- jsonData
    if not (validateDay day && validateTasks newTasks)
        then status status400 -- Bad request
        else do
            created <- liftIO $ modifyMVar tasks' $ \tasks ->
                if M.member day tasks -- Este dia já está cadastrado?
                    then return (tasks, False)
                    else return (M.insert day newTasks tasks, True)
            if created
                then status status200 -- OK
                else status status403 -- Forbidden

Essa função traz algumas novidades. A primeira é a função jsonData, que faz o parsing da string JSON no corpo da requisição POST para um tipo de dados Haskell (no nosso caso, uma lista). A segunda são as funções validateDay e validateTasks, que são simples predicados para validar os dados recebidos. Por fim, temos a aplicação da função modifyMVar. Essa já requer uma explicação um pouco mais elaborada…

O primeiro argumento da função modifyMVar é uma MVar que, como vimos anteriormente, você pode entender como sendo “um ponteiro para variável global”. O segundo argumento dessa função é uma expressão lambda (outra função) que mapeia o conteúdo armazenado na “variável global” — no nosso caso, um Map Day [Task] — em uma tupla cujo primeiro elemento é o novo valor a ser atribuído à “variável global” (também do tipo Map Day [Task]) e, o segundo, é o valor de retorno da função (Bool). Usamos um booleano para indicar se uma nova lista de tarefas foi ou não criada e retornamos um código HTTP diferente para cada caso.

Agora que você já viu o post /tasks/:day, como você implementaria o put /tasks/:day? E o delete /tasks/:day? (Dica: veja as funções M.update e M.delete).

Eu vou deixar esses dois como exercício pra você, mas caso fique alguma dúvida, é só deixar aqui embaixo nos comentários! Não se esqueça que eu disponibilizei o código-fonte deste tutorial no GitHub (lá tem a resposta).

Conclusão

Neste tutorial nós aprendemos a usar o Scotty para construir APIs REST em Haskell. Aprendemos como receber dados no formato JSON, tratar requisições HTTP de diferentes métodos e a retornar uma resposta para o cliente.

Ainda temos uma longa estrada até implementarmos tudo que uma aplicação real requer, por exemplo, precisamos armazenar os dados em um banco de dados de verdade, gerar documentação automaticamente, executar testes automatizados, etc.

Eu ficarei feliz em trazer todos esses conteúdos, mas antes eu preciso que você deixe um comentário aqui embaixo dizendo o quanto você adora programação funcional e como você acha divertidos esses tutoriais sobre Haskell… (Porque é claro que você gosta… Né?! :no_mouth:)

Um forte abraço, e até o próximo post!

Comentários desabilitados...