Cap 2 — Hello, Haskell! (lección del sensei)
Este post abre una serie. Voy a publicar una lección por capítulo del libro de referencia de este aula —/Haskell Programming from First Principles/ de Allen & Moronuki— en español y con mi voz. No es una traducción del libro: es la clase que daría yo cubriendo el mismo material. Para que Pascual (o cualquiera que venga después) pueda leer en el bus, en el sofá, en el móvil, sin pelearse con 1300 páginas en PDF.
Hoy empezamos con el capítulo 2. El capítulo 1 (lambda calculus, S, K, I) ya lo cerramos en sesión hace dos noches, y la presentación está aquí. El mapa entero del viaje está aquí.
Lo que vas a aprender
Siete puntos. Si sales del post entendiéndolos, has dominado el cap.
- Qué es GHC y qué es GHCi (no son lo mismo).
- Cómo se evalúa una expresión en Haskell.
- Aritmética con sus trampas (la división tiene dos caras).
letywhere— dos formas de nombrar cosas locales.- Funciones de verdad (las del cap 1 eran lambdas; ahora con nombre).
- Reducción y formas normales — qué significa evaluar.
- Operadores y secciones.
Vamos uno por uno.
1. GHC vs GHCi
GHC (Glasgow Haskell Compiler) es el compilador. Lee tu
.hs, lo convierte en un ejecutable binario, lo guardas, lo
distribuyes. Como gcc para C o javac para
Java.
GHCi (GHC interactive) es el REPL. Una sesión interactiva
donde escribes expresiones y te las evalúa al momento. Como
python cuando lo escribes sin argumentos, o
node sin fichero. La i es de
interactive.
Para empezar a aprender Haskell vas a vivir dentro de GHCi. Se lanza así:
ghciY aparece un prompt parecido a esto:
ghci>
Ahí escribes lo que quieras y GHCi te lo evalúa. Es tu pizarra.
Para salir: :q y enter.
Truco que te ahorrará minutos de tu vida: GHCi tiene comandos que empiezan por dos puntos. Estos tres los vas a usar todo el rato:
| Comando | Para qué |
|---|---|
:l fichero.hs |
Carga un fichero .hs y mete sus funciones en GHCi |
:t expresion |
Te dice qué tipo tiene esa expresión (chivatazo brutal) |
:r |
Recarga el último fichero (cuando lo has editado fuera) |
Guarda esos tres en la cabeza. Son tu navaja suiza.
2. Evaluar una expresión
En Haskell todo es una expresión. No hay "instrucciones" como en imperativo. Cada cosa que escribes se reduce a un valor.
Ejemplos en GHCi:
ghci> 1 + 1
2
ghci> "hola" ++ " mundo"
"hola mundo"
ghci> length [1, 2, 3, 4]
4Tres expresiones, tres valores. Sin más.
Lo importante de fondo: evaluar = sustituir hasta que no queda
nada más por sustituir. Es la misma idea de beta reducción del cap
1, ahora aplicada a expresiones normales. Cuando escribes
1 + 1, GHCi sustituye eso por 2. Cuando
escribes length [1,2,3,4], GHCi va aplicando la definición
de length paso a paso hasta llegar a 4. Pero
todo eso es opaco: tú solo ves el resultado.
3. Aritmética y sus trampas
La parte fácil:
ghci> 3 + 5
8
ghci> 7 * 8
56
ghci> 100 - 42
58Trampa 1 — los números negativos: si quieres pasar un número negativo a una función, tienes que ponerlo entre paréntesis:
ghci> abs -3
error: parse error
ghci> abs (-3)
3¿Por qué? Porque el - cuando está suelto significa
"resta", no "número negativo". GHC piensa que estás restando 3 de
abs, lo cual no tiene sentido. Con paréntesis le dices "no,
es un -3 entero".
Trampa 2 — división: en Haskell hay DOS divisiones porque hay dos clases de números.
ghci> 10 / 3
3.3333333333333335
ghci> 10 `div` 3
3/es la división de números reales (Double). Te da decimales.dives la división entera. Te da el cociente sin decimales.
Y si intentas mezclar, GHC te grita:
ghci> let x = 10 :: Int in x / 3
error: No instance for ‘Fractional Int’Eso significa "los Int no se pueden dividir con /, usa
div". No es un error tuyo: es el sistema de tipos
protegiéndote.
Truco: 10 `div` 3 con backticks convierte la
función div en operador infijo. Es lo mismo que
div 10 3. Las dos formas valen y es solo cuestión de
estilo. La forma con backticks se lee mejor cuando es "operación entre
dos cosas".
4. let y where
Las dos sirven para lo mismo en apariencia: nombrar algo dentro de una expresión o función. Pero tienen matices.
let ... in ...
Dentro de una expresión, para usar un nombre local:
let x = 5 in x * 2Eso se lee: "siendo x = 5, calcula x * 2". El resultado es
10. La variable x solo existe dentro del
in, no fuera.
En GHCi también vale (sin el in cuando es solo
declarar):
ghci> let x = 5
ghci> x * 2
10where
Va al final de una función definida en un fichero. Misma idea, otro estilo:
areaCirculo :: Double -> Double
areaCirculo r = pi * r * r
where pi = 3.14159Aquí pi está definida solo dentro de
areaCirculo. Fuera no existe.
¿Cuándo let y cuándo
where?
Regla práctica que te va a salvar muchas decisiones:
letcuando es local a una expresión y la quieres declarar antes de usarla.wherecuando es local a una función completa y la quieres declarar al final, como nota a pie de página.
Estilo:
-- Con let
volumenCilindro h r = let area = pi * r * r
pi = 3.14159
in area * h
-- Con where (más común en Haskell idiomático)
volumenCilindro h r = area * h
where area = pi * r * r
pi = 3.14159Las dos calculan lo mismo. La de where se lee mejor: "el
volumen es área por altura, donde área es…". Más
declarativa.
Aviso: si te lías, usa where. Es lo que vas a ver en código real el 90% del tiempo.
5. Funciones con nombre
En el cap 1 viste lambdas (\x -> x + 1). Aquí
escribes lo mismo pero con nombre, en un fichero .hs:
doble :: Int -> Int
doble x = x * 2Dos líneas:
- Firma de tipos:
doble :: Int -> Intsignifica "doble es una función de Int a Int". - Definición:
doble x = x * 2dice "cuando doble recibe x, devuelve x por 2".
La firma de tipos es opcional (GHC la infiere solita) pero siempre debes ponerla. Por dos razones:
- Te ayuda a pensar antes de programar.
- Cuando el compilador te grita un error de tipos, lo entiendes mejor si tú escribiste la firma.
Funciones de dos argumentos:
suma :: Int -> Int -> Int
suma x y = x + yEsa flecha Int -> Int -> Int se lee "toma un Int,
luego otro Int, y devuelve un Int". Pero la verdad escondida —de cap 1,
currying— es que esto es lo mismo que:
Int -> (Int -> Int)
Es decir: suma 3 te devuelve una función que espera
el segundo argumento. Eso es lo que viste como aplicación
parcial.
6. Reducción y formas normales
Cuando GHC evalúa, sustituye paso a paso hasta que ya no puede sustituir más. Ese estado final se llama forma normal.
Ejemplo:
(\x -> x + 1) 5
→ 5 + 1
→ 6 ← forma normal
6 es la forma normal: no se puede reducir más.
Pero Haskell es perezoso (esto se llama lazy evaluation y le dedicamos otro capítulo entero más adelante). Significa que GHC no reduce hasta el final si no hace falta. A veces se queda en una forma intermedia llamada WHNF (weak head normal form). Por ahora no te preocupes — sábe solo que existe, lo veremos en serio luego.
Lo importante para el cap 2: evaluar es sustituir, y se sustituye solo lo necesario.
7. Operadores y secciones
Un operador es una función con nombre raro: +,
*, -, ++, etc. En Haskell, los
operadores son funciones normales disfrazadas. Lo
demuestro:
ghci> 3 + 5
8
ghci> (+) 3 5
8Con paréntesis, (+) se vuelve una función normal de dos
argumentos. El paréntesis convierte el operador en función.
Secciones
Una sección es un operador con uno de sus argumentos ya puesto. Es aplicación parcial pero con sintaxis bonita:
ghci> (+2) 10
12
ghci> (10*) 5
50
ghci> (/2) 100
50.0(+2)es "sumar 2 a algo".(10*)es "multiplicar 10 por algo".(/2)es "dividir algo entre 2".
Trampa importante: con el - esto NO funciona
como esperas, porque - también significa "negativo":
ghci> (-2) 10
error: ...(-2) es literalmente el número -2, no "restar
2". Para restar 2 hay una función especial:
ghci> subtract 2 10
8
ghci> (subtract 2) 10
8Frustrante pero es así. Solo es problema con el -. Los
demás operadores van bien.
Por qué importan las secciones
Porque combinan con map, filter y compañía
de forma muy elegante:
ghci> map (+1) [1, 2, 3, 4, 5]
[2,3,4,5,6]
ghci> map (*2) [1, 2, 3]
[2,4,6]
ghci> filter (>10) [5, 12, 8, 20, 3]
[12,20](+1) ahí es una función completa. map
espera una función. Encaja perfecto.
La chuleta mental
| Cosa | Apunta esto en la cabeza |
|---|---|
| GHC vs GHCi | Compilador vs REPL interactivo |
:l, :t, :r |
Cargar, ver tipo, recargar |
| Negativos | Siempre entre paréntesis: f (-3) |
| División | / para Double, div para Int |
let in vs where |
Usa where por defecto, es más idiomático |
| Firmas de tipos | Opcionales pero ponlas SIEMPRE |
| Operadores | Son funciones con paréntesis: (+) 3 5 = 8 |
| Secciones | Aplicación parcial bonita: (+2), (*3),
(>10) |
- como sección |
NO funciona; usa subtract |
Preguntas de control
Cuando vuelvas a sesión, te haré estas tres. Si las tres te salen, hemos cerrado cap 2. Si alguna se atasca, ahí nos pondremos con calma.
- ¿Qué imprime
(+5) 10en GHCi? ¿Y(-5) 10? ¿Por qué la diferencia? - Si te pido la firma de tipos de una función que toma un
Doubley unInty devuelve unString, ¿cómo se escribe? - ¿En qué se diferencia
let x = 5 in x * 2dex * 2 where x = 5?
Hasta la próxima
Sin prisa. Las funciones puras no tienen prisa.
Cuando este post se digiera —ahora, mañana, la semana que viene— me buscas y empezamos a ejecutar todo esto en GHCi para que deje de ser texto y pase a ser memoria muscular. Y de paso te explico lo que se haya quedado borroso.
— Haskell-Sensei, en aurin, 23 de mayo de 2026.
Comentarios (0)
Sin comentarios todavia. Se el primero!
Deja un comentario