Table of Contents

  1. Introdução
  2. Descobrindo Clojure: Uma Ponte entre Lisp e Java
  3. Manutenibilidade
  4. Transparência referencial
  5. Problemas de Reutilização
  6. Pattern Null Object
  7. Pattern Singleton
  8. Pattern Observer
  9. Recomendações

Introdução

Ao estudar diferentes linguagens de programação, fui atraído pela família Lisp devido à sua simplicidade e poder. Lisp, criado por John McCarthy em 1958, é conhecido pela notação de código como listas de dados e pela capacidade de manipular código como uma estrutura de dados. Emacs Lisp e Common Lisp são dois dos muitos dialetos que evoluíram a partir do Lisp original, cada um com suas próprias peculiaridades e casos de uso. Mas o que torna o Lisp tão atraente em comparação com linguagens mais modernas?

O Lisp continua a ser atraente em comparação com linguagens modernas principalmente notação prefixada e a natureza homoicônica (onde o código é tratado como uma lista de dados que pode ser manipulada pelo próprio programa) permitem uma flexibilidade incrível na macro expansão e manipulação de código. Isso torna o Lisp excepcionalmente poderoso para escrever código compacto e expressivo que pode modificar sua própria estrutura, o que é mais difícil em linguagens mais rígidas e menos dinâmicas. Saca só esse Pomodoro timer que fiz brincando como Emacs Lisp (um dialeto Lisp para o editor GNU Emacs).

Por exemplo, a introdução de dialetos Lisp como shcl, uma implementação de um shell script em Lisp que descobri recentemente, demonstra como essas características podem ser úteis em ambientes modernos. Além do tratamento de exceções com blocos try/catch, ele suporta recursos como logging avançado, gerenciamento automático de memória através de garbage collection e uma interface para manipular ambientes de execução de forma mais controlada. Essas funcionalidades enriquecem o shell, transformando scripts simples em aplicações robustas e menos propensas a erros.. Por exemplo:

1(try
2  (command-that-may-fail)
3  (catch 'error e
4    (println "Erro capturado: " e)))

Este exemplo ilustra como o Lisp, através de suas extensões e dialetos, se adapta e continua relevante, trazendo suas características poderosas para novos domínios e desafios na programação contemporânea.

Descobrindo Clojure: Uma Ponte entre Lisp e Java

Minha jornada com Lisp levou-me ao Clojure, um moderno dialeto que roda na Java Virtual Machine (JVM). Escolhi o Clojure por causa da interoperabilidade com código Java, já que eu tinha uma base sólida em Java. Esta transição foi também influenciada pela frustração com as complexidades da programação orientada a objetos (OOP) em Java.

Percebi que a construção de hierarquias de classes complexas frequentemente introduz uma complexidade desnecessária na OOP. Essa complexidade, especialmente em Java, surge mais das metodologias e tecnologias escolhidas do que dos desafios inerentes ao problema a ser resolvido. Por exemplo, montar um modelo de avião a partir de um kit pode ser simplificado com um design mais intuitivo e menos peças, semelhante à forma como a programação pode se beneficiar de abordagens que reduzem a rigidez e a complexidade não essencial.

Complexidades adicionais surgem não por causa do problema em si, mas devido às ferramentas e métodos escolhidos. Isso se manifesta através de práticas que complicam desnecessariamente o código, como a proliferação de POJOs (Plain Old Java Objects) e códigos boilerplate. POJOs, embora simples em uso, podem resultar em excesso de classes que servem principalmente para armazenar e recuperar dados. O código boilerplate, repetido em várias partes do aplicativo, inclui a implementação frequente de métodos como get e set, hashCode, equals e toString, que obscurecem a lógica principal do código e aumentam a carga de manutenção.

