# 05 — Control de flujo > [← 04 — Acciones y errores](04-acciones-y-errores.md) · [README](README.md) · [Siguiente: 06 — Módulos y paquetes →](06-modulos-y-paquetes.md) ## Enums Un type que solo puede ser uno de sus valores posibles. Cada uno de esos valores se llama **variante**. El compilador rechaza cualquier otro. **Forma simple — variantes sin datos.** En una sola línea, separadas por coma: ``` Direction can be north, south, east, west Role can be admin, user, guest heading: Direction = north heading = "arriba" # error — valor inválido ``` **Forma con datos por variante.** Algunas variantes pueden llevar campos asociados — el patrón clásico de "error con contexto" o "resultado con valor". Se usa el bloque multilínea cerrado con `end`; cada variante en su línea, y `has` después del nombre introduce sus campos (uno o varios separados por coma): ``` LoginError can be invalid_credentials account_locked rate_limited has retry_after: number end HttpResponse can be ok has body: text redirect has url: text not_found server_error has code: number, message: text end ProcessOutcome can be ok has enriched: EnrichedTransaction malformed_row unknown_sku invalid_amount end ``` Las variantes con y sin datos pueden convivir en el mismo enum. **Construcción.** Una variante sin datos se usa por nombre directamente. Una con datos se construye con `with` y sus campos nombrados, igual que se invocaría a una acción: ``` err: LoginError = invalid_credentials # variante sin datos err: LoginError = rate_limited with retry_after = 30 # variante con datos return ok with enriched = my_transaction # idem dentro de una acción ``` Cada variante actúa como un constructor auto-generado: `invalid_credentials` es de cero parámetros, `rate_limited` toma sus campos como parámetros nombrados. El compilador la reconoce por contexto (el tipo esperado le dice de qué enum es). **Defaults y opcionales en variantes con datos.** Los campos `has` dentro de una variante siguen las mismas reglas que los campos de un struct (ver "Creación de instancias" en el archivo 03). Pueden ser obligatorios, tener default explícito (`= expr`), ser opcionales (`or nothing`), o combinar opcionalidad con default (`or nothing = expr`): ``` LoginError can be invalid_credentials account_locked rate_limited has retry_after: number = 60, reason: text or nothing end err: LoginError = rate_limited # retry_after = 60, reason = nothing err: LoginError = rate_limited with retry_after = 5 # reason = nothing err: LoginError = rate_limited with reason = "abuse" # retry_after = 60 ``` **Variantes homónimas en uniones de enums.** Cuando una variable tiene tipo `EnumA or EnumB` y ambos enums declaran una variante con el mismo nombre, la construcción simple es ambigua y el compilador la rechaza. El programador desambigua con la calificación `EnumName.variant`, la misma forma que ya usan los handlers de fails-with con dos enums (ver "Múltiples tipos de error" en el archivo 04): ``` LoginError can be timeout, invalid_credentials HttpError can be timeout, network_error err: LoginError or HttpError = timeout # error — ambigüedad err: LoginError or HttpError = LoginError.timeout # OK err: LoginError or HttpError = HttpError.timeout # OK ``` La regla evita "una construcción funciona hasta que otro módulo agrega una variante con nombre colisionante, y de repente el call-site compila distinto". La calificación explícita inmuniza el sitio. **Binding en `match`.** La forma de bindear los datos depende de cuántos campos tiene la variante — la misma regla que ya usan los handlers de fails-with con `on success as user`: - **Cero campos**: sin binding (`as` no se usa). - **Un campo**: `as nombre` binda directamente el valor del campo. - **N campos**: `as nombre` binda un struct con todos los campos; se accede con `.field`. ``` match outcome ok as enriched -> apply_success with report, enriched # 1 campo: directo malformed_row -> apply_failure with report, "malformed_row" # 0 campos: sin as unknown_sku -> apply_failure with report, "unknown_sku" invalid_amount -> apply_failure with report, "invalid_amount" end match response ok as body -> show with body # 1 campo: directo redirect as url -> redirect_to with url not_found -> show with "404" server_error as r -> show with "Error {r.code}: {r.message}" # N campos: struct end ``` La exhaustividad se mantiene: el compilador rechaza un `match` que no cubra todas las variantes salvo que haya `else`. Las variantes con datos también funcionan en `fails with` — los handlers usan exactamente la misma forma (ver "Errores" en el archivo 04). ## if Una línea — un solo statement. `if` como expresión retorna un valor: ``` if player.lives = 0 then game_over if player.lives = 0 then game_over else continue_game tier = if score > 1000 then "oro" else "plata" ``` Multilínea como statement (ramas son bloques) — cierra con `end`: ``` if player.lives = 0 game_over reset_level end if player.lives = 0 game_over else continue_game end ``` Multilínea como expresión (ramas son expresiones simples, una por rama) — sin `end`. `then` y `else` arrancan línea: ``` quote = if item.in_stock then fixed with amount = item.price else on_request with reason = "stock variable" ``` La regla: `end` sólo aparece cuando hay un bloque de statements que cerrar. Una expresión por rama, sin bloque, termina con la última rama. Coherente con la forma de una línea (`if cond then expr else expr`), que ya no lleva `end`. Para múltiples condiciones, preferir `match` en lugar de `if/else` encadenado — ver "Sin sujeto" más abajo para el patrón canónico. ## Match Tres formas según el contexto. **Sobre enum — exhaustividad garantizada por el compilador:** Si no están todos los casos cubiertos y no hay `else`, es error de compilación. `else -> nothing` para ignorar casos explícitamente. ``` match player.direction north -> player.y = player.y - 1 south -> player.y = player.y + 1 east -> player.x = player.x + 1 west -> player.x = player.x - 1 end ``` **Con sujeto — rangos y valores, primer caso que se cumple:** ``` match score > 1000 -> "oro" > 500 -> "plata" else -> "bronce" end tier = match score > 1000 -> "oro" > 500 -> "plata" else -> "bronce" end ``` **Sin sujeto — condiciones independientes (equivalente a `cond` de Lisp):** Evita el anidamiento de `if/else` cuando las condiciones involucran variables distintas. ``` match score > 1000 -> award_trophy player.lives = 0 -> game_over time_remaining < 10 -> play_warning_sound else -> nothing end ``` Las tres formas usan la misma sintaxis `->` y el mismo `else`. **Las tres pueden usarse también como expresión que retorna un valor** — incluyendo la forma sin sujeto: ``` tier = match score > 1000 -> "oro" score > 500 -> "plata" else -> "bronce" end ``` **Tipo de las ramas: todas iguales.** Cuando `if` o `match` se usan como expresión, todas las ramas deben producir el mismo tipo concreto. El compilador rechaza ramas que retornan tipos distintos, incluso si son compatibles "por unión" (`number` y `text`, por ejemplo). Las uniones `T or U` existen solo como anotación de variable o en `fails with` — no se construyen ad-hoc desde `if` o `match`. Si el resultado naturalmente puede tomar formas distintas, el patrón canónico es un **tagged union explícito**: ``` PriceQuote can be fixed has amount: number on_request has reason: text end quote = if item.in_stock then fixed with amount = item.price else on_request with reason = "stock variable" # quote: PriceQuote — el llamador hace match para discriminar ``` Esto preserva la disciplina del lenguaje (cada tipo nombra explícitamente sus formas) y le da al consumidor un punto canónico para ramificar con `match` exhaustivo. ## Range Secuencias numéricas para iteración. **El rango incluye ambos extremos cuando el step los alcanza.** No existe forma "exclusiva" tipo `until` — una sola sintaxis cubre todos los casos. ``` 1 to 10 # 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ``` Sin `by` (paso implícito de 1), ambos extremos siempre se alcanzan. **Paso** — keyword `by`: ``` 0 to 100 by 10 # 0, 10, 20, ... 100 — el 100 se alcanza 1 to 10 by 2 # 1, 3, 5, 7, 9 — el 10 NO se alcanza, último valor 9 ``` Con `by N`, el extremo final se incluye solo si `(to - from)` es múltiplo exacto de `N` en valor absoluto. Si no, la secuencia se detiene en el último valor que cabe sin pasar el extremo. La regla garantiza que el step nunca produzca un valor fuera de `[from, to]`. **Descendente** — el compilador infiere la dirección por el orden de los extremos: ``` 10 to 1 # 10, 9, 8, ... 1 10 to 1 by 2 # 10, 8, 6, 4, 2 — el 1 NO se alcanza, último valor 2 ``` Si el programador necesita iterar hasta `length - 1` de una colección, escribe `0 to length - 1`. La forma explícita hace visible qué valor se alcanza efectivamente, sin que el lector tenga que recordar si el extremo es inclusivo o exclusivo. **Validación de `by`.** El step debe cumplir tres reglas, chequeadas estáticamente cuando los operandos son literales y en runtime cuando son dinámicos: 1. **No puede ser cero.** `1 to 10 by 0` es loop infinito sin progreso. Si el literal es `0`, el compilador rechaza la expresión. Si el step viene de una variable y resulta `0` en runtime, aborta el contexto. 2. **El signo debe coincidir con la dirección inferida por los extremos.** `1 to 10 by -1` (ascendente con step negativo) o `10 to 1 by 2` (descendente con step positivo y ambos extremos en orden descendente — el `by` solo aporta magnitud, no dirección, así que `by 2` se interpreta como `by -2` automáticamente). El compilador rechaza la combinación contradictoria cuando los signos son visibles en literales; runtime abort cuando vienen de variables. Para descender con step explícito, el `by` toma el valor absoluto y el orden de los extremos define la dirección — no hay forma de escribir un step "negativo" que sirva. 3. **El shape (int/float) del step debe coincidir con el de los extremos.** `1 to 10 by 2.5` (extremos int, step float) es error de compilación. Para iterar con step fraccional, convertir extremos: `1.0 to 10.0 by 2.5`. Esta regla siempre se chequea en compile-time porque el shape forma parte del type. ``` 1 to 10 by 0 # error en compilación — step cero 1 to 10 by -1 # error en compilación — signo contradice dirección 1 to 10 by 2.5 # error en compilación — shape inconsistente (int vs float) 1.0 to 10.0 by 2.5 # ok — todos float 10 to 1 by 2 # ok — step toma magnitud, dirección inferida ``` ``` for each n in 1 to 10 ... end ``` ## for each Iteración sobre cualquier expresión que produzca una colección o un range. Cubre los dos modos del lenguaje — como statement (efecto, sin valor) y como expresión (transformación a una colección nueva). **Forma statement — bloque con efecto, sin valor.** Cierra con `end`: ``` for each sprite in sprites.items sprite.angle = camera.facing sprite.shadow = true end for each i in 0 to count of items - 1 show with items[i] end ``` **Forma expresión — produce una `list of T` resultado de aplicar el cuerpo a cada elemento.** La flecha `->` introduce la expresión por iteración: ``` doubled = for each n in numbers -> n * 2 names = for each user in users -> user.name ``` El resultado es siempre una `list`, en el orden de iteración. Para producir un `set` o `map` se compone con conversiones de stdlib (`set of ...`). **Filtro `where` — antes de la transformación.** Acepta una expresión booleana. Los elementos que no la satisfacen no entran al cuerpo: ``` evens = for each n in numbers where n % 2 = 0 -> n adults = for each user in users where user.age >= 18 -> user.name ``` **Posición sintáctica fija.** El orden es siempre `for each in [where ] [in parallel] (-> | end)`. `where` antes de `in parallel`; `->` o `end` cierran. **Totalidad del cuerpo.** La expresión del cuerpo debe ser total — no puede declarar `fails with` ni invocar acciones con `fails with` sin manejar el error en el sitio. Aplica a las dos formas (statement con bloque que contiene `fails with`, y expresión con `->`). El motivo es composicional: la lista resultante tiene que existir entera o no existir; un fallo a mitad de iteración deja la colección en estado inconsistente. Si el cuerpo legítimamente puede fallar, el patrón canónico es envolverlo en un tagged union — ver "Paralelismo de datos" en el archivo 08 para el caso `for each in parallel`, donde la regla aparece primero. **Iteración sobre `map`.** El binding recibe **entries** con campos `.key` y `.value`: ``` for each entry in scores show with "{entry.key}: {entry.value}" end biggest = for each entry in scores where entry.value > 90 -> entry.key ``` ## while Loop controlado por una condición booleana evaluada al inicio de cada iteración. Cubre el caso donde el número de vueltas no se conoce desde el call-site — no hay una colección a recorrer ni un range definido de antemano. ``` while count of candidates > 0 then p = candidates[0] primes add p candidates remove p end ``` La sintaxis exige `then` como separador (consistente con `if cond then ...`) y `end` para cerrar el bloque. La condición se evalúa antes de cada iteración; si es `false` al entrar, el cuerpo no corre ninguna vez. **Cuándo usar `while` vs `for each`.** Si la iteración es naturalmente "una vez por elemento" de una colección o range, `for each` lee mejor y deja el conteo al lenguaje. `while` aparece cuando el criterio de salida es **dinámico** — depende de algo que cambia dentro del cuerpo, o de I/O externa, o de la propia colección que se está consumiendo: ``` # preferir for each cuando hay colección o range for each user in users notify with user end # while cuando la condición no es "una pasada sobre algo" while not socket.closed then msg = socket.next_message handle with msg end ``` **No hay `do ... while`** (loop con condición al final). La equivalencia se logra con un `while true ... break` si la primera iteración debe correr incondicionalmente — pero la versión con condición al inicio cubre el 99% de los casos sin sintaxis extra. **Totalidad del cuerpo.** Como con `for each`, una acción `fails with` invocada dentro del cuerpo debe manejar su fallo en el sitio. El motivo es el mismo: si la iteración aborta a la mitad, el estado del módulo queda parcialmente actualizado. Para fallos legítimos hay que envolver con un tagged union. ## break y continue Dos statements de escape dentro de loops (`while`, `for each` sobre colección, `for each` sobre range): - `break` — sale del loop más interno. La ejecución continúa después del `end`. - `continue` — salta a la siguiente iteración del loop más interno (re-evalúa la condición en `while`, avanza el iterador en `for each`). ``` # break: salir cuando se cumple un criterio dinámico for each n in 1 to 1000 if n * n > target then break end squares add n * n end # continue: filtrar dentro del loop sin condicional anidado for each user in users if user.banned then continue end notify with user end ``` Ambos statements **solo afectan al loop más interno** — no hay forma de break o continue a un loop exterior por nombre. Si el patrón aparece (poco común), conviene refactorizar a una acción separada con `return` temprano. Usar `break` y `continue` con moderación: cuando aparecen, suelen ser síntoma de que el loop tiene dos responsabilidades. La versión con filtro explícito (`for each ... where ...`) o con una acción que encapsule el criterio suele leer mejor. La conveniencia existe porque hay casos donde la alternativa es peor — pero el camino canónico para "iterar y descartar" es `where`, no `continue`. ## String templates Mecanismo oficial para embeber valores de cualquier tipo dentro de texto. No es una coerción — el compilador convierte el valor internamente en el punto de interpolación. ``` age = 25 greeting = "Hello {name}, you are {age} years old" ``` **Sin comillas anidadas.** Una expresión interpolada dentro de `{...}` no puede contener literales de string. Si necesitas un string dentro de la interpolación, lo extraes a una variable y la interpolas: ``` # error — comillas anidadas no permitidas show with "{score} — {if won then "ganaste" else "perdiste"}" # correcto — extraer a variable con nombre semántico status = if won then "ganaste" else "perdiste" show with "{score} — {status}" ``` Esto mantiene el lexer simple (cada `"` cierra el template) y empuja al programador a nombrar las expresiones complejas, en la misma línea de disciplina que el resto del lenguaje (retornos múltiples → type explícito, comportamiento parametrizable → `does`, etc.). El nombre extra suele ser más legible que la expresión inline. Las llamadas a acciones, accesos a campos, operaciones aritméticas y booleanas sí están permitidos dentro de `{...}` — la restricción es exclusivamente literales de string. Para escribir llaves literales sin interpolación, se duplican — `{{` produce `{` y `}}` produce `}`: ``` "La variable {{age}} contiene {age}" # → La variable {age} contiene 25 "Un objeto JSON vacío: {{}}" # → Un objeto JSON vacío: {} ``` Para strings sin interpolación en absoluto, `raw` desactiva el mecanismo de interpolación para ese string: ``` raw "Usa {llaves} libremente sin interpolación" ``` `raw` es un **modificador léxico** del literal de string que le sigue inmediatamente — no un operador sobre expresiones. Su efecto se limita a *cómo se lee* ese literal. Por eso no aparece en la tabla de precedencia: ``` raw "texto" # literal string sin interpolación raw "a" + "b" # → (raw "a") + "b" — solo el primer literal es raw raw ("a" + "b") # error — raw no se aplica a expresiones, solo a literales ``` El valor resultante es un `text` normal, indistinguible en tipo de cualquier otro string: la diferencia es enteramente cómo se construyó. `text + text` es válido (concatenación). `text + number` es error de compilación — el template es la forma designada para mezclar tipos en texto. ## Scope Una variable vive en el bloque donde se declara. El bloque cierra con `end` (la indentación sigue las reglas generales del archivo 01). ``` x = 10 # nivel módulo — visible en todo el módulo action calculate needs n: number y = n * 2 # solo existe dentro de calculate return y end # y no existe aquí — el compilador lo rechaza ``` **Estado a nivel módulo — inmutabilidad por defecto** A nivel módulo, una variable se declara una sola vez. El compilador rechaza reasignaciones posteriores: si una acción intenta `x = 20` sobre la `x` del módulo, es error de compilación. ``` PI = 3.14159 # constante a nivel módulo action area_of_circle with r: number -> number return PI * r * r end action reset_pi PI = 0 # error de compilación — PI no es reasignable end ``` Para tener estado mutable a nivel módulo, se declara una **instancia de un type**. La variable que apunta a la instancia no se puede reasignar, pero sus campos sí pueden mutar (sujeto a la disciplina de `has` / `derives from` / `expose` del type): ``` camera: Camera position = point with 0, 0 tilt = 0 facing = 0 end action move_camera with at: Point camera.position = at # válido — muta un campo camera = otra_camera # error — camera no es reasignable end ``` Esto empuja al programador a poner el estado dentro de types — donde la disciplina del lenguaje aplica naturalmente — en lugar de tener "variables sueltas" a nivel módulo que serían estado global desordenado. Es también lo que hace coherente la promesa de core single-threaded: no hay variables sueltas mutables que dos partes del programa puedan pisarse. **Variables locales — rebindables. Parámetros — inmutables.** Dentro de una acción, una variable local declarada con `=` puede reasignarse libremente. Es el caso típico del acumulador en un loop: ``` action sum_list with items: list of number -> number total = 0 for each n in items total = total + n # válido — reasignación de variable local end return total end ``` Los parámetros, en cambio, son inmutables: actúan como una "constante de entrada" durante toda la acción. Reasignarlos es error de compilación. ``` action discount_price with price: number, percent: number -> number price = price - price * percent / 100 # error — los parámetros no se reasignan return price end # correcto — usar una variable local action discount_price with price: number, percent: number -> number final = price - price * percent / 100 return final end ``` No existe una keyword tipo `const` para marcar una local como inmutable — una "constante local" es simplemente una variable que el programador no reasigna. **Instancias locales — la variable no se reasigna, los campos sí.** La regla de las instancias a nivel módulo (variable no reasignable, campos mutables) aplica idéntico dentro de una acción. Si una variable local se declara con la **forma de instancia** — `nombre: Tipo ... end`, con bloque de campos — apunta a esa instancia para siempre dentro de su scope; lo que muta son sus campos, no a quién apunta. La distinción es la forma sintáctica, no la presencia de `:`. Una declaración con tipo explícito e inicializador (`nombre: Tipo = expr`) es una **variable local con anotación de tipo** y sigue la regla de variables locales — rebindable libremente. Es lo que pasa en `items: list of number = list { 1, 2, 3 }` seguido de `items = list {}`: el binding sigue las reglas de variable, no las de instancia. ``` items: list of number = list { 1, 2, 3 } # variable rebindable items = list {} # válido — rebind sprite: Sprite # instancia ligada (forma de bloque) position = at angle = 0 end sprite = otro_sprite # error — sprite no es reasignable sprite.angle = 45 # válido — muta un campo ``` ``` action add_sprite with at: Point -> Sprite new_sprite: Sprite position = at angle = 0 ... end new_sprite.angle = 45 # válido — muta un campo new_sprite = otro_sprite # error — la instancia local no es reasignable return new_sprite end ``` La distinción es estructural: `=` introduce una variable local rebindable; `: Tipo` introduce una instancia ligada. No hace falta una keyword nueva — la forma sintáctica de la declaración decide la disciplina.