| Bellacosa Mainframe e uma viagem ao centro do clojure |
☕ Um Café no Bellacosa Mainframe
Viagem ao Centro do Clojure: COBOL, Rich Hickey e a Linguagem que Decidiu que Mudar Estado Era um Péssimo Hábito
🌋 Lisp, JVM, imutabilidade, REPL, funções, memória, performance, concorrência, Datomic, Nubank e a expedição de um COBOLzeiro ao interior de uma linguagem de 2007 que parecia ter sido escrita por Júlio Verne depois de descobrir a programação funcional
Há tecnologias que entram na sala fazendo barulho.
Chegam com conferências, camisetas, logos coloridos e evangelistas dizendo:
“Isto vai mudar tudo.”
E há tecnologias que entram silenciosamente, sentam no fundo da sala, olham para cinquenta anos de programação imperativa e perguntam:
“Vocês têm certeza de que ficar alterando estado o tempo inteiro foi uma boa ideia?”
Clojure pertence ao segundo grupo.
Para um programador COBOL iniciante, especialmente alguém acostumado a imaginar programas como uma sequência de:
MOVE
ADD
SUBTRACT
COMPUTE
IF
PERFORM
Clojure parece inicialmente uma criatura saída das profundezas de um romance de Júlio Verne.
Você olha para:
(+ 1 2)
e pergunta:
— Por que o operador está do lado errado?
Depois encontra:
(defn debitar [saldo valor]
(- saldo valor))
e pensa:
— Cadê minha WORKING-STORAGE?
Cinco minutos depois alguém escreve:
(->> transacoes
(filter suspeita?)
(map analisar)
(group-by :cliente))
e o COBOLzeiro começa a procurar a saída de emergência.
Mas talvez devêssemos fazer exatamente o contrário.
Em vez de fugir, vamos acender as lanternas.
Hoje descemos ao centro do Clojure.
Capítulo I — O mapa da expedição
Nossa viagem começa em 2007.
Naquele ano, Java já dominava boa parte do mundo corporativo.
.NET crescia.
C++ permanecia forte.
PHP estava por toda a Web.
Ruby ganhava atenção.
Python ainda não tinha conquistado o planeta inteiro.
E a JVM parecia um continente relativamente bem cartografado.
Então Rich Hickey aparece com uma pergunta perigosa:
E se pudéssemos usar a JVM sem carregar todo o modelo mental orientado a objetos tradicional?
A resposta foi Clojure.
O desenvolvimento havia começado alguns anos antes, por volta de 2005, e em 2007 a linguagem tornou-se pública.
Ela nasceu como um dialeto de Lisp projetado especificamente para rodar sobre a JVM.
Isso é muito importante.
Rich Hickey não tentou construir:
LINGUAGEM NOVA
+
VM NOVA
+
ECOSSISTEMA NOVO
+
BIBLIOTECAS NOVAS
+
BOA SORTE
Ele fez algo muito mais pragmático:
CLOJURE
|
v
JVM
|
v
JAVA ECOSYSTEM
Isso significava acesso a:
bibliotecas Java
drivers JDBC
threads JVM
garbage collector
networking
cryptography
AWS SDK
Kafka
HTTP
filesystem
Ou seja, Clojure podia ser estranha por cima sem precisar reinventar o planeta por baixo.
Capítulo II — Quem é Rich Hickey?
Rich Hickey é o criador de Clojure e uma das figuras mais influentes da programação funcional moderna.
Ele também está intimamente ligado ao Datomic.
Isso importa porque existe uma linha intelectual direta:
RICH HICKEY
|
+---- CLOJURE
| |
| +-- valores
| +-- imutabilidade
| +-- funções
| +-- estado controlado
|
+---- DATOMIC
|
+-- fatos imutáveis
+-- tempo
+-- histórico
+-- database values
Se Clojure é a filosofia aplicada ao programa, Datomic é parte dessa filosofia aplicada aos dados.
Por isso entender Clojure ajuda brutalmente a entender por que Datomic parece tão diferente de Db2, Oracle ou PostgreSQL.
Capítulo III — Qual “geração” de linguagem é Clojure?
Se usarmos aquela classificação antiga:
1GL → máquina
2GL → assembler
3GL → alto nível
4GL → declarativas/especializadas
Clojure cabe genericamente como linguagem de alto nível.
Mas isso diz pouco.
Uma descrição melhor seria:
LISP
+
FUNCIONAL
+
DINÂMICA
+
COMPILADA PARA JVM
+
PROPÓSITO GERAL
+
MULTIPARADIGMA
Ela não é puramente funcional no sentido extremo.
Você pode ter estado.
Você pode ter efeitos colaterais.
Você pode conversar com Java.
Mas a linguagem favorece fortemente valores imutáveis e transformação de dados através de funções.
Capítulo IV — A primeira grande heresia: valores não deveriam mudar
Em COBOL fazemos naturalmente:
MOVE 1000 TO WS-SALDO.
SUBTRACT 200 FROM WS-SALDO.
Antes:
WS-SALDO = 1000
Depois:
WS-SALDO = 800
Nosso modelo mental é:
MESMA COISA
|
v
MUDOU
Clojure prefere separar:
VALOR A = 1000
FUNÇÃO
|
v
VALOR B = 800
1000 continua sendo 1000.
800 é outro valor.
A diferença parece banal.
Não é.
Quando você possui:
threads
concorrência
cache
eventos
estado compartilhado
sistemas distribuídos
objetos mutáveis podem virar pequenas minas terrestres.
Capítulo V — Estruturas persistentes imutáveis
Agora encontramos uma das maiores forças da linguagem.
Clojure oferece estruturas como:
[1 2 3]
vector.
'(1 2 3)
lista.
#{1 2 3}
set.
E:
{:nome "Arthur"
:saldo 1000M}
map.
Agora:
(def cliente
{:nome "Arthur"
:saldo 1000M})
Depois:
(def cliente2
(assoc cliente :saldo 800M))
Temos:
cliente = saldo 1000
cliente2 = saldo 800
O primeiro não mudou.
Isso não significa copiar ingenuamente cada byte inteiro.
Clojure utiliza persistent data structures, compartilhando internamente partes estruturais.
É uma técnica inteligente para tornar imutabilidade prática.
Capítulo VI — O império dos parênteses
Chega a hora daquilo que todos comentam.
(+ 2 3)
Em vez de:
2 + 3
Isso é prefix notation.
Outro:
(* (+ 2 3) 4)
Resultado:
20
Parece alienígena.
Mas tem uma vantagem.
A sintaxe tende a seguir:
(função argumento argumento argumento)
Exemplo:
(str "Bellacosa" " Mainframe")
Tudo começa a seguir uma estrutura relativamente uniforme.
E aqui surge o primeiro Easter egg:
Lisp não possui parênteses demais.
Nós é que crescemos acostumados a escondê-los atrás de quinze tipos diferentes de sintaxe.
O Clojure apenas deixou todos à vista.
😄
Capítulo VII — def, defn, let e companhia
Comecemos simples.
Definir um valor:
(def saldo 1000M)
Definir função:
(defn somar [a b]
(+ a b))
Executar:
(somar 10 20)
Resultado:
30
Variáveis locais:
(let [saldo 1000M
compra 200M]
(- saldo compra))
Resultado:
800M
E sim, existe if.
(if (> saldo 0)
"POSITIVO"
"NEGATIVO")
O COBOLzeiro respira aliviado.
A humanidade ainda não conseguiu eliminar:
IF
Capítulo VIII — Funções de primeira classe
Em COBOL tradicional, subrotinas e PERFORM possuem papel central.
Em Clojure, funções são tratadas como valores.
Você pode passá-las para outras funções.
Exemplo:
(map inc [1 2 3])
Resultado:
(2 3 4)
inc é uma função passada para map.
Outro:
(filter even? [1 2 3 4 5 6])
Resultado:
(2 4 6)
Aos poucos você para de escrever:
FOR cada item
IF condição
faça algo
e começa a pensar:
pegue coleção
filtre
transforme
reduza
Capítulo IX — map, filter e reduce: a Santíssima Trindade funcional
Suponha transações:
(def transacoes
[100 200 50 900 300])
Quero apenas valores acima de 200:
(filter #(> % 200) transacoes)
Resultado:
(900 300)
Quero dobrar:
(map #(* % 2) transacoes)
Quero somar:
(reduce + transacoes)
Isso é programação orientada a transformação de dados.
Para um COBOLzeiro, pense em algo como:
INPUT FILE
|
v
FILTER
|
v
TRANSFORM
|
v
AGGREGATE
É quase um SORT/ICETOOL filosófico.
😄
Capítulo X — Threading macros: o encanamento elegante
Uma das características mais simpáticas é -> e ->>.
Sem:
(println
(clojure.string/upper-case
"bellacosa"))
Com:
(-> "bellacosa"
clojure.string/upper-case
println)
Leia:
"bellacosa"
|
v
uppercase
|
v
println
Para coleções:
(->> [1 2 3 4 5 6]
(filter even?)
(map #(* % 10)))
Resultado:
(20 40 60)
Isso deixa pipelines de dados extremamente legíveis.
Capítulo XI — REPL: programando dentro da máquina viva
REPL significa:
Read
Eval
Print
Loop
Você abre o ambiente e escreve:
(+ 2 2)
Ele responde:
4
Depois:
(def cliente
{:nome "Arthur"
:saldo 1000})
Depois:
(:saldo cliente)
E recebe:
1000
Você constrói, testa e inspeciona o programa sem aquele ciclo pesado de compilar tudo e recomeçar.
Para quem viveu:
EDIT
COMPILE
LINK
RUN
ABEND
o REPL parece máquina do tempo.
E isso muda profundamente o workflow.
Em Clojure, o REPL não é apenas calculadora interativa.
É ferramenta de desenvolvimento.
Capítulo XII — Estado existe, mas precisa mostrar documento
Clojure não proíbe estado mutável.
Ele apenas não quer que mudanças fiquem espalhadas e invisíveis.
Exemplo com atom:
(def saldo
(atom 1000))
Consultar:
@saldo
Resultado:
1000
Alterar:
(swap! saldo - 200)
Agora:
@saldo
retorna:
800
A diferença é que o ponto onde existe identidade mutável está explícito.
A linguagem possui também:
Vars
Refs
Agents
Atoms
cada qual com propósitos diferentes.
Capítulo XIII — STM: transações sem banco de dados
Clojure também possui Software Transactional Memory.
Refs podem ser atualizadas coordenadamente dentro de transações de memória.
Isso parece quase familiar:
BEGIN
ALTERA A
ALTERA B
COMMIT
Mas sobre referências de memória.
A ideia é evitar manualmente boa parte do pesadelo de locks.
Não significa que STM seja solução universal.
Mas demonstra a obsessão de Hickey:
estado mutável precisa ser controlado, não espalhado.
Capítulo XIV — Concorrência: onde Clojure mostra por que nasceu
Imagine:
THREAD 1 → altera objeto
THREAD 2 → lê objeto
THREAD 3 → altera objeto
Se todo mundo compartilha estado mutável, temos:
race condition
locks
deadlocks
synchronization
Agora:
VALOR IMUTÁVEL
pode ser compartilhado livremente porque ninguém consegue alterá-lo.
Isso não elimina concorrência.
Mas reduz radicalmente certas classes de problema.
Esse é um dos grandes argumentos em favor da linguagem.
Capítulo XV — Java está logo abaixo do convés
Uma das vantagens fundamentais de Clojure é Java interop.
Você pode chamar diretamente:
(System/currentTimeMillis)
Ou:
(.toUpperCase "cobol")
Resultado:
"COBOL"
Ou criar:
(java.util.Date.)
Isso significa que Clojure não depende de esperar alguém escrever uma biblioteca “clojuriana” para tudo.
Pode aproveitar:
JDBC
Kafka
AWS SDK
Java crypto
HTTP clients
XML parsers
drivers
Essa foi uma decisão arquitetural brilhante.
Capítulo XVI — Performance: rápido ou lento?
Agora a pergunta inevitável.
Clojure não é simplesmente interpretada.
Ela compila para bytecode JVM.
Depois:
JVM
|
v
JIT
|
v
MACHINE CODE
Isso lhe permite boa performance para enorme quantidade de workloads empresariais.
Mas existem custos.
dynamic dispatch
boxing
reflection
persistent data structures
lazy sequences
object allocation
GC
Então não existe:
CLOJURE = LENTO
nem:
CLOJURE = TÃO RÁPIDO QUANTO C OTIMIZADO
O correto é:
depende do workload e da qualidade do código.
Capítulo XVII — Boxing, primitives e type hints
JVM possui tipos primitivos eficientes.
Mas linguagens dinâmicas podem acabar empacotando valores em objetos.
Isso custa memória e CPU.
Você pode ajudar o compilador em alguns casos:
(defn tamanho [^String texto]
(.length texto))
O:
^String
é type hint.
Isso reduz necessidade de reflection em determinados pontos.
Mas não faça isso em todo lugar por superstição.
A velha regra continua:
MEÇA
ANTES
DE
OTIMIZAR
Capítulo XVIII — Memória e persistent structures
Clojure frequentemente consome mais memória que código Java cuidadosamente otimizado usando arrays primitivos.
Exemplo:
Java int[]
pode ser extremamente compacto.
Uma collection Clojure genérica traz metadados, nós e objetos.
Mas as persistent structures também compartilham estrutura, evitando cópias inteiras.
Quando performance de construção é crítica, Clojure possui:
transients
que permitem uma fase de mutabilidade controlada para performance.
É quase:
“Você pode mexer, mas devolva tudo arrumado.”
Capítulo XIX — Garbage Collection: o monstro subterrâneo
Estamos na JVM.
Então existe GC.
A vantagem:
malloc/free manual
não é problema normal do programador.
A desvantagem:
muita alocação
|
v
GC
|
v
pausas / CPU
Em sistemas de baixa latência, você precisa observar:
heap
GC pause
allocation rate
latency p99
latency p999
working set
O COBOLzeiro olha e diz:
“Sabia que havia alguma coisa para monitorar.”
Sempre há.
Capítulo XX — Onde Clojure é particularmente forte?
Ela se encaixa muito bem em:
backends
serviços concorrentes
event processing
data pipelines
fintech
fraude
regra de negócio
APIs
Kafka
sistemas distribuídos
Datomic
Especialmente quando dados fluem por várias transformações.
O modelo:
DADOS
|
FUNÇÃO
|
DADOS
|
FUNÇÃO
pode resultar em código pequeno e expressivo.
Capítulo XXI — Fraquezas reais
Agora vamos parar de romance.
Curva de aprendizado
O problema não são apenas parênteses.
É mudar a cabeça.
Um desenvolvedor OO pergunta:
qual classe?
O clojurista pergunta:
quais dados?
qual transformação?
Ecossistema menor
Java, Python e JavaScript possuem comunidades muito maiores.
Empregabilidade mais estreita
Há empresas sérias usando Clojure, mas o número de vagas é menor.
Ferramentas
O ecossistema é bom, porém menos vasto.
Mensagens de erro
Podem ser duras para iniciantes.
JVM startup
Para scripts minúsculos pode ser pesado.
Memória
Pode haver overhead.
Capítulo XXII — Ferramentas da expedição
Hoje você normalmente encontrará:
Clojure CLI
deps.edn
REPL
nREPL
Clojure LSP
Calva
CIDER
Cursive
Leiningen
Para começar, eu escolheria algo simples:
Java
+
Clojure CLI
+
VS Code
+
Calva
ou IntelliJ + Cursive.
Não comece com quinze plugins.
Aprenda a linguagem.
Capítulo XXIII — Máquina necessária
Para aprender Clojure você não precisa de infraestrutura monstruosa.
Uma máquina perfeitamente normal:
4 cores
8 GB RAM
SSD
Java moderno
funciona bem.
16 GB deixam desenvolvimento mais confortável.
Produção obviamente depende do workload.
O tamanho correto não é:
MÁQUINA PADRÃO CLOJURE
É:
MÁQUINA ADEQUADA AO SERVIÇO
CPU.
RAM.
Throughput.
GC.
Latência.
Tudo de novo.
Capítulo XXIV — Licença e código-fonte
Clojure é open source.
Historicamente está sob a Eclipse Public License 1.0.
O projeto possui código público no GitHub.
Mas existe uma característica importante:
o desenvolvimento do core é relativamente conservador e controlado.
Isso não é necessariamente defeito.
Linguagem de programação é infraestrutura.
Quebrar compatibilidade todo semestre seria divertido apenas para consultorias.
Capítulo XXV — O conservadorismo inesperado
Aqui existe uma ironia deliciosa.
Clojure é:
Lisp
funcional
imutável
JVM
e parece radical.
Mas a evolução da linguagem é relativamente conservadora.
Rich Hickey valorizou muito:
stability
simplicity
backward compatibility
Para um COBOLzeiro isso deveria soar curiosamente familiar.
Tecnologia moderna com mentalidade:
“Não vamos quebrar produção só porque terça-feira chegou.”
Há algo admirável nisso.
Capítulo XXVI — Empregabilidade
Clojure é nicho.
Isso precisa ser dito claramente.
Não existe volume de vagas comparável a:
Java
Python
JavaScript
C#
Mas existe mercado profissional.
Fintechs.
Empresas de software.
Dados.
Saúde.
Sistemas distribuídos.
O curioso é a dinâmica:
POUCAS VAGAS
+
POUCOS PROFISSIONAIS
Isso pode gerar bons nichos.
Mas também significa maior dependência de especialização.
Capítulo XXVII — Nubank e o elefante roxo
Você não consegue falar de Clojure moderno sem falar do Nubank.
A empresa adotou Clojure cedo.
Adotou Datomic.
Escalou ambos brutalmente.
E em 2020 adquiriu a Cognitect.
Isso criou uma situação extraordinária.
CLOJURE
|
COGNITECT
|
NUBANK USA
|
NUBANK CRESCE
|
NUBANK COMPRA COGNITECT
O cliente tornou-se também sustentador central do ecossistema.
Isso dá enorme prova operacional.
Mas também concentra parte do ecossistema em torno de um usuário gigantesco.
Capítulo XXVIII — Penetração acadêmica
Clojure não é uma linguagem universitária dominante.
Universidades usam muito mais:
Python
Java
C
C++
Scheme
Racket
Haskell
dependendo do curso.
Clojure aparece em contextos como:
functional programming
concurrency
Lisp
language design
persistent structures
Seu crescimento é muito mais profissional/comunitário que acadêmico.
Capítulo XXIX — Como aprender sem afundar o Nautilus
Minha rota recomendada:
Etapa 1
Aprenda:
def
defn
let
if
vectors
maps
sets
keywords
Etapa 2
Depois:
map
filter
reduce
sort
group-by
Etapa 3
Entenda:
immutability
assoc
update
conj
Etapa 4
Aprenda:
->
->>
Etapa 5
Use REPL diariamente.
Etapa 6
Depois:
atom
refs
agents
STM
Etapa 7
Java interop.
Etapa 8
Somente então:
HTTP
Kafka
database
Datomic
Capítulo XXX — Mini sistema bancário
Vamos construir algo simples.
Cliente:
(def cliente
{:id 123
:nome "Arthur Dent"
:saldo 1000M})
Função débito:
(defn debitar [cliente valor]
(update cliente :saldo - valor))
Executar:
(def cliente2
(debitar cliente 200M))
Agora:
(:saldo cliente)
continua:
1000
Enquanto:
(:saldo cliente2)
é:
800
Este pequeno exemplo explica Clojure melhor que cinquenta slides.
Capítulo XXXI — Autorização financeira
Agora:
(defn autorizar [cliente valor]
(if (>= (:saldo cliente) valor)
{:status :aprovada
:cliente (debitar cliente valor)}
{:status :negada
:cliente cliente}))
Teste:
(autorizar cliente 200M)
Você recebe:
{:status :aprovada
:cliente {:id 123
:nome "Arthur Dent"
:saldo 800M}}
Observe o estilo.
Dados simples.
Funções simples.
Pouca cerimônia.
Nada de vinte classes para representar uma transferência de R$ 200.
Capítulo XXXII — Clojure versus Java
Simplificando brutalmente:
Java costuma incentivar:
OBJECT
|
METHOD
|
STATE
Clojure:
DATA
|
FUNCTION
|
NEW DATA
Não significa que Java seja incapaz de programação funcional.
Nem que Clojure não tenha estado.
É questão de gravidade cultural.
A linguagem puxa seu pensamento numa direção.
Capítulo XXXIII — Clojure versus COBOL
Agora vem uma comparação deliciosamente inesperada.
COBOL:
01 CLIENTE.
05 CLIENTE-ID PIC 9(09).
05 CLIENTE-NOME PIC X(30).
05 CLIENTE-SALDO PIC S9(9)V99.
Clojure:
{:id 123
:nome "Arthur"
:saldo 1000M}
Ambos possuem uma característica interessante:
dados estão na cara.
Clojure não exige que você esconda tudo dentro de dezenas de classes.
Nesse sentido, um COBOLzeiro pode achar Clojure menos alienígena depois de superar a aparência Lisp.
Capítulo XXXIV — E onde entra Datomic?
Agora finalmente tudo se encaixa.
Clojure diz:
VALORES IMUTÁVEIS
Datomic diz:
FATOS IMUTÁVEIS
Clojure:
VALOR T1
|
FUNÇÃO
|
VALOR T2
Datomic:
DATABASE T1
|
TRANSACTION
|
DATABASE T2
Clojure separa identidade de valor.
Datomic faz algo semelhante com banco e tempo.
Por isso:
CLOJURE
+
DATOMIC
é uma combinação intelectualmente coerente.
Capítulo XXXV — Easter egg: o nome Clojure
“Clojure” é uma brincadeira fonética com:
closure
um conceito importante da programação funcional.
Mas com:
J
fazendo referência à JVM/Java.
Uma pequena assinatura escondida no nome.
Capítulo XXXVI — Outro Easter egg: Simple Made Easy
Uma das palestras mais famosas de Rich Hickey é:
Simple Made Easy
A distinção central é maravilhosa:
SIMPLE
!=
EASY
Algo pode ser fácil de usar hoje e ainda assim criar enorme complexidade amanhã.
Isso deveria estar colado na parede de todo projeto corporativo.
Outro conceito famoso:
Hammock Driven Development
A ideia:
pense antes de sair digitando furiosamente.
Em alguns ambientes isso seria considerado atividade subversiva.
😄
Capítulo XXXVII — O que o COBOLzeiro deve levar consigo?
Não tente aprender Clojure como:
“Java com sintaxe estranha.”
Vai sofrer.
Também não tente:
“COBOL com parênteses.”
Pior ainda.
Pense:
DADOS
+
FUNÇÕES
+
VALORES
+
TRANSFORMAÇÕES
+
ESTADO EXPLÍCITO
Essa é a porta.
Epílogo — A jornada de volta à superfície
Depois de dias no interior da Terra, nosso explorador COBOL finalmente retorna.
No bolso:
Clojure
Na mochila:
REPL
immutability
persistent structures
functions
JVM
atoms
Datalog
Datomic
O colega pergunta:
— Então COBOL morreu?
Ele responde:
— Não.
— Java morreu?
— Também não.
— Clojure é o futuro?
— Para algumas coisas.
— Então o que você aprendeu?
O velho viajante olha para o horizonte.
— Que cada linguagem carrega uma teoria sobre como devemos pensar.
COBOL diz:
descreva claramente os dados e procedimentos do negócio.
Java tradicional diz:
modele entidades e comportamento através de objetos.
Clojure diz:
trate dados como valores, transforme-os com funções e seja extremamente cuidadoso quando precisar mudar estado.
Nenhuma frase substitui engenharia.
Mas cada uma muda as perguntas que fazemos.
E talvez essa seja a coisa mais fascinante em Clojure.
Ela não tenta simplesmente entregar:
mais uma linguagem
Ela tenta convencer o programador de que parte da complexidade de software foi criada por nós mesmos.
Porque passamos décadas dizendo:
X = 10
X = 20
X = 30
sem perguntar:
O que exatamente é X?
É o valor?
É a identidade?
É seu estado num determinado instante?
E o que aconteceu com os estados anteriores?
Clojure transformou essas perguntas em linguagem.
Datomic transformou parte delas em banco de dados.
Nubank transformou ambas em infraestrutura financeira real.
E nosso COBOLzeiro, depois de atravessar VSAM, IMS, Adabas, Db2 e quase cinquenta anos de evolução tecnológica, descobre algo curioso.
Ele não precisa abandonar o passado para compreender o futuro.
Muito pelo contrário.
Quem conhece bem:
estado
transação
dados
concorrência
integridade
performance
memória
recovery
já possui metade do mapa.
Só precisa aprender a ler as novas coordenadas.
O professor fecha o diário de bordo.
Na última página:
(println
"Não entre em pânico.
São apenas parênteses.")
E abaixo, escrito à mão por algum COBOLzeiro anônimo:
* TODO:
* DESCOBRIR ONDE DIABOS FICA A PROCEDURE DIVISION.
☕ Fim da viagem.
Sem comentários:
Enviar um comentário