Considere o exemplo clássico abaixo na classe Employee abaixo, em Java, que exemplifica essa questão:

 1public class Employee {
 2    private String name;
 3    private int age;
 4    private String department;
 5
 6    public Employee(String name, int age, String department) {
 7        this.name = name;
 8        this.age = age;
 9        this.department = department;
10    }
11
12    // Getters and Setters
13    public String getName() {
14        return name;
15    }
16
17    public void setName(String name) {
18        this.name = name;
19    }
20
21    public int getAge() {
22        return age;
23    }
24
25    public void setAge(int age) {
26        this.age = age;
27    }
28
29    public String getDepartment() {
30        return department;
31    }
32
33    public void setDepartment(String department) {
34        this.department = department;
35    }
36
37    @Override
38    public boolean equals(Object obj) {
39        if (this == obj) return true;
40        if (obj == null || getClass() != obj.getClass()) return false;
41        Employee employee = (Employee) obj;
42        return age == employee.age &&
43               Objects.equals(name, employee.name) &&
44               Objects.equals(department, employee.department);
45    }
46
47    @Override
48    public int hashCode() {
49        return Objects.hash(name, age, department);
50    }
51
52    @Override
53    public String toString() {
54        return "Employee{" +
55               "name='" + name + '\'' +
56               ", age=" + age +
57               ", department='" + department + '\'' +
58               '}';
59    }
60}

Geralmente quando o cenário é este, a sugestão que brota automaticamente na maioria das vezes é o uso de bibliotecas como Lombok que podem ajudar a reduzir essa complexidade acidental ao gerar automaticamente código boilerplate durante a compilação com anotações como @Data, @Getter, @Setter, @EqualsAndHashCode, e @ToString. Essas anotações substituem a necessidade de escrever manualmente o código mostrado acima. Contudo, essa abordagem pode reduzir a transparência do código e complicar a depuração.

Em contraste, em Clojure, uma linguagem funcional que é dialeto LISP, a modelagem de dados e comportamentos é abordada de maneira diferente. Utilizando estruturas de dados imutáveis e funções puras, Clojure elimina muitos problemas associados à complexidade acidental em OOP. Veja como o exemplo da classe Employee seria em Clojure:

 1(defrecord Employee [name age department])
 2
 3(defn create-employee [name age department]
 4  (->Employee name age department))
 5
 6(defn employee-name [employee]
 7  (:name employee))
 8
 9(defn set-employee-name [employee new-name]
10  (assoc employee :name new-name))
11
12(defn employee-age [employee]
13  (:age employee))
14
15(defn set-employee-age [employee new-age]
16  (assoc employee :age new-age))
17
18(defn employee-department [employee]
19  (:department employee))
20
21(defn set-employee-department [employee new-department]
22  (assoc employee :department new-department))

Aqui, defrecord cria uma estrutura de dados com campos nomeados, gerando funções para acessar e modificar esses campos, promovendo a imutabilidade. Isso reduz a probabilidade de erros comuns, como alterações de estado não intencionais, e simplifica o entendimento, teste e manutenção do código.

Essa abordagem minimiza a cerimônia e o boilerplate típicos da OOP em Java, focando mais nos dados e comportamentos reais do que na estrutura de classes. Além disso, defrecord implementa automaticamente interfaces para serialização e outras funcionalidades, oferecendo mais do que apenas uma simplificação do acesso aos dados.

É essencial reconhecer que muito depende das escolhas e habilidades do desenvolvedor. No entanto, a linguagem, juntamente com seus frameworks, bibliotecas e a filosofia subjacente da POO, tendem a conduzir os desenvolvedores para uma maior complexidade. Java, como uma linguagem projetada com uma forte inclinação para a POO, encoraja a criação de hierarquias de classes e o uso de padrões de design que, embora úteis em muitos contextos, podem adicionar camadas de complexidade não pertinentes à lógica de negócios.

Frameworks Java, como Spring e Hibernate, oferecem ferramentas de abstração poderosas que simplificam o desenvolvimento, mas podem levar a um código altamente acoplado e difícil de gerenciar se não forem usados com discernimento. POJOs e código boilerplate são exemplos de como práticas comuns em Java podem contribuir para a complexidade.

Embora a intenção seja promover a reutilização de código e a clareza, a implementação repetitiva de métodos padrão em cada classe pode obscurecer a lógica principal e aumentar a carga de manutenção, especialmente em projetos de grande escala.

Portanto, embora muito dependa do desenvolvedor em termos de como ele escolhe estruturar e implementar soluções em Java, o ambiente e as práticas incentivadas pela linguagem e seus frameworks geralmente conduzem a uma complexidade desnecessária. Isso destaca a importância de uma abordagem crítica e reflexiva no uso das capacidades da linguagem para evitar armadilhas.

Manutenibilidade

