Alura > Cursos de Programação > Cursos de Clojure > Conteúdos de Clojure > Primeiras aulas do curso Clojure: Schemas

Clojure: Schemas

Schemas - Introdução

Bem vindo a mais um curso de Clojure aqui na Alura! Nessa etapa do treinamento conversaremos sobre schema, que não deve ser confundido com Scheme, outra linguagem de programação funcional. No dia-a-dia, sabemos que os dados em Clojure, quando representados por meio de mapas, precisam estar "o mais livres possível", de forma que não temos como inferir o conteúdo desses mapas.

Isso traz algumas dificuldades para nós, e criaremos códigos com certas validações manuais que são propensas a erro. Passaremos então pelo uso de bibliotecas, no caso schema, que garantem certas coisas, seja de uma forma imperativa, validandos esses dados, ou de forma declarativa, criando os schemas e deixando que eles sejam validados automaticamente, por exemplo quando invocamos funções ou rodamos nossos testes.

Aprenderemos a utilizar essas bibliotecas para criar schemas cada vez mais complexos e, ao final, ganhar o poder de validar os dados quando necessário e da forma necessária. Claro, como, quando e quanto é necessário validar os dados (por exemplo, validar a presença de campos não esperados) são perguntas que não possuem certo ou errado, mas sim vantagens ou desvantagens de acordo com a abordagem escolhida. Aqui, discutiremos essas abordagens à medida em que as implementamos.

Schemas - Problemas de não ter schemas

Começaremos nossos estudos utilizando o IntelliJ para criarmos um novo projeto chamado "hospital" no diretório de sua preferência. Criaremos então um arquivo aula1.clj com o namespace hospital.aula1 e invocando o (:use clojure.pprint).

(ns hospital.aula1
  (:use clojure.pprint))

Definiremos uma função adiciona-paciente, com a qual já trabalhamos algumas vezes, recebendo uma lista de pacientes e um paciente a ser adicionado. No último curso, quando falamos sobre protocolos e records, trabalhamos com uma variação que verificava se o paciente já estava dentro desse conjunto.

Sendo assim, faremos if-let para verificarmos a existência do id do paciente. Em caso positivo, associaremos o novo paciente ao conjunto pacientes com o assoc. Caso contrário, usaremos o throw para invocar um ex-info com a mensagem "Paciente não possui id", passando as informações desse paciente.

(defn adiciona-paciente [pacientes paciente]
  (if-let [id (:id paciente)]
    (assoc pacientes id paciente)
    (throw (ex-info "Paciente não possui id" {:paciente paciente}))))

Para testarmos esse código, criaremos uma nova função testa-uso-de-pacientes utilizando o let para criar alguns pacientes no nosso conjunto. Serão eles: guilherme, com o id 15 e o nome "Guilherme"; daniela, com o id 20 e o nome "Daniela"; e paulo com o id25 e o nome "Paulo".

Por fim, faremos um pprint desses pacientes em um vetor (afinal essa função só recebe um argumento) e invocaremos testa-uso-de-pacientes.

(ns hospital.aula1
  (:use clojure.pprint))

(defn adiciona-paciente [pacientes paciente]
  (if-let [id (:id paciente)]
    (assoc pacientes id paciente)
    (throw (ex-info "Paciente não possui id" {:paciente paciente}))))

(defn testa-uso-de-pacientes []
  (let [guilherme {:id 15, :nome "Guilherme"}
        daniela {:id 20, :nome "Daniela"}
        paulo {:id 25, :nome "Paulo"}]
    (pprint [guilherme paulo daniela])))

(testa-uso-de-pacientes)

Executando o código, teremos:

[{:id 15, :nome "Guilherme"}

{:id 25, :nome "Paulo"}

{:id 20, :nome "Daniela"}]

Agora queremos chamar a função adiciona-paciente, mas para os três pacientes de uma só vez. Para isso, podemos utilizar reduce adiciona-paciente, de modo que essa função será chamada várias vezes. Como parâmetro, passaremos primeiro um mapa vazio ({}) e em seguida um vetor contendo guilherme, daniela e paulo.

Assim, chamaremos reduce adiciona-paciente primeiro com os argumentos {} e guilherme. Em seguida, uma nova chamada será feita com o mapa resultante e daniela; e por último com esse novo mapa e paulo.

