Boas vindas!
Este é o curso Clojure: Mutabilidade com átomos e refs com o instrutor Guilherme Silveira!
Até o momento, vimos nos cursos anteriores como trabalhar com valores imutáveis quando invocamos funções como MapReduce, suas variações, funções recursivas entre outras, gerando novos dados de forma otimizada para simular diversas situações em sistemas.
Podemos criar funções, laços e iterações em uma coleção através de invocações recursivas e outras várias atividades. Porém, existem situações nas quais precisamos de um valor mutável.
Falamos brevemente sobre isso no início do curso de Clojure através das definições chamadas def
, parecidas com variáveis globais de outras linguagens de programação. Em nosso caso, chamamos de root binding
, que nos permite atribuir um valor raiz a um símbolo que é compartilhado por todas as threads
, o que nos leva a problemas de concorrência.
Nessas aulas, veremos como trabalhar com memória compartilhada entre várias threads
em um espaço concorrente e como o faremos em Clojure. Para isso, utilizaremos conceitos e abordagens com a de átomo e a de referências, observando suas vantagens e desvantagens no contexto de multithreading.
Iniciaremos threads
ou features para rodarmos códigos em paralelo e avaliar possíveis situações estranhas para analisar as ações disponíveis.
Para quem já trabalhou com programação corrente ou outras linguagens, verá estes cenários ocorrendo neste curso. Para quem ainda não e tem interesse, pode acessar outras aulas desta área. De qualquer forma, lidaremos com mutabilidade e o espaço compartilhado entre diversas threads
no próximo passo.
Vamos lá!
O primeiro passo é criar um novo projeto no IntelliJ IDEA, escolhendo "Clojure" e "Leiningen" para nomear como "hospital" e salvar no diretório de sua preferência.
Finalize, fecha a caixa de diálogo "Tip of the Day" e clique na aba lateral "1: Project" para abrir o projeto. Na lista lateral, acesse "hospital > src > hospital > core.clj". O core.clj
possui uma função padrão que deve ser removida pois não será utilizada. Rode o REPL clicando com o botão direito sobre o project.clj
na lista lateral e selecionando "Run REPL for hospital" para carregar e testar o código.
Nosso projeto simula um hospital com um departamento de laboratório onde os pacientes são atendidos; neste, há três sessões distintas em andares diferentes com três filas de espera que realizam diversos tipos de exame laboratoriais e coletas. Quando uma pessoa chega nesse lugar, é recebida por um atendimento geral com uma sala de espera.
Começamos com essa primeira fila de espera; a medida que vai liberando espaço nos laboratórios 1, 2 ou 3, as pessoas são deslocadas de uma para a seguinte. Conforme os exames são finalizados, os pacientes são removidos desta última fila. Logo, para acessar este departamento, o hospital conta com quatro filas no total.
Nosso trabalho é gerenciar estes deslocamentos dinâmicos de forma lógica e organizada. Se queremos representar vários pacientes, conferimos identificadores individuais utilizando um vetor.
Na lista lateral, clique com o botão direito sobre a pasta "hospital" dentro de "src" para criar um novo arquivo indo em "New > File" e nomeá-lo como "colecoes.clj". Neste, podemos testar e explorar o que já conhecemos de coleções.
Teste o vetor que não recebe nada, defina como vetor inicial a espera e dentro deste já identifique o paciente 111
e 222
. Dentro do let
, imprima a espera e adicione outra pessoa 333
imprimindo com conj
. Por fim, invoque o testa-vetor
e rode no REPL.
(ns hospital.colecoes)
(defn testa-vetor []
(let [espera [111 222]]
(println espera)
(println (conj espera 333))
))
(testa-vetor)
Com isso, visualizamos os identificadores no REPL. Se quisermos adicionar mais alguém no fim do vetor, insira outra linha com conj
para imprimir o paciente 444
.
Rodando novamente, a terceira linha impressa substitui 333
por 444
, já que o vetor é imutável. O símbolo espera
continua referenciando 111
e 222
mas comete esse equívoco na sequência pois não subscrevemos o símbolo redefinindo espera
como resultado de conj
.
Já que o conj
adiciona, devemos olhar as funções que trabalham com coleções para retirar, seja no core
do Clojure ou na documentação. É possível retirar um elemento específico como 111
com disj
.
(defn testa-vetor []
(let [espera [111 222]]
(println espera)
(println (conj espera 333))
(println (conj espera 444))
(println (disj espera 111))
))
(testa-vetor)
Salve e rode novamente. O sistema nos alerta que não podemos fazer IPersistentSet
. No caso do vetor, algumas funções podem remover elementos dele. Da mesma maneira que temos o conj
, temos remove
, pop
e outras maneiras de trabalhar esta ação.
No caso do remove
, devemos passar um predicado de função para definir o que será removido, como um filtro. Já o pop
recebe uma coleção, sendo o mais indicado para nosso projeto.
(defn testa-vetor []
(let [espera [111 222]]
(println espera)
(println (conj espera 333))
(println (conj espera 444))
(println (pop espera))
))
(testa-vetor)
Rodando novamente, vemos que quem sai da espera é o paciente 222
, e não o 111
que é atendido antes e sai primeiro. Assim, vemos que o pop
no vetor não cumpre a lógica da fila, pois retira o último elemento.
Isso acontece porque o vector
, baseado em um array na memória, age mais rapidamente removendo o item do final. Pode ser útil para outros trabalhos, mas não para esta estrutura de dados específica. Portanto, não podemos proceder com um vetor neste caso, independente da grandeza de suas operações.
Há outras estruturas de dados diferentes de vetor, como a lista. Copie e cole o bloco na sequência e substitua vetor
por lista
, implementada por aspas e parênteses.
(defn testa-lista []
(let [espera '(111 222)]
(println espera)
(println (conj espera 333))
(println (conj espera 444))
(println (pop espera))
))
(testa-lista)
Rodando o código, vemos a lista com os parênteses; porém, o conj
adicionou os pacientes no começo e o pop
retirou do início também. Mas em uma fila, quando uma nova pessoa chega, entra ao final e depois sai do começo.
Esse problema diz respeito a uma questão de implementação, pois em uma lista com um único sentido é mais rápido adicionar no começo como o conj
fez. A sensação antes era a de que sempre conseguimos implementar o código voltado à interface, o que nem sempre acontece. Portanto, a estrutura de dados a ser utilizada deve ser do tipo fila.
Testamos a coleção de conjunto, a qual tem a parte matemática #
e chaves. Copie e cole o último bloco, substituindo lista
por conjunto
.
(defn testa-conjunto []
(let [espera #{111 222}]
(println espera)
(println (conj espera 333))
(println (conj espera 444))
(println (pop espera))
))
(testa-conjunto)
Rodamos para ver que o pop
não funcionou como esperado. Surge um alerta do sistema indicando que para usar o pop
, necessitando a implementação de uma pilha.
Como ainda não é o que queremos, buscamos implementar uma fila - traduzida para o inglês como queue - onde a ordem é importante, dispensando o uso de conjunto
também. Para isso, copiamos e colamos o último bloco na sequência e usamos o código para fila vazia clojure.lang.PersistQueue/EMPTY
.
(defn testa-fila []
(let [espera clojure.lang.PersistentQueue/EMPTY]
(println "fila")
(println espera)
(println (conj espera 111))
(println (conj espera 333))
(println (conj espera 444))
))
(testa-fila)
Rodamos para visualizar várias informações longas e complicadas na janela. O que podemos fazer é retirar momentaneamente as linhas com os identificadores de pacientes e transformar espera
em uma sequência para rodar novamente e ver que não há pacientes inclusos por enquanto.
(defn testa-fila []
(let [espera clojure.lang.PersistentQueue/EMPTY]
(println "fila")
(println (seq espera))
))
(testa-fila)
Dessa vez, o retorno é nulo ao rodar, pois não há pessoas na fila por enquanto. Agora podemos colocar os dois primeiros elementos na fila chamando conj
na sentença que possui o código da fila e seq
nas adições de pacientes.
Para retirar, chame pop
com sequência para impressão. Assim, o 111
deveria ser o primeiro elemento a ser retirado. Depois, é interessante conseguirmos visualizar o paciente retirado da fila com a operação peek
.
(defn testa-fila []
(let [espera (conj clojure.lang.PersistentQueue/EMPTY "111" "222")]
(println "fila")
(println (seq espera))
(println (seq (conj espera "333")))
(println (seq (pop espera)))
(println (peek espera))
))
(testa-fila)
Rode mais uma vez para ver os resultados. Então, quando quisermos trabalhar com filas de processamento em Clojure ou outras coleções que não possuem uma sintaxe nativa da própria linguagem mais simplificada, podemos trabalhar com a mais longa sem problemas, como a referência padrão utilizada.
Porém, ainda estamos tendo que criar uma seq
em cima da fila para podermos imprimir de forma clara. Para simplificar, já existe no Clojure a função pprint
que deve ser adicionada logo após a hospital.colecoes
.
(ns hospital.colecoes
(:use [clojure pprint]))
Assim, usamos (pprint espera)
ao final das linhas de impressão para visualizar de forma mais clara o resultado. Essa função só recebe um elemento como argumento, diferente de println
que pode receber vários.
(defn testa-fila []
(let [espera (conj clojure.lang.PersistentQueue/EMPTY "111" "222")]
(println "fila")
(println (seq espera))
(println (seq (conj espera "333")))
(println (seq (pop espera)))
(println (peek espera))
(pprint espera)
)))
(testa-fila)
Agora que fizemos diversos testes e sabemos como criar uma fila, poderemos voltar ao hospital e criar um modelo com as quatro filas existentes para trabalharmos no próximo passo.
Agora que já sabemos trabalhar com uma fila, podemos criar um hospital com quatro delas.
Clicando com o botão direito sobre "hospital" dentro de "src", selecione "New > File" para criar um arquivo de modelo chamado "model.clj". Declare (ns hospital.model)
no topo da nova área e crie uma nova função chamada novo-hospital
que devolve um mapa com a primeira fila de espera e as outras três dos diferentes laboratórios todas vazias com clojure.lang.PersistentQueue/EMPTY
em cada uma.
Para evitar as longas repetições nas sintaxes, defina um novo objeto chamado fila-vazia
que referencia a sentença citada.
(ns hospital.model)
(def fila-vazia clojure.lang.PersistentQueue/EMPTY)
(defn novo-hospital []
{ :espera fila-vazia
:laboratorio1 fila-vazia
:laboratorio2 fila-vazia
:laboratorio3 fila-vazia})
Com isso, retorne à aba hospital.core
para testar adicionando um require
de hospital.model
como h.model
. Defina nosso hospital hospital-do-gui
como sendo novo-hospital
e imprima-o com pprint
. Não esqueça de importar esse recurso com use
no topo do código, como já fizemos.
Em seguida, imprima a fila vazia chamando fila-vazia
.
(ns hospital.core
(:use [clojure pprint])
(:require [hospital.model :as h.model]))
(let [hospital-do-gui (h.model/novo-hospital)]
(pprint hospital-do-gui))
(pprint h.model/fila-vazia)
Com este código, rodamos o REPL para visualizar as filas impressas. Agora podemos começar a trabalhar com o hospital e resolver possíveis problemas.
O core
serve para testarmos o funcionamento do programa, e os próximos passos serão divididos em aulas. Crie um novo arquivo no mesmo diretório chamado "aula1.clj".
Neste novo documento, declare ns hospital.aula1
no topo, copie e cole as linhas require
e use
usadas anteriormente. Em seguida, crie um hospital com uma função defn
que simula um dia de funcionamento para trabalharmos chamada simula-um-dia
.
dentro desta, crie um valor def
e atribua ao símbolo global hospital
para que as threads
tenham acesso dentro deste namespace. Invoque o valor de h.model/novo-hospital
como um root binding
.
Queremos anunciar a chegada de um novo paciente 111
na fila de espera
através de uma função de lógica chega-em
. Por enquanto usamos string
para realizar outros testes de exploração.
(ns hospital.aula1
(:use [clojure pprint])
(:require [hospital.model :as h.model]))
(defn simula-um-dia []
; root binding
(def hospital (h.model/novo-hospital))
(chega-em hospital :espera "111")
)
(simula-um-dia)
A função de lógica chega-em
é pura e deve ser extraída para um arquivo chamado "logic.clj" a partir da mesma pasta "hospital" presente na lista lateral. Neste, importe o namespace hospital.logic
e em seguida defina a função chega-em
que recebe o hospital
, o departamento
e a pessoa
.
Tendo em mente que nosso hospital é um mapa que possui uma chave chamada departamento
, não podemos usar assoc
. Como queremos atualizar um valor do mapa, chamamos o update
que aplica uma função em um valor específico na chave espera
que no caso é o departamento.
Nesta chave, pegue o valor e chame a função conj
que adiciona algo em uma fila, passando como parâmetro a pessoa
.
(ns hospital.logic)
(defn chega-em
[hospital departamento pessoa]
(update hospital departamento conj pessoa)
)
De volta a hospital.aula1
, imprimimos o resultado do update
com pprint
antes de chega-em
. Além do require
do model
, temos também do logic :as h.logic
.
(ns hospital.aula1
(:use [clojure pprint])
(:require [hospital.model :as h.model]
[hospital.logic :as h.logic]))
(defn simula-um-dia []
; root binding
(def hospital (h.model/novo-hospital))
(pprint (h.logic/chega-em hospital :espera "111"))
)
(simula-um-dia)
Com este código, podemos rodar para analisar os resultados na janela REPL. Por enquanto, retire o pprint
deste código para podermos adicionar mais os pacientes 222
e 333
, escrevendo pprint
em seguida para imprimir o hospital
ao rodar.
O resultado ainda retorna um hospital vazio, pois o update
chega-em
trabalha em forma imutável, ou seja, sua atualização devolve um novo mapa. Portanto, reatribua def hospital
às quatro linhas def
.
Ainda que o uso constante do símbolo global torne o código repetitivo e longo, o usamos para explorar suas funcionalidades por enquanto. Como queremos que outros pacientes acessem também as filas dos laboratórios, adicione mais duas linhas para outros dois laboratórios.
Por fim, imprima o mapa e rode no REPL novamente.
(defn simula-um-dia []
; root binding
(def hospital (h.model/novo-hospital))
(def hospital (h.logic/chega-em hospital :espera "111"))
(def hospital (h.logic/chega-em hospital :espera "222"))
(def hospital (h.logic/chega-em hospital :espera "333"))
(pprint hospital)
(def hospital (h.logic/chega-em hospital :laboratorio1 "444"))
(def hospital (h.logic/chega-em hospital :laboratorio3 "555"))
(pprint hospital)
)
(simula-um-dia)
Mais adiante, criaremos mais uma lógica da mesma maneira que chegaram pessoas novas em hospital
e nos departamentos, e queremos atendê-las. Em seguida, adicione h.logic/atende
no hospital
para o laboratorio1
e outra linha para espera
.
Crie a função atende
no arquivo hospital.logic
que recebe o hospital
e o departamento
. Na linha seguinte, use get
para pegar o departamento
do hospital
e ser a fila. Desta fila, pegamos a primeira pessoa para ser atendida.
(ns hospital.logic)
(defn chega-em
[hospital departamento pessoa]
(update hospital departamento conj pessoa)
(defn atende
[hospital departamento]
(let [fila (get hospital departamento)]
))
Precisamos de um retorno atualizado deste código, logo temos que refazer a função e obter uma melhor alternativa; para isso, apague a definição da função atende
e refaça-a recebendo hospital
e departamento
novamente. Queremos atender e retirar a primeira pessoa e devolver o mapa atualizado, começando de forma simples e complexificando cada vez mais ao longo do processo.
Chamamos a função que traz o resto da fila chamada pop
no departamento. Aplique-a de maneira a receber todo o hospital chamando update
antes.
(defn atende
[hospital departamento]
(update hospital departamento pop))
Salve as alterações e rode hospital.aula1
com o seguinte bloco imprimindo os atendimentos:
(defn simula-um-dia []
; root binding
(def hospital (h.model/novo-hospital))
(def hospital (h.logic/chega-em hospital :espera "111"))
(def hospital (h.logic/chega-em hospital :espera "222"))
(def hospital (h.logic/chega-em hospital :espera "333"))
(pprint hospital)
(def hospital (h.logic/chega-em hospital :laboratorio1 "444"))
(def hospital (h.logic/chega-em hospital :laboratorio3 "555"))
(pprint hospital)
(def hospital (h.logic/atende hospital :laboratorio1))
(def hospital (h.logic/atende hospital :espera))
(pprint hospital)
)
(simula-um-dia)
O próximo passo é adicionar um limite às filas, implementando manualmente.
O curso Clojure: mutabilidade com átomos e refs possui 182 minutos de vídeos, em um total de 44 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:
Impulsione a sua carreira com os melhores cursos e faça parte da maior comunidade tech.
1 ano de Alura
Assine o PLUS e garanta:
Formações com mais de 1500 cursos atualizados e novos lançamentos semanais, em Programação, Inteligência Artificial, Front-end, UX & Design, Data Science, Mobile, DevOps e Inovação & Gestão.
A cada curso ou formação concluído, um novo certificado para turbinar seu currículo e LinkedIn.
No Discord, você tem acesso a eventos exclusivos, grupos de estudos e mentorias com especialistas de diferentes áreas.
Faça parte da maior comunidade Dev do país e crie conexões com mais de 120 mil pessoas no Discord.
Acesso ilimitado ao catálogo de Imersões da Alura para praticar conhecimentos em diferentes áreas.
Explore um universo de possibilidades na palma da sua mão. Baixe as aulas para assistir offline, onde e quando quiser.
Acelere o seu aprendizado com a IA da Alura e prepare-se para o mercado internacional.
1 ano de Alura
Todos os benefícios do PLUS e mais vantagens exclusivas:
Luri é nossa inteligência artificial que tira dúvidas, dá exemplos práticos, corrige exercícios e ajuda a mergulhar ainda mais durante as aulas. Você pode conversar com a Luri até 100 mensagens por semana.
Aprenda um novo idioma e expanda seus horizontes profissionais. Cursos de Inglês, Espanhol e Inglês para Devs, 100% focado em tecnologia.
Transforme a sua jornada com benefícios exclusivos e evolua ainda mais na sua carreira.
1 ano de Alura
Todos os benefícios do PRO e mais vantagens exclusivas:
Mensagens ilimitadas para estudar com a Luri, a IA da Alura, disponível 24hs para tirar suas dúvidas, dar exemplos práticos, corrigir exercícios e impulsionar seus estudos.
Envie imagens para a Luri e ela te ajuda a solucionar problemas, identificar erros, esclarecer gráficos, analisar design e muito mais.
Escolha os ebooks da Casa do Código, a editora da Alura, que apoiarão a sua jornada de aprendizado para sempre.