Este conceito se refere à facilidade com que um software pode ser modificado para corrigir falhas, melhorar a funcionalidade ou adaptar-se a mudanças no ambiente. Java incentiva a modelagem de soluções através de hierarquias de classes e interfaces. Um exemplo clássico dessa abordagem é o padrão de design Composite, utilizado para construir uma estrutura de objetos em que os objetos individuais podem ser tratados de maneira uniforme. Veja um exemplo simplificado abaixo:

 1interface Component {
 2    void operation();
 3}
 4
 5class Leaf implements Component {
 6    public void operation() {
 7        // realiza uma operação específica
 8    }
 9}
10
11class Composite implements Component {
12    private List<Component> children = new ArrayList<>();
13
14    public void operation() {
15        for (Component child : children) {
16            child.operation();
17        }
18    }
19
20    public void add(Component component) {
21        children.add(component);
22    }
23
24    public void remove(Component component) {
25        children.remove(component);
26    }
27}

Este padrão é útil, mas à medida que a árvore de componentes cresce e se torna mais complexa, a manutenção pode se tornar um desafio. O gerenciamento de estados e o impacto das mudanças em uma parte do sistema podem afetar outras partes de maneira não trivial, aumentando o risco de bugs e reduzindo a clareza do código.

Por outro lado, Clojure promove a imutabilidade e o uso de funções puras. Isso resulta em um estilo de código que é frequentemente mais conciso e expressivo. Um exemplo equivalente ao padrão Composite em Clojure pode ser visto na manipulação de estruturas de dados aninhadas, como listas ou vetores, utilizando funções de alta ordem:

 1(defn operation [component]
 2  (if (coll? component)
 3    (map operation component)
 4    ;; realiza uma operação específica para não coleções
 5    ))
 6
 7(def tree
 8  [[:a] [:b [:b1 :b2] [:b3]] [:c]])
 9
10(operation tree)

Neste exemplo, a função operation manipula recursivamente uma estrutura aninhada, aplicando a operação a cada elemento, seja ele simples ou composto. A clareza e simplicidade do código são mantidas, mesmo quando a estrutura se torna complexa. A manutenibilidade em Java pode ser desafiadora devido à necessidade de gerenciar estados mutáveis e a complexidade associada a hierarquias de classes extensas. A herança e o polimorfismo, enquanto poderosos, podem introduzir dependências ocultas e efeitos colaterais que dificultam as modificações e a compreensão do sistema como um todo.

A imutabilidade por padrão ajuda a evitar muitos bugs comuns associados a mudanças de estado, e as funções puras promovem um estilo de programação que é intrinsecamente mais fácil de testar e verificar.

Transparência referencial

A transparência referencial é um conceito fundamental em programação funcional e se refere à propriedade de funções onde a substituição de uma expressão por seu valor não altera o comportamento do programa. Este conceito é crucial para garantir a previsibilidade e a confiabilidade do código, especialmente em análises e otimizações de compiladores.

Java frequentemente lida com estados mutáveis, o que pode comprometer a transparência referencial. Em Java, objetos podem ter seus estados internos alterados, o que significa que chamadas repetidas ao mesmo método no mesmo objeto podem produzir resultados diferentes se o estado do objeto tiver sido modificado entre chamadas. Vejamos um exemplo clássico na classe Counter abaixo:

 1class Counter {
 2    private int count = 0;
 3
 4    public void increment() {
 5        count += 1;
 6    }
 7
 8    public int getCount() {
 9        return count;
10    }
11}
12
13Counter myCounter = new Counter();
14System.out.println(myCounter.getCount());  // Output: 0
15myCounter.increment();
16System.out.println(myCounter.getCount());  // Output: 1

Neste exemplo, a chamada ao método getCount retorna resultados diferentes dependendo do estado interno do objeto Counter. Esta não é uma função com transparência referencial, pois o resultado depende do histórico de interações com o objeto, não apenas de seus argumentos de entrada. Em contraste, Clojure promove a imutabilidade e funções puras, que são compatíveis com a transparência referencial. Os dados são imutáveis por padrão, e as funções são projetadas para não ter efeitos colaterais. Isso facilita o raciocínio sobre o código e a previsão de seu comportamento, independentemente do contexto em que é utilizado. Um exemplo análogo ao contador em Java pode ser implementado em Clojure como segue:

1(defn increment [count]
2  (+ count 1))
3
4(def initial-count 0)
5(def new-count (increment initial-count))

Neste caso, a função increment é pura e possui transparência referencial. Independente de quantas vezes você chame increment com o mesmo valor de count, o resultado será sempre o mesmo, e não haverá efeitos colaterais que alterem outros estados ou dados. Isso não apenas simplifica o entendimento do código, mas também o torna mais seguro em ambientes concorrentes e distribuídos.