(defn testa-uso-de-pacientes []
  (let [guilherme {:id 15, :nome "Guilherme"}
        daniela {:id 20, :nome "Daniela"}
        paulo {:id 25, :nome "Paulo"}
        pacientes (reduce adiciona-paciente {} [guilherme, daniela, paulo])]
    (pprint pacientes)))

(testa-uso-de-pacientes)

Como resultado, teremos:

{15 {:id 15, :nome "Guilherme"},

20 {:id 20, :nome "Daniela"},

25 {:id 25, :nome "Paulo"}}

Antes tínhamos um vetor com os três pacientes, e agora temos um mapa com as informações que passamos. Prosseguindo, além de termos pacientes, queremos visitar o hospital. Para isso, criaremos uma função adiciona-visita que recebe as visitas e a visita que está sendo adicionada. Mas qual lógica vamos utilizar para essa implementação?

Uma prática bem comum quando estamos desenvolvendo código, que costuma aparecer em testes de development, por exemplo, é primeiro escrever o código que gostaríamos que funcionasse. Sendo assim, a ideia é começarmos com um mapa vazio (visitas {}) ao qual adicionaremos visitas novas chamando, por exemplo, adiciona-visita visitas 15 ["01/01/2019"] - código no qual estamos adicionando uma visita para o paciente guilherme de ID 15 no dia 01/01/2019.

Em seguida, chamaríamos a mesma função para o id 20 nos dias 01/02/2019 e 01/01/2020, e novamente para o ID 15 no dia 01/03/2019. Por fim, faremos o (pprint) dos pacientes antes dessas execuções para analisarmos o que está acontecendo no código.

(defn adiciona-visita
  [visitas, paciente])

(defn testa-uso-de-pacientes []
  (let [guilherme {:id 15, :nome "Guilherme"}
        daniela {:id 20, :nome "Daniela"}
        paulo {:id 25, :nome "Paulo"}
        pacientes (reduce adiciona-paciente {} [guilherme, daniela, paulo])
        visitas {}]
    (pprint pacientes)
    (adiciona-visita visitas 15 ["01/01/2019"])
    (adiciona-visita visitas 20 ["01/02/2019", "01/01/2020"])
    (adiciona-visita visitas 15 ["01/03/2019"])
    ))

(testa-uso-de-pacientes)

Ainda precisamos fazer a função adiciona-visita funcionar. Além de visitas e paciente, também precisaremos receber como parâmetro as novas-visitas desse paciente. Sabemos que visitas é um mapa no estilo {15 [], 20 [], 25 []}, ou seja, o ID do paciente e um vetor com suas visitas (ou, se não existe uma visita, um conjunto vazio).

; { 15 [], 20 [], 25 []}
(defn adiciona-visita
  [visitas, paciente, novas-visitas])

A ideia é que, se o paciente já existir em visitas, precisaremos concatenar o vetor que já existe com o novo. Já se ele não existir, é a primeira vez que esse paciente está sendo visitado. Sendo assim, faremos um assoc na chave paciente de visitas com o vetor novas-visitas que foi recebido por parâmetro.

; { 15 [], 20 [], 25 []}
(defn adiciona-visita
  [visitas, paciente, novas-visitas]
  (if (contains? visitas paciente)
    (concatenar)
    (assoc visitas paciente novas-visitas)))

Para trabalhar na situação em que o paciente já existe, precisaremos aprender a concatenar vetores. No Clojure existe uma função concat que recebe como parâmetros os vetores que deverão ser concatenados, como no exemplo:

(concat [1,5] [2,3])

=> (1 5 2 3)

