Joao Brietzke Blog

← voltar

Lógica booleana e proposicional

O fundamento que o dev usa o tempo todo: proposições, operadores, tabelas-verdade, De Morgan e o custo exponencial de cada condição.

Lógica booleana é um daqueles assuntos que todo desenvolvedor usa em todas as horas de trabalho e poucos param pra estudar de verdade. Todo if, todo while, todo filtro, todo WHERE numa query é lógica booleana em ação. Justamente por ser tão onipresente, ela passa despercebida: a pessoa acha que domina porque conhece &&, || e !, e é aí que mora a diferença entre o dev que faz funcionar e o dev que entende o que está fazendo.

Este post cobre o assunto em si primeiro, e só depois mostra por que ele impacta diretamente a qualidade do código que você escreve.

O que é uma proposição

Uma proposição é qualquer afirmação que é verdadeira ou falsa, nunca as duas ao mesmo tempo. "O usuário é premium" é uma proposição. "O saldo é maior que zero" é uma proposição. No código, é qualquer expressão que resulta em true ou false:

const ehPremium = usuario.plano === "premium";
const temSaldo = saldo > 0;

Lógica proposicional é o estudo de como combinar essas afirmações e raciocinar sobre o resultado da combinação. É a gramática por baixo de toda decisão que o seu código toma.

Os operadores

Três operadores formam a base, e todo o resto se constrói a partir deles.

O NÃO (!) inverte o valor: !true é false. O E (&&) só é verdadeiro quando as duas partes são verdadeiras. O OU (||) é verdadeiro quando pelo menos uma parte é verdadeira.

Um detalhe que engana: na linguagem natural, "ou" costuma ser exclusivo ("café ou chá" significa um ou outro). Na lógica, || é inclusivo, verdadeiro até quando as duas partes são verdadeiras. Confundir os dois é uma fonte comum de erro ao traduzir uma regra de negócio pra código.

A tabela-verdade: a ferramenta que resolve qualquer dúvida

Uma tabela-verdade lista todas as combinações possíveis de entrada e o resultado de cada uma. Quando você não tem certeza se uma expressão está correta, não discute: enumera os casos e confere.

Para a && b:

a b a && b
V V V
V F F
F V F
F F F

A tabela-verdade é a forma honesta de provar que duas expressões fazem a mesma coisa. Gere a tabela das duas e compare linha a linha; se batem em todas, são equivalentes. Isso é útil na prática toda vez que você quer simplificar um condicional sem mudar o comportamento.

As equivalências: o coração do assunto

Duas expressões são equivalentes quando dão o mesmo resultado em todas as combinações. Saber transformar uma na outra é a habilidade central da lógica booleana, e é o que permite escrever a mesma decisão de forma mais legível.

As leis de De Morgan são as mais importantes. Negar um grupo inverte o operador:

!(a && b)  ===  !a || !b   // o E vira OU
!(a || b)  ===  !a && !b   // o OU vira E

Isso importa porque toda regra escrita na forma negativa ("não libera a menos que...", "bloqueia se não...") esconde um De Morgan, e traduzir errado é uma das causas mais comuns de bug em condicional. O erro clássico é achar que !(a && b) é !a && !b, o que é falso.

Outra distinção fundamental é entre implicação e recíproca. "Se A então B" não é o mesmo que "se B então A". "Se é premium, tem acesso" não quer dizer que "se tem acesso, é premium", porque a pessoa pode ter acesso por outro motivo. Confundir os dois é um erro de raciocínio, não de sintaxe, e é o que faz alguém ler uma regra como se fosse mão dupla.

E há equivalências úteis pra simplificar expressões, como a distributiva, que permite fatorar uma condição repetida:

(a && b) || (a && c)  ===  a && (b || c)

Dominar essas transformações é o que diferencia quem escreve um condicional emaranhado de quem escreve o mesmo comportamento de forma limpa.

Como isso impacta o dev: o custo de cada condição

Estabelecido o fundamento, chega a consequência prática mais importante, e a mais subestimada. Ela aparece quando a lógica booleana se acumula dentro de um método.

Cada condição booleana que você adiciona a um método dobra o número de comportamentos possíveis. Não soma, multiplica por dois. Um método cujo resultado depende de N condições tem 2^N combinações de entrada:

for (let n = 1; n <= 10; n++) {
  console.log(`${n} condições -> ${2 ** n} combinações`);
}
// 3  -> 8
// 5  -> 32
// 7  -> 128
// 10 -> 1024

Com 3 condições são 8 combinações, o que um humano segura na cabeça. Com 7 são 128. Com 10, mais de mil. O crescimento é exponencial, e o cérebro humano é ruim em sentir isso, então a quinta condição entra sem alarme e de repente o método tem centenas de caminhos que ninguém mapeou.

Esse número cobra em quatro lugares concretos:

Testar. Cobrir de verdade um método de N condições exigiria testar as 2^N combinações. Na prática, testa-se o caminho feliz e mais alguns casos. Num método de 7 condições, isso cobre talvez 4 dos 128 comportamentos; os outros vão pra produção sem nunca terem sido executados. A afirmação "está testado" perde sentido conforme as condições crescem.

Ler e entender. Pra prever o que o método faz numa combinação específica, você precisa simular aquela combinação na cabeça, e com muitas variáveis isso excede a memória de trabalho humana. O método vira uma caixa preta que funciona mas cujo comportamento ninguém consegue prever sem rodar. É daí que vem o medo de mexer em certos trechos, e o medo é racional.

Manter. Cada condição existente é uma interação potencial com qualquer mudança futura. Adicionar a nona condição a um método de oito insere uma variável que agora interage com todas as outras, e é assim que uma alteração pequena quebra um caso aparentemente sem relação.

Revisar o que a IA gera. Ferramentas de IA produzem métodos condicionais rápido e com boa aparência. Um método de 8 condições gerado assim tem 256 caminhos que ninguém testou. Revisar isso lendo linha por linha não funciona, porque o problema está na combinação, não em nenhuma linha isolada. Saber estimar o 2^N e reconhecer a explosão vira uma ferramenta de revisão.

O que o fundamento te permite fazer

É aqui que a lógica booleana deixa de ser teoria e vira vantagem prática. Quem domina o assunto responde à explosão de caminhos de três formas.

Trata cada condição como uma decisão de design, não como uma linha: antes de adicionar a próxima, reconhece que está dobrando o espaço de comportamento e pergunta se ela precisa mesmo estar ali, ou se o método está acumulando responsabilidades.

Reduz o espaço quebrando o método: dois métodos de 4 condições têm 16 + 16 comportamentos; um método de 8 tem 256. Separar a lógica em pedaços com responsabilidades claras reduz o espaço que cada pedaço precisa cobrir, e o todo volta a ser verificável.

Simplifica com equivalências e prova com tabela-verdade: usa De Morgan e a distributiva pra enxugar condições, e confirma que a versão nova é equivalente à antiga comparando as tabelas em vez de confiar no olho.

O princípio

Lógica booleana e proposicional não é um tópico acadêmico que ficou pra trás na faculdade. É o fundamento que decide quantos comportamentos um método pode ter, e esse número cresce de forma exponencial com cada condição. Dominar o assunto é o que permite manter o espaço de comportamento dentro do que um ser humano consegue verificar.

O dev que entende isso não escreve necessariamente menos condições. Ele escreve com consciência do custo: sabe quando a condição vale, quando o método precisa ser quebrado, e como provar que uma versão mais simples é equivalente. É a diferença entre um método que funciona e um método que você consegue afirmar que está correto.