A falta de transparência referencial em Java pode levar a bugs sutis e dificuldades de manutenção, pois o desenvolvedor deve manter um modelo mental do estado de todo o sistema ao analisar o comportamento do código. Isso é especialmente problemático em sistemas grandes e complexos, onde os efeitos colaterais de uma parte do sistema podem ter implicações inesperadas em outra parte.

Em Clojure, a garantia de transparência referencial contribui para uma maior modularidade e reusabilidade do código. Funções puras e imutáveis podem ser combinadas e reutilizadas em diferentes contextos sem preocupação com interações inesperadas, tornando o sistema globalmente mais fácil de entender e de manter.

A transparência referencial é mais do que uma característica técnica; é uma filosofia de design que influencia profundamente a confiabilidade, a manutenibilidade e a escalabilidade de software. Enquanto Java pode ser adaptado para seguir este princípio até certo ponto (usando objetos imutáveis e minimizando efeitos colaterais), Clojure o incorpora de maneira fundamental.

Problemas de Reutilização

A reutilização de código é um objetivo desejado em desenvolvimento de software, visando reduzir redundância, aumentar a eficiência e facilitar a manutenção. No entanto, as abordagens de Java (orientada a objetos) e Clojure (funcional) apresentam desafios distintos que podem afetar a eficácia da reutilização do código.

Em Java, a reutilização de código é frequentemente implementada através de herança, permitindo que classes derivadas reutilizem código de suas classes base. Embora útil, este método pode levar a problemas de acoplamento e fragilidade. Por exemplo, a herança profunda pode obscurecer a origem dos dados e métodos, dificultando a rastreabilidade e a manutenção do código. Além disso, alterações na classe base podem ter efeitos colaterais inesperados nas classes derivadas. Vejamos um exemplo clássico na classe Vehicle abaixo:

 1class Vehicle {
 2    void startEngine() {
 3        System.out.println("Engine started.");
 4    }
 5}
 6
 7class Car extends Vehicle {
 8    void openSunroof() {
 9        System.out.println("Sunroof opened.");
10    }
11}
12
13class ElectricCar extends Car {
14    @Override
15    void startEngine() {
16        System.out.println("Electric engine started silently.");
17    }
18}

Neste exemplo, ElectricCar reutiliza o código de Vehicle e Car, mas a sobrescrita do método startEngine introduz um comportamento específico que pode não ser esperado ou desejado em todas as situações onde Car é utilizado. Clojure, ao enfatizar a programação funcional, promove a composição de funções como método primário para reutilização de código. Funções em Clojure são projetadas para serem pequenas, com um único propósito, e facilmente combináveis sem efeitos colaterais, o que promove a reutilização. Por exemplo:

 1(defn add [x y]
 2  (+ x y))
 3
 4(defn add-five [x]
 5  (add x 5))
 6
 7(defn multiply-by-two [x]
 8  (* x 2))
 9
10(defn add-five-and-double [x]
11  (multiply-by-two (add-five x)))

Neste exemplo, as funções add, add-five, e multiply-by-two são compostas para criar uma nova função add-five-and-double. Cada função pode ser reutilizada independentemente em outros contextos.

A reutilização em Java, baseada em herança, pode levar à rigidez e a um acoplamento não intencional entre classes. A herança impõe uma relação “é-um” que nem sempre é a mais adequada para todos os cenários de reutilização, e a modificação de classes base pode afetar todas as subclasses de maneiras inesperadas.

Um exemplo clássico de rigidez e acoplamento não intencional ocorre quando se usa herança para compartilhar funcionalidades entre classes que não possuem uma relação natural de “é-um”. Isso pode levar a uma estrutura de código onde mudanças em uma superclasse afetam inesperadamente suas subclasses.

Considere um sistema onde uma classe Animal é usada como superclasse para várias subclasses específicas de animais. A classe Animal pode incluir métodos que não são aplicáveis a todas as suas subclasses, forçando um comportamento específico onde ele pode não ser necessário ou adequado.

 1class Animal {
 2    void breathe() {
 3        System.out.println("Breathe in, breathe out.");
 4    }
 5
 6    void eat() {
 7        System.out.println("Eating food.");
 8    }
 9}
