Alura > Cursos de Programação > Cursos de Clojure > Conteúdos de Clojure > Primeiras aulas do curso Clojure: mutabilidade com átomos e refs

Clojure: mutabilidade com átomos e refs

Vetores, Listas, Conjuntos e Filas - Introdução

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á!

Vetores, Listas, Conjuntos e Filas - Vetor, lista, conjunto e filas

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.

Vetores, Listas, Conjuntos e Filas - Simulando as 4 filas do hospital, espera e atendimento

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.

Sobre o curso Clojure: mutabilidade com átomos e refs

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:

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

Conheça os Planos para Empresas