Sendo assim, queremos aplicar o concat nos nossos elementos. Anteriormente, vimos que quando queremos alterar o valor de um mapa, utilizamos a função update. Nesse caso, queremos fazer um update na chave paciente de visitas (sabendo que existe um valor lá dentro, afinal chamamos o contains. Em seguida, queremos chamar a função concat passando como argumentos tanto o valor que está dentro do nosso mapa quanto as novas-visitas.

Faremos então um pprint de todas as chamadas de adiciona-visita para entendermos o que está acontecendo na execução.

(defn adiciona-visita
  [visitas, paciente, novas-visitas]
  (if (contains? visitas paciente)
    (update visitas paciente concat novas-visitas)
    (assoc visitas paciente novas-visitas)))

(defn testa-uso-de-pacientes []
  (let [guilherme {:id 15, :nome "Guilherme"}
        daniela {:id 20, :nome "Daniela"}
        paulo {:id 25, :nome "Paulo"}
        pacientes (reduce adiciona-paciente {} [guilherme, daniela, paulo])
        visitas {}]
    (pprint pacientes)
    (pprint (adiciona-visita visitas 15 ["01/01/2019"]))
    (pprint (adiciona-visita visitas 20 ["01/02/2019", "01/01/2020"]))
    (pprint (adiciona-visita visitas 15 ["01/03/2019"]))
    ))

Como retorno, teremos:

{15 {:id 15, :nome "Guilherme"},
 20 {:id 20, :nome "Daniela"},
 25 {:id 25, :nome "Paulo"}}
{15 ["01/01/2019"]}
{20 ["01/02/2019" "01/01/2020"]}
{15 ["01/03/2019"]}

Após a execução, visitas, que sempre se inicia como um conjunto vaziou, tornou-se 15 ["01/01/2019"], 20 ["01/02/2019" "01/01/2020"] e 15 ["01/03/2019"]. De maneira improvisada, poderíamos atualizar esse conjunto continuadamente, de forma semelhante a um shadowing:

(defn testa-uso-de-pacientes []
  (let [guilherme {:id 15, :nome "Guilherme"}
        daniela {:id 20, :nome "Daniela"}
        paulo {:id 25, :nome "Paulo"}
        pacientes (reduce adiciona-paciente {} [guilherme, daniela, paulo])
        visitas {}
        visitas (adiciona-visita visitas 15 ["01/01/2019"])
        visitas (adiciona-visita visitas 20 ["01/02/2019", "01/01/2020"])
        visitas (adiciona-visita visitas 15 ["01/03/2019"])]
    (pprint pacientes)
    (pprint visitas)
    ))

Assim, teremos como retorno:

{15 {:id 15, :nome "Guilherme"},
 20 {:id 20, :nome "Daniela"},
 25 {:id 25, :nome "Paulo"}}
{15 ("01/01/2019" "01/03/2019"), 20 ["01/02/2019" "01/01/2020"]}

Ou seja, conseguimos atribuir corretamente as datas das visitas para os respectivos pacientes (15 e 20). Outra forma de fazermos isso seria passando um reduce com o primeiro valor e continuando a execução com os pares de valores seguintes. Porém, o reduce funcionou de modo simples anteriormente pois a função adiciona-paciente recebia apenas um parâmetro, mas é mais complexo trabalhar com um número maior de parâmetros. Como esse não é o foco no momento, manteremos dessa forma.

(defn testa-uso-de-pacientes []
  (let [guilherme {:id 15, :nome "Guilherme"}
        daniela {:id 20, :nome "Daniela"}
        paulo {:id 25, :nome "Paulo"}

        ; uma variação com reduce, mais natural
        pacientes (reduce adiciona-paciente {} [guilherme, daniela, paulo])

        ; uma variação com shadowing, fica feio
        visitas {}
        visitas (adiciona-visita visitas 15 ["01/01/2019"])
        visitas (adiciona-visita visitas 20 ["01/02/2019", "01/01/2020"])
        visitas (adiciona-visita visitas 15 ["01/03/2019"])]
    (pprint pacientes)
    (pprint visitas)
    ))

Seria comum extrairmos o conjunto [guilherme, daniela, paulo] para um vetor, por exemplo pacientes. Entretanto, no nosso caso, isso resultaria em dois pacientes, um vetor e um shadowing (um mapa). Claro, também poderíamos redefinir a função adiciona-paciente com outras aridades ou variações, mas não é o nosso caso.

Para continuarmos, criaremos uma função imprime-relatorio-de-paciente que recebe como parâmetros visitas e paciente. Nela, usaremos o println para imprimir uma mensagem construída com o conteúdo desses símbolos.

(defn imprime-relatorio-de-pacientes [visitas, paciente]
  (println "Visitas do paciente" paciente "são" (get visitas paciente)))

Com a nossa função definida, vamos chamá-la ao término da execução de testa-uso-de-pacientes utilizando como um dos parâmetros o guilherme.

(defn testa-uso-de-pacientes []
  (let [guilherme {:id 15, :nome "Guilherme"}
        daniela {:id 20, :nome "Daniela"}
        paulo {:id 25, :nome "Paulo"}

        ; uma variação com reduce, mais natural
        pacientes (reduce adiciona-paciente {} [guilherme, daniela, paulo])

        ; uma variação com shadowing, fica feio
        visitas {}
        visitas (adiciona-visita visitas 15 ["01/01/2019"])
        visitas (adiciona-visita visitas 20 ["01/02/2019", "01/01/2020"])
        visitas (adiciona-visita visitas 15 ["01/03/2019"])]
    (pprint pacientes)
    (pprint visitas)
    (imprime-relatorio-de-pacientes visitas guilherme)))

Como retorno, teremos:

Visitas do paciente {:id 15, :nome Guilherme} são nil

Algo está errado, certo? Afinal, o guilherme deveria possuir duas visitas, mas o retorno foi nil. Vamos repetir a execução, dessa vez para a daniela. Da mesma forma, nosso retorno será nil:

Visitas do paciente {:id 20, :nome Daniela} são nil

Para analisarmos o que está acontecendo, adicionaremos um println da chamada de get passando como parâmetros o conjunto visitas e o ID 20, referente à Daniela.

(defn testa-uso-de-pacientes []
  (let [guilherme {:id 15, :nome "Guilherme"}
        daniela {:id 20, :nome "Daniela"}
        paulo {:id 25, :nome "Paulo"}

        ; uma variação com reduce, mais natural
        pacientes (reduce adiciona-paciente {} [guilherme, daniela, paulo])

        ; uma variação com shadowing, fica feio
        visitas {}
        visitas (adiciona-visita visitas 15 ["01/01/2019"])
        visitas (adiciona-visita visitas 20 ["01/02/2019", "01/01/2020"])
        visitas (adiciona-visita visitas 15 ["01/03/2019"])]
    (pprint pacientes)
    (pprint visitas)
    (imprime-relatorio-de-pacientes visitas daniela)
    (println (get visitas 20))))

Com isso, continuaremos recebendo um nil ao tentarmos acessar as visitas da Daniela com a função imrpime-relatorio-de-pacientes, mas a função get imprimirá corretamente o conteúdo.

Visitas do paciente {:id 20, :nome Daniela} são nil

[01/02/2019 01/01/2020]

Isso está acontecendo pois estamos trabalhando com o símbolo paciente em diversos pontos do código, mas com diversos significados distintos. Em adiciona-paciente, esse símbolo é um paciente inteiro com :id e :nome. Já em adiciona-visita, o paciente é um número. Mas e em imprime-relatorio-de-paciente? Não sabemos, pois passamos como parâmetro um nome (daniela, por exemplo), que é o paciente inteiro. Como estamos utilizando visitas, só precisávamos do id, e não desse paciente inteiro. Ao utilizá-lo como chave, não conseguimos encontrá-lo.

O correto seria, na chamada de imprime-relatorio-de-paciente, termos utilizado um ID - por exemplo, 20.

(defn testa-uso-de-pacientes []
  (let [guilherme {:id 15, :nome "Guilherme"}
        daniela {:id 20, :nome "Daniela"}
        paulo {:id 25, :nome "Paulo"}

        ; uma variação com reduce, mais natural
        pacientes (reduce adiciona-paciente {} [guilherme, daniela, paulo])

        ; uma variação com shadowing, fica feio
        visitas {}
        visitas (adiciona-visita visitas 15 ["01/01/2019"])
        visitas (adiciona-visita visitas 20 ["01/02/2019", "01/01/2020"])
        visitas (adiciona-visita visitas 15 ["01/03/2019"])]
    (pprint pacientes)
    (pprint visitas)
    (imprime-relatorio-de-pacientes visitas 20)
    (println (get visitas 20))))

Visitas do paciente 20 são [01/02/2019 01/01/2020]

Repare, então, que tivemos um problema ao nos perdermos em relação à utilização do símbolo paciente nos diversos pontos do código. Esse é um problema clássico, e gostaríamos, em várias linguagens incluindo o Clojure, de garantir que quem invoca a função imprime-relatorio-de-paciente passe o parâmetro paciente como um único número inteiro, um long, um UUID ou outro tipo que definirmos como ID.

A ideia, então, é termos algumas garantias de schema no nosso programa em Clojure. Vimos que o Record até poderia tentar aglomerar valores, ou mesmo poderíamos utilizar typehints do Java, mas isso fugiria da linguagem em questão. Será que o próprio Clojure possui funções e declarações que podem ser utilizadas para nos ajudar a trabalhar com esses tipos de validações e garantias em tempo de compilação do código? Ou mesmo, na execução, ao invés de retornar um nil obtermos uma mensagem avisando que, por exemplo, um paciente inteiro foi passado ao invés de um número, indicando que um erro foi feito no código?

A seguir conheceremos uma biblioteca bastante utilizada em Clojure justamente para obter esse tipo de garantia.

Schemas - Schema na invocação de função

Mostramos anteriormente que é comum termos diversos tipos de funções no dia-a-dia que recebem parâmetros com certas estruturas, como um agrupamento de valores, e que gostaríamos que tivessem certas características. Às vezes esses parâmetros possuem até mesmo um nome/símbolo que foi reutilizado em situações distintas, mas, em determinados contextos, esperamos um valor diferente.

Para resolver esse tipo de problema em Clojure, temos à disposição as bibliotecas de schema. No nosso caso, utilizaremos a biblioteca de schema da Plumatic, a mais famosa hoje em dia, e que precisa ser adicionada ao nosso projeto.

Para utilizarmos a biblioteca, usaremos a seguinte definição, com o nome da biblioteca e sua versão:

[prismatic/schema "1.1.12"]

Se quiser, você pode utilizar uma versão mais recente sem problemas. De volta ao IntelliJ, abriremos o arquivo project.clj, no qual encontraremos as definições das dependências:

(defproject hospital "0.1.0-SNAPSHOT"
  :description "FIXME: write description"
  :url "http://example.com/FIXME"
  :license {:name "EPL-2.0 OR GPL-2.0-or-later WITH Classpath-exception-2.0"
            :url "https://www.eclipse.org/legal/epl-2.0/"}
  :dependencies [[org.clojure/clojure "1.10.0"]]
  :repl-options {:init-ns hospital.core})

No momento, nossa única dependência é [org.clojure/clojure "1.10.0"]. Adicionaremos então a definição citada anteriormente e salvaremos as alterações.

(defproject hospital "0.1.0-SNAPSHOT"
  :description "FIXME: write description"
  :url "http://example.com/FIXME"
  :license {:name "EPL-2.0 OR GPL-2.0-or-later WITH Classpath-exception-2.0"
            :url "https://www.eclipse.org/legal/epl-2.0/"}
  :dependencies [[org.clojure/clojure "1.10.0"]
                 [prismatic/schema "1.1.12"]
                 ]
  :repl-options {:init-ns hospital.core})

Feito isso, podemos fechar o arquivo. Para utilizarmos essa biblioteca, assim como qualquer código que escrevemos, precisaremos importar o seu namespace. Na documentação da Plumatic/Schema encontramos o require comum dela:

(ns schema-examples
  (:require [schema.core :as s
             :include-macros true ;; cljs only
             ]))

No arquivo aula1.clj, dentro da definição do nosso namespace, tentaremos fazer essa importação com (:require [schema.core :as s]).

(ns hospital.aula1
  (:use clojure.pprint)
   (:require [schema.core :as s]))

Se rodarmos o nosso código dessa fora, receberemos um FileNotFoundException, referindo-se justamente ao schema/core.clj. Isso ocorre porque, apesar de termos adicionado a dependência no project.clj, não baixamos essa dependência. Ou seja, sempre que alteramos as dependências no Clojure, precisamos atualizá-las, e existem duas maneiras de fazermos isso.

A primeira delas ocorre automaticamente, quando o IntelliJ percebe que o arquivo project.clj foi alterado e nos sugere atualizar as dependências para a nova versão com um aviso na parte superior direita da tela. Como isso não ocorreu no nosso caso, temos duas opções.

A primeira delas é abrirmos o arquivo project.clj e pressionarmos "Shift" duas vezes para acessarmos a busca. Nela, procuraremos por "leinengen" e clicaremos na opção "Refresh Leinengen Projects" para atualizarmos nossas dependências. A segunda opção é acessarmos a aba do Leinengen no canto superior direito e a utilizarmos para recarregar o arquivo.

Feito isso, bastará reiniciarmos o REPL e carregarmos o arquivo aula1.clj novamente (o que pode ser feito com "Alt + Shift + L"). Agora que fizemos o require corretamente, vamos começar a explorar a nova biblioteca.

Primeiro, o schema nos proporciona algumas características de validação. A função s/validate, por exemplo, nos permite validar algum valor, como o número 15. Para isso, precisamos informar qual o schema a ser seguido, e existem vários schemas pré-definidos, alguns provindos do próprio java (como java.Long) ou do Schema.

Para testarmos, vamos imprimir o resultado de s/validate Long 15.

(pprint (s/validate Long 15))

Como retorno teremos o próprio número 15. Isso porque, quando a função s/validate valida corretamente o valor, ele devolve o próprio valor. Vamos fazer um novo teste, dessa vez para a string "guilherme".

(pprint (s/validate Long "guilherme"))

Dessa vez, como essa string não segue o schema de um long, nosso retorno será um erro:

Syntax error (ExceptionInfo) compiling at (aula1.clj:41:1).
Value does not match schema: (not (instance? java.lang.Long "guilherme"))

O mesmo ocorre se passarmos um vetor, por exemplo [15 13].

Syntax error (ExceptionInfo) compiling at (aula1.clj:42:1).
Value does not match schema: (not (instance? java.lang.Long [15 13]))

Assim, aprendemos que o schema permite a utilização de schemas pré-criados ou a criação de novos schemas para validarmos os dados que estão sendo passados. Chamar o s/validate é uma maneira explícita de fazermos essa validação, mas não é exatamente isso que queremos fazer.

A ideia é, de maneira declarativa, informarmos que a função imprime-relatorio-de-paciente recebe um paciente que segue o schema Long. Para isso, podemos utilizar uma variação do defn do próprio Schema, que é o macro s/defn. Testaremos essa variação criando uma função teste-simples que recebe um parâmetro [x] e o imprime no console com println. Em seguida, chamaremos essa função para os valores 15, 30 e guilherme.

(s/defn teste-simples [x]
  (println x))

(teste-simples 15)
(teste-simples 30)
(teste-simples "guilherme")

Como retorno, teremos exatamente os valores que passamos para a função. O macro s/defn nos permite definir algumas coisas a mais que em uma função normal. Por exemplo, nos nossos parâmetros, podemos informar que o x segue o schema Long utilizando :- Long:

(s/defn teste-simples [x :- Long]
  (println x))

(teste-simples 15)
(teste-simples 30)
(teste-simples "guilherme")

Porém, rodando o código dessa forma, todos os valores - inclusive "guilherme" - serão impressos no console corretamente. Isso ocorre pois, por padrão, o macro s/defn não faz uma validação, somente quando invocamos explicitamente o s/validate. Porém, existem várias maneiras de definirmos como e quando queremos essa validação - sempre, no namespace, nos testes, etc.

Pra testarmos, usaremos s/set-fn-validation! true para definirmos que a validação deverá ocorre sempre.

(s/set-fn-validation! true)

(s/defn teste-simples [x :- Long]
  (println x))

(teste-simples 15)
(teste-simples 30)
(teste-simples "guilherme")

Agora, se tentarmos chamar o teste-simples com a string "guilherme", receberemos um erro no console:

Syntax error (ExceptionInfo) compiling at (aula1.clj:52:1).
Input to teste-simples does not match schema: 

       [(named (not (instance? java.lang.Long "guilherme")) x)]  

Agora sim conseguimos, com a biblioteca de Schema, validar de forma declarativa que o nosso parâmetro x deve receber um schema Long, assim como em linguagens como o Java conseguimos definir que um parâmetro é do tipo Long. Claro, tipos e schemas possuem características diferentes, e veremos isso ao longo do curso.

A ideia agora é alterarmos a função imprime-relatorio-de-pacientes. Antes de prosseguirmos da maneira correta, definiremos a função novamente com o macro defn e tentaremos passar :- Long para os nossos parâmetros.

(defn imprime-relatorio-de-paciente
  [visitas, paciente :- Long]
  (println "Visitas do paciente" paciente "são" (get visitas paciente)))

Como retorno, teremos um erro:

Syntax error macroexpanding clojure.core/defn at (aula1.clj:54:1).
visitas - failed: vector? at: [:fn-tail :arity-n :bodies :params] spec: :clojure.core.specs.alpha/param-list
(:- Long) - failed: Extra input at: [:fn-tail :arity-1 :params] spec: :clojure.core.specs.alpha/param-list

Isso ocorre pois o compilador do Clojure fica confuso, interpretando :- Long como um novo parâmetro, o que não faz sentido. Alteraremos então o macro da definição para s/defn.

Antes, ao tentarmos chamar a função testa-uso-de-pacientes (que, ao final, chama (imprime-relatorio-de-paciente visitas daniela)), recebíamos nulo, afinal estávamos passando um paciente ao invés de um ID (representado por um número inteiro ou Long).

(ns hospital.aula1
  (:use clojure.pprint)
   (:require [schema.core :as s]))

(defn adiciona-paciente
  [pacientes paciente]
  (if-let [id (:id paciente)]
    (assoc pacientes id paciente)
    (throw (ex-info "Paciente não possui id" {:paciente paciente}))))

(defn imprime-relatorio-de-paciente [visitas, paciente]
  (println "Visitas do paciente" paciente "são" (get visitas paciente)))

; { 15 [], 20 [], 25 []}
(defn adiciona-visita
  [visitas, paciente, novas-visitas]
  (if (contains? visitas paciente)
    (update visitas paciente concat novas-visitas)
    (assoc visitas paciente novas-visitas)))

(defn testa-uso-de-pacientes []
  (let [guilherme {:id 15, :nome "Guilherme"}
        daniela {:id 20, :nome "Daniela"}
        paulo {:id 25, :nome "Paulo"}

        ; uma variação com reduce, mais natural
        pacientes (reduce adiciona-paciente {} [guilherme, daniela, paulo])

        ; uma variação com shadowing, fica feio
        visitas {}
        visitas (adiciona-visita visitas 15 ["01/01/2019"])
        visitas (adiciona-visita visitas 20 ["01/02/2019", "01/01/2020"])
        visitas (adiciona-visita visitas 15 ["01/03/2019"])]
    (pprint pacientes)
    (pprint visitas)
    (imprime-relatorio-de-paciente visitas daniela)
    (println (get visitas 20))))

(testa-uso-de-pacientes)

(pprint (s/validate Long 15))
; (pprint (s/validate Long "guilherme"))
; (pprint (s/validate Long [15, 13]))

(s/set-fn-validation! true)

(s/defn teste-simples [x :- Long]
  (println x))

(teste-simples 15)
(teste-simples 30)
; (teste-simples "guilherme")

(s/defn imprime-relatorio-de-paciente
  [visitas, paciente :- Long]
  (println "Visitas do paciente" paciente "são" (get visitas paciente)))

(testa-uso-de-pacientes)

Agora, ao invocarmos novamente a função, os pacientes e as visitas serão impressos corretamente, mas então receberemos um erro indicando que, ao invés de um Long, passamos um PersistentArrayMap.

Syntax error (ExceptionInfo) compiling at (aula1.clj:58:1).
Input to imprime-relatorio-de-paciente does not match schema: 

       [nil (named (not (instance? java.lang.Long a-clojure.lang.PersistentArrayMap)) paciente)]  

Agora conseguimos uma mensagem de erro clara, em tempo de execução, informando que o valor passado como parâmetro não condiz com o schema Long. Com o :- podemos, de maneira declarativa, informar quais schemas são esperados como parâmetro das nossas funções, o que ajudará bastante a escrevermos os nossos códigos. A seguir continuaremos explorando schemas.

Sobre o curso Clojure: Schemas

O curso Clojure: Schemas possui 123 minutos de vídeos, em um total de 31 atividades. Gostou? Conheça nossos outros cursos de Clojure em Programação, ou leia nossos artigos de Programação.

Matricule-se e comece a estudar com a gente hoje! Conheça outros tópicos abordados durante o curso:

Aprenda Clojure acessando integralmente esse e outros cursos, comece hoje!

Conheça os Planos para Empresas