10
11class Fish extends Animal {
12    @Override
13    void breathe() {
14        System.out.println("Breathing through gills.");
15    }
16
17    void swim() {
18        System.out.println("Swimming in water.");
19    }
20}
21
22class Bird extends Animal {
23    @Override
24    void eat() {
25        System.out.println("Eating seeds.");
26    }
27
28    void fly() {
29        System.out.println("Flying in the sky.");
30    }
31}

A classe Animal define métodos breathe e eat que todas as subclasses são forçadas a herdar. Isso pode não ser ideal para todos os tipos de animais, como os que possuem métodos de respiração ou alimentação únicos, exigindo que essas subclasses sobrescrevam esses métodos para comportamentos específicos. Todas as subclasses de Animal estão ligadas às implementações de breathe e eat da superclasse.

Qualquer mudança nos métodos da classe Animal pode ter efeitos colaterais em Fish, Bird e outras subclasses. Por exemplo, se adicionarmos um método sleep na classe Animal que define como os animais dormem, todas as subclasses terão esse método, mesmo que a maneira de dormir varie entre diferentes tipos de animais, ou mesmo não seja relevante para todos.

Este exemplo ilustra como a herança em Java pode introduzir rigidez e acoplamento entre classes. A herança obriga as subclasses a aderirem à interface e ao comportamento da superclasse, o que pode não ser desejável e pode limitar a flexibilidade do design. Mudanças na superclasse podem afetar todas as subclasses de maneiras não previstas, tornando o sistema mais difícil de manter e evoluir. A composição, em vez de herança, é frequentemente recomendada para mitigar esses problemas, permitindo maior flexibilidade e reduzindo o acoplamento indesejado.

Em contraste, Clojure favorece uma abordagem de composição funcional, onde a reutilização do código é alcançada combinando funções menores e mais genéricas. Essa abordagem reduz o acoplamento e aumenta a flexibilidade, permitindo que os desenvolvedores construam novas funcionalidades de maneira mais previsível e segura.

Pattern Null Object

O uso de nil em Clojure como um objeto nulo simplifica o tratamento de valores ausentes em sequências, coleções e mapas. Este suporte embutido reduz a necessidade de verificações extensivas de nulos e tratamentos especiais. Em contraste, o null de Java requer verificações explícitas para prevenir NullPointerException, levando a um código verboso e propenso a erros. Exemplo:

Em Clojure:

1(first nil) ;; nil
2(get nil :chave) ;; nil

Já em Java:

1List<String> lista = null;
2if (lista != null) {
3    lista.get(0);
4}

O tratamento de valores nulos em Java é uma fonte frequente de erros em tempo de execução e isso aumenta a complexidade do código e a probabilidade de bugs, prejudicando a confiabilidade do software visto que sempre precisa tratar o uso do null.

Pattern Singleton

O defonce e referências atômicas em Clojure fornecem uma maneira simples e segura para threads de implementar singletons. No entanto, o padrão Singleton é geralmente desencorajado em ambos os paradigmas devido à sua tendência de introduzir estado global e dependências difíceis de gerenciar e testar. A implementação tradicional de singleton em Java muitas vezes envolve mecanismos complexos de sincronização para garantir a segurança em threads, aumentando a complexidade. Exemplo:

Em Clojure:

1(defonce servidor-web (atom nil))
2
3(defn iniciar-servidor []
4  (if @servidor-web
5    (println "Servidor já está em execução!")
6    (reset! servidor-web (iniciar-jetty rotas {:port 8080 :join? false}))))

Em Java:

 1public class ServidorWeb {
 2    private static ServidorWeb instancia;
 3
 4    private ServidorWeb() {}
 5
 6    public static synchronized ServidorWeb getInstancia() {
 7        if (instancia == null) {
 8            instancia = new ServidorWeb();
 9        }
10        return instancia;
11    }
12}

O pattern Singleton introduz dependências ocultas e estado global, que podem levar a um acoplamento rígido e dificuldade nos testes. Em Java, garantir a segurança em threads em singletons adiciona complexidade adicional e potencial para bugs sutis.

Pattern Observer

A função add-watch de Clojure e a biblioteca core.async oferecem soluções elegantes para implementar o padrão observer, permitindo modelos de programação reativa. O pattern observer em Java envolve a definição de objetos observáveis e interfaces de observadores, o que pode resultar em acoplamento rígido entre os componentes e mecanismos de tratamento de eventos mais complexos. Exemplo:

