Clase 046 — Booleanos y valores de verdad

Parte 3 — Valores, tipos y variables · ⏱️ Duración estimada: 90 min · Nivel: IntermedioClase construida — 10 implementaciones del núcleo verificadas contra casos.json.


🎯 Objetivo

En 1854 George Boole demostró que la lógica podía tratarse como un álgebra: un cálculo con solo dos valores, verdadero y falso, y tres operaciones —conjunción, disyunción y negación— capaces de expresar cualquier razonamiento. Casi un siglo después, Claude Shannon vio que esos mismos dos valores describían los circuitos de conmutación, y con ello el booleano se convirtió en la piedra angular de toda la computación. El objetivo de esta clase es que domines ese núcleo mínimo —AND (ambos), OR (alguno) y NOT (negación)— no como tres funciones sueltas, sino como el lenguaje en el que se escribe toda decisión que tomará un programa.

Cada if, cada while, cada filtro de una consulta se reduce en el fondo a evaluar una expresión booleana. Por eso conviene verlos aislados, con su tabla de verdad completa, antes de que en la Parte 4 aparezcan enredados dentro de condiciones complejas. Aquí construimos un booleano a partir de una entrada numérica (0 o 1), aplicamos las tres operaciones y observamos algo revelador: los lenguajes no se ponen de acuerdo en cómo escribir "verdadero". Python y C# imprimen True con mayúscula; JavaScript, Java o Go imprimen true; C ni siquiera tuvo un tipo booleano nativo hasta 1999. Esa disparidad obliga a normalizar la salida, y esa normalización es, en sí misma, una lección sobre cómo cada lenguaje modela la verdad.

📚 Resultados de aprendizaje

Al finalizar, podrás:

  1. Calcular AND, OR y NOT sobre valores booleanos.
  2. Construir un booleano a partir de una entrada (0/1).
  3. Normalizar la impresión de booleanos entre lenguajes.

🗺️ Temas

# Tema Por qué importa
1 AND, OR, NOT Las tres operaciones lógicas fundamentales
2 Representar la verdad 0/1, true/false, según el lenguaje
3 Impresión de booleanos true vs. True: hay que normalizar
4 Base de las condiciones Todo if depende de un booleano

📖 Definiciones y características

Un booleano es un tipo con exactamente dos habitantes: verdadero y falso. Desde la perspectiva de la teoría de tipos que expone Pierce, es el tipo suma más pequeño no trivial, y su valor típico nace de una comparación (a > b, x == 0) o de otra expresión booleana. Las tres operaciones fundamentales forman, junto a esos dos valores, un álgebra de Boole completa: AND (∧, conjunción) es verdadero solo cuando ambos operandos lo son; OR (∨, disyunción) es verdadero cuando al menos uno lo es; y NOT (¬, negación) invierte el valor. Con solo estas tres se puede construir cualquier función lógica imaginable —de hecho, con NAND o NOR una sola basta—, y de ahí su papel de cimiento.

Hay dos sutilezas que distinguen la teoría de la práctica en los lenguajes reales. La primera es la evaluación en cortocircuito: en a && b, si a ya es falso, el resultado es falso sin necesidad de mirar b, así que la mayoría de lenguajes ni siquiera lo evalúan; lo mismo con a || b cuando a es verdadero. Esto importa cuando b tiene efectos secundarios o podría fallar (por ejemplo, p != null && p.valor > 0). La segunda es la veracidad (truthiness): muchos lenguajes dinámicos permiten usar valores no booleanos donde se espera una condición —el 0, la cadena vacía o null se consideran "falsos"—, una comodidad que Haverbeke advierte que puede sorprender en JavaScript. En esta clase evitamos esa ambigüedad convirtiendo explícitamente la entrada con a != 0, para obtener un booleano genuino antes de operar.

Los términos en breve:

🧩 Situación

"Si es fin de semana Y no llueve, salgo al parque." Esa frase cotidiana es ya una expresión booleana: dos condiciones (es_finde, no_llueve) unidas por un AND. Cambia el conector y cambia la política: "si es feriado O es fin de semana" abre más días; "si NO hay reunión" niega una condición. Toda regla de negocio, todo permiso de acceso, todo filtro se descompone en esta gramática de tres operadores, y por eso practicarla aislada —con la tabla de verdad delante— es la mejor inversión antes de encadenar condiciones reales.