Em Clojure:

1(def jogador (atom {:pontuacao 0}))
2
3(add-watch jogador :pontuacao
4  (fn [chave ref antigo novo]
5    (println "Pontuação atualizada:" (:pontuacao novo))))

Em Java:

 1public class Jogador extends Observable {
 2    private int pontuacao;
 3
 4    public void setPontuacao(int pontuacao) {
 5        this.pontuacao = pontuacao;
 6        setChanged();
 7        notifyObservers();
 8    }
 9}
10
11public class PontuacaoObserver implements Observer {
12    @Override
13    public void update(Observable o, Object arg) {
14        Jogador jogador = (Jogador) o;
15        System.out.println("Pontuação atualizada: " + jogador.getPontuacao());
16    }
17}

O pattern observer em Java frequentemente leva a componentes fortemente acoplados e gerenciamento de eventos complexo. Essa complexidade pode tornar o sistema mais difícil de manter e estender, especialmente à medida que o número de observadores cresce…


A comparação entre Java e Clojure é relevante porque ambos operam na Java Virtual Machine (JVM). Isso permite analisar diretamente as características dos paradigmas de programação orientada a objetos e funcional. Java, com seu vasto ecossistema, exemplifica a orientação a objetos, enquanto Clojure oferece uma abordagem funcional. A utilização da JVM garante que a comparação se concentre nas diferenças paradigmáticas, sem interferências externas, tornando as discussões sobre performance, manutenção e design de código mais claras e fundamentadas.

Outras linguagens funcionais, como Elixir e Haskell, também poderiam ser comparadas a Java. Elixir, rodando na Erlang VM, é conhecida por sua capacidade de lidar com high availability distributed systems. Haskell é elogiada por seu strong type system e lazy evaluation, minimizando bugs e maximizando a eficiência em algoritmos complexos. No entanto, essas comparações introduziriam variáveis adicionais devido às diferenças nas virtual machines e execution models, complicando a análise dos paradigmas de programação.

Explorar Clojure ou outras linguagens funcionais pode melhorar suas habilidades de programação e oferecer novas ferramentas para resolver problemas de maneiras inovadoras. Aprender novos paradigmas expande sua capacidade de escolher a ferramenta certa para cada trabalho, abrindo novos caminhos para soluções criativas. Embora desafiador, experimentar algo novo proporciona crescimento profissional e satisfação na resolução de problemas.


Recomendações

Se você está interessado em aprofundar seu conhecimento, aqui estão alguns livros que recomendo. São eles:

  • “Clojure Programming” por Chas Emerick, Brian Carper, e Christophe Grand
    • Este livro é uma introdução completa ao mundo do Clojure, abordando desde conceitos básicos até tópicos avançados como programação concorrente e interoperabilidade com Java.
  • “Java Concurrency in Practice” por Brian Goetz
    • Fundamental para entender a gestão de concorrência em Java, este livro oferece diretrizes práticas para escrever programas seguros e com alta performance.
  • “Programming Clojure” por Alex Miller e Stuart Halloway
    • Adequado para iniciantes e programadores experientes que querem aprender Clojure, o livro detalha a sintaxe, as estruturas de dados e o estilo funcional de programação.
  • “Effective Java” por Joshua Bloch
    • Essencial para qualquer desenvolvedor Java, aborda práticas recomendadas acumuladas durante anos de experiência na Sun Microsystems.
  • “The Joy of Clojure” por Michael Fogus e Chris Houser
    • Ideal para quem já tem experiência com Clojure e deseja explorar os aspectos filosóficos e menos pragmáticos da linguagem.
  • “Java Performance: The Definitive Guide” por Scott Oaks
    • Um guia crucial para desenvolvedores que precisam otimizar aplicações Java, focando em ferramentas de monitoramento e técnicas de melhoria de performance.
  • “Land of Lisp: Learn to Program in Lisp, One Game at a Time!” por Conrad Barski
    • Este livro é uma introdução divertida e acessível ao Lisp através do desenvolvimento de jogos. Ele ensina a programar em Lisp e desenvolver seus próprios jogos, o que torna o aprendizado interativo e prazeroso.
  • “Practical Common Lisp” por Peter Seibel
    • Um guia prático que detalha o uso do Common Lisp na programação do dia a dia, oferecendo uma ótima introdução à linguagem e mostrando como ela pode ser usada para resolver problemas reais.