El laboratorio recibe dos bits a b y produce and, or y not_a. Fíjate en el caso 1 0 de casos.json, cuyo esperado es and=false or=true not_a=false: con a=verdadero y b=falso, la conjunción es falsa (no ambos), la disyunción verdadera (al menos uno), y la negación de a es falsa. El caso 0 0 da and=false or=false not_a=true, el único donde not_a se vuelve verdadero. Reproducir estas tres filas exactas en diez lenguajes obliga a resolver el problema real: cada uno imprime la verdad con una grafía distinta, y hay que unificarla.

🧮 Modelo

Especificación y verificación en casos.json:

stdin esperado
1 0 and=false or=true not_a=false
1 1 and=true or=true not_a=false
0 0 and=false or=false not_a=true

📐 Algoritmo (pseudocódigo neutral)

LEER a, b
ba <- (a != 0) ; bb <- (b != 0)
ESCRIBIR "and=" (ba Y bb) " or=" (ba O bb) " not_a=" (NO ba)

🌐 Implementaciones idiomáticas — el código a la vista

Mismo algoritmo, forma idiomática en cada lenguaje. Todas producen la salida de casos.json. Cada bloque es el archivo real de implementaciones/: el enlace de cada lenguaje abre su fuente, y el comando de al lado lo ejecuta.

Python · python/main.py · python main.py

import sys

a, b = map(int, sys.stdin.readline().split())
ba, bb = a != 0, b != 0
tf = lambda x: "true" if x else "false"
print(f"and={tf(ba and bb)} or={tf(ba or bb)} not_a={tf(not ba)}")

🧬 El mismo programa en la familia Scripting dinámico: Ruby · Perl · Lua · Tcl · R

JavaScript · javascript/main.mjs · node main.mjs

import { readFileSync } from "node:fs";

const [ai, bi] = readFileSync(0, "utf8").trim().split(/\s+/).map(Number);
const a = ai !== 0;
const b = bi !== 0;
const tf = (x) => (x ? "true" : "false");
console.log(`and=${tf(a && b)} or=${tf(a || b)} not_a=${tf(!a)}`);

🧬 El mismo programa en la familia JavaScript / web: Dart · ActionScript

TypeScript · typescript/main.ts · pnpm exec tsx main.ts

import { readFileSync } from "node:fs";

const [ai, bi]: number[] = readFileSync(0, "utf8").trim().split(/\s+/).map(Number);
const a: boolean = ai !== 0;
const b: boolean = bi !== 0;
const tf = (x: boolean): string => (x ? "true" : "false");
console.log(`and=${tf(a && b)} or=${tf(a || b)} not_a=${tf(!a)}`);

🧬 El mismo programa en la familia JavaScript / web: Dart · ActionScript

Java · java/Main.java · java Main.java

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class Main {
    static String tf(boolean x) {
        return x ? "true" : "false";
    }

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        String[] p = br.readLine().trim().split("\\s+");
        boolean a = Integer.parseInt(p[0]) != 0;
        boolean b = Integer.parseInt(p[1]) != 0;
        System.out.printf("and=%s or=%s not_a=%s%n", tf(a && b), tf(a || b), tf(!a));
    }
}

🧬 El mismo programa en la familia JVM: Kotlin · Scala · Groovy · Clojure

C# · csharp/Program.cs · dotnet run

using System;

string[] p = Console.In.ReadToEnd()
    .Split(new[] { ' ', '\t', '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries);
bool a = int.Parse(p[0]) != 0;
bool b = int.Parse(p[1]) != 0;
string Tf(bool x) => x ? "true" : "false";
Console.WriteLine($"and={Tf(a && b)} or={Tf(a || b)} not_a={Tf(!a)}");

🧬 El mismo programa en la familia .NET: F# · VB.NET

Go · go/main.go · go run main.go

package main

import (
    "bufio"
    "fmt"
    "os"
    "strconv"
    "strings"
)

func tf(x bool) string {
    if x {
        return "true"
    }
    return "false"
}

func main() {
    line, _ := bufio.NewReader(os.Stdin).ReadString('\n')
    f := strings.Fields(line)
    ai, _ := strconv.Atoi(f[0])
    bi, _ := strconv.Atoi(f[1])
    a, b := ai != 0, bi != 0
    fmt.Printf("and=%s or=%s not_a=%s\n", tf(a && b), tf(a || b), tf(!a))
}

🧬 El mismo programa en la familia Sistemas: Zig · Nim · D

Rust · rust/main.rs · rustc main.rs -o main && ./main

use std::io::Read;

fn tf(x: bool) -> &'static str {
    if x { "true" } else { "false" }
}

fn main() {
    let mut s = String::new();
    std::io::stdin().read_to_string(&mut s).unwrap();
    let v: Vec<i64> = s.split_whitespace().map(|x| x.parse().unwrap()).collect();
    let (a, b) = (v[0] != 0, v[1] != 0);
    println!("and={} or={} not_a={}", tf(a && b), tf(a || b), tf(!a));
}

🧬 El mismo programa en la familia Sistemas: Zig · Nim · D

C · c/main.c · cc main.c -o main && ./main

#include <stdio.h>

static const char *tf(int x) {
    return x ? "true" : "false";
}

int main(void) {
    int a, b;
    if (scanf("%d %d", &a, &b) != 2) return 1;
    a = a != 0;
    b = b != 0;
    printf("and=%s or=%s not_a=%s\n", tf(a && b), tf(a || b), tf(!a));
    return 0;
}

🧬 El mismo programa en la familia C / llaves: C++ · Objective-C

SQL · sql/main.sql · sqlite3 :memory: < main.sql

-- SQL no tiene tipo booleano nativo: se expresa con CASE WHEN.
WITH pares(a, b) AS (VALUES (1, 0), (1, 1), (0, 0))
SELECT printf('and=%s or=%s not_a=%s',
       CASE WHEN a <> 0 AND b <> 0 THEN 'true' ELSE 'false' END,
       CASE WHEN a <> 0 OR b <> 0 THEN 'true' ELSE 'false' END,
       CASE WHEN NOT (a <> 0) THEN 'true' ELSE 'false' END) AS resultado
FROM pares;

🧬 El mismo programa en la familia Lógica y declarativa: Prolog · Datalog

PHP · php/main.php · php main.php

<?php
[$a, $b] = preg_split('/\s+/', trim(fgets(STDIN)));
$a = ((int) $a) !== 0;
$b = ((int) $b) !== 0;
$tf = fn($x) => $x ? "true" : "false";
printf("and=%s or=%s not_a=%s\n", $tf($a && $b), $tf($a || $b), $tf(!$a));

🧬 El mismo programa en la familia Scripting dinámico: Ruby · Perl · Lua · Tcl · R

SQL es declarativo: no lee de stdin como los demás; su implementación muestra la misma idea sobre una tabla de casos, y el verificador la marca como ilustrativa.

🔎 Recorrido guiado por el código

Sigamos el caso testigo 1 0, cuya salida esperada en casos.json es and=false or=true not_a=false, para ver cómo cada lenguaje llega exactamente ahí.

En Python, tras leer los dos enteros, la línea ba, bb = a != 0, b != 0 es el paso conceptual clave: convierte números en booleanos genuinos. Con a=1 y b=0 obtenemos ba=True, bb=False. Podríamos habernos saltado esta conversión —Python trata cualquier entero no nulo como verdadero por truthiness—, pero hacerla explícita evita ambigüedades y deja claro que operamos sobre verdad, no sobre cantidad. Luego ba and bb da False, ba or bb da True, y not ba da False. El detalle de presentación vive en tf = lambda x: "true" if x else "false": sin este ayudante, Python imprimiría False con mayúscula y rompería el contrato. La f-string final produce el texto exacto and=false or=true not_a=false.

JavaScript sigue la misma coreografía con otra sintaxis: const a = ai !== 0 construye el booleano, y luego a && b, a || b, !a aplican las tres operaciones. Aquí && y || son los operadores lógicos con cortocircuito —muy distintos de & y |, que operarían bit a bit sobre los números—. El mismo ayudante tf = (x) => (x ? "true" : "false") normaliza la salida, aunque en JavaScript sea menos necesario porque su true/false ya van en minúscula por defecto.

El contraste iluminador es C#, porque expone por qué la normalización no es opcional. Si escribiéramos Console.WriteLine(a && b) directamente, C# imprimiría False con F mayúscula (el resultado de bool.ToString()), y el verificador lo rechazaría frente al esperado false. Por eso existe string Tf(bool x) => x ? "true" : "false": fuerza la grafía en minúscula que el contrato exige. Python comparte exactamente este problema (True/False), mientras que Java, Go, Rust, PHP y JavaScript ya usan minúsculas y solo emplean tf por consistencia.

El caso más didáctico es C, que enseña el origen histórico de todo esto. C no tuvo tipo booleano hasta C99; aquí int a, b; guarda enteros, y a = a != 0; reduce cualquier valor a 0 o 1 aprovechando que en C el resultado de una comparación es un int. Sus operadores &&, ||, ! devuelven también int (0 o 1), no un bool. El ayudante static const char *tf(int x) recibe ese entero y devuelve la cadena adecuada. Es la implementación más "descarnada": muestra que, por debajo, un booleano siempre fue un número disfrazado.

Por su parte SQL no tiene tipo booleano nativo en SQLite, así que traduce cada operación a un CASE WHEN … THEN 'true' ELSE 'false' END sobre la tabla de casos declarada con VALUES (1,0), (1,1), (0,0). Es la misma tabla de verdad, expresada de forma declarativa fila por fila; el verificador la marca como ilustrativa porque no lee de stdin.

🔬 Comparación

La lógica es universal —las tres tablas de verdad son idénticas en todo lenguaje—, pero cada uno decide tres cosas por su cuenta: cómo se escriben los operadores, cómo se imprime la verdad y si existe siquiera un tipo booleano de primera clase. Ahí es donde surgen las divergencias que la tabla resume.

Clase de diferencia Observación entre lenguajes
Sintáctica Palabras vs. símbolos: and/or/not (Python) frente a &&/\|\|/! (C, Java, JS, Go, Rust, PHP). El significado es el mismo; solo cambia la grafía del operador.
Semántica Grafía de la verdad: C# y Python imprimen True/False con mayúscula por defecto; JS, Java, Go, Rust y PHP usan minúscula. C carece de tipo bool hasta C99 y opera con int (0/1). Todos deben normalizarse a true/false con el ayudante tf.
Paradigmática SQL (SQLite) no tiene booleano nativo: expresa cada operación con CASE WHEN a<>0 AND b<>0 THEN 'true' ELSE 'false' END sobre una tabla declarada con VALUES.

Merece una fila propia una distinción que causa errores reales: operadores lógicos vs. de bits. &&/|| son lógicos y cortocircuitan; &/| son bitwise y evalúan siempre ambos lados operando bit a bit. Sobre valores 0/1 pueden dar el mismo resultado numérico y ocultar el error, pero su semántica difiere: con & no hay cortocircuito, así que un p != null & p.valor > 0 sí intentaría leer p.valor aunque p sea nulo. La familia entera comparte esta trampa.

🧬 El concepto en la familia

En Ruby, a && b, con true/false en minúscula por defecto (y solo nil y false cuentan como falsos). En Haskell son &&, ||, not, con el tipo Bool explícito y valores True/False, sin ninguna veracidad implícita: una condición debe ser un Bool. Swift también exige un Bool estricto en sus if. El patrón general: cuanto más estático y tipado es el lenguaje, menos tolera usar un número donde se espera una verdad, y más pura resulta el álgebra de Boole que subyace a todo.

✅ Prueba común

Los mismos casos para todas las implementaciones: casos.json. Verifica la equivalencia:

python scripts/verificar_equivalencia.py 046

🧪 Reto de transferencia

Detalle en reto.md.

⚠️ Errores comunes

❓ Preguntas frecuentes

🔗 Referencias

Libros de la parte:

Libros de los lenguajes del núcleo:


⏮️ Clase 045 · 📂 Parte · 📚 Índice · 🌐 Atlas · Clase 047 ⏭️