# 04 — Acciones y errores > [← 03 — Declaraciones e instancias](03-declaraciones-instancias.md) · [README](README.md) · [Siguiente: 05 — Control de flujo →](05-control-flujo.md) ## Acciones Unidad de comportamiento. Reemplaza a las funciones. Soporta recursión de forma natural. **Dos formas sintácticas — no intercambiables.** La forma de una línea usa `with` y `->`. La multilínea usa `needs` y `returns`. No se mezclan: si eliges multilínea, no se admite `with`/`->` en la firma, y viceversa. La keyword te dice de un vistazo qué formato esperar. ``` # firma en una línea — privilegia la concisión action factorial with n: number -> number if n = 0 then return 1 return n * factorial with n - 1 end # firma multilínea — privilegia la declaratividad action login needs email: text, password: text returns User or fails with LoginError ... end ``` Criterio práctico: usá la forma de una línea cuando la firma cabe cómoda en una línea (uno o dos parámetros, sin tipos de error). Usá multilínea cuando hay varios parámetros, cuando declarás errores con `fails with`, o cuando la firma se hace difícil de leer apretada. **Returns implícito.** Si una acción no declara cláusula de retorno (ni `-> X` en la forma de una línea ni `returns X` en multilínea), retorna `nothing` implícitamente. Es el caso por defecto para acciones cuyo propósito es producir un efecto sin valor de salida — handlers, mutadores, comandos. Si la acción puede fallar pero no devuelve valor útil, la forma explícita es `returns nothing or fails with ...`. ``` action notify_change # equivalente a -> nothing for each listener in listeners invalidate with listener end end action deposit needs account: Account, amount: number # sin returns — retorna nothing account.balance = account.balance + amount end ``` **Las acciones siempre se invocan con `nombre with arg`, nunca como métodos.** Niell no tiene métodos asociados a types: la sintaxis `instancia.accion` no existe como invocación, y el `.` se usa exclusivamente para acceso a miembros (`account.balance`, `Math.pi`, `Authentication.login`). Para operar sobre una instancia, se pasa como argumento (`deposit with account, 100`), no como receptor (`account.deposit with 100` es error). La decisión elimina la dualidad método/función, mantiene una sola forma de llamada, y hace que el lookup de qué acción se invoca dependa solo del nombre y del módulo en scope — nunca del tipo del primer argumento. Sobrecarga — la misma acción con distintas firmas. El compilador resuelve en tres pasos: 1. **Por cantidad de parámetros.** Solo entran al match las firmas con el número exacto de parámetros que recibió la llamada. 2. **Por tipos.** Entre las que quedan, se descartan las que no aceptan los tipos pasados — sea por coincidencia concreta o por instanciación de un parámetro genérico. 3. **Por especificidad** (solo entra cuando hay genéricos en juego). Un parámetro con tipo concreto es más específico que uno con tipo genérico. Una sobrecarga es más específica que otra si tiene al menos un parámetro más específico y ninguno menos específico. La más específica gana. Si después de los tres pasos quedan dos o más sobrecargas igualmente válidas, es error de compilación. **Identidad de la firma — sólo aridad y tipos.** Dos firmas con la misma tripla (nombre de acción, aridad, tipos de parámetro en orden) son la misma firma a efectos de sobrecarga. Declarar dos firmas que sólo difieren en los nombres de parámetro es error de compilación por firma duplicada. Los nombres son etiquetas para el call-site; no participan de la resolución. Si dos acciones representan operaciones distintas con la misma shape (`find with email` vs `find with username`), llevan nombres distintos (`find_by_email`, `find_by_username`). La sobrecarga por tipo de retorno no existe en Niell: una llamada debe ser comprensible leyendo solo esa línea, sin depender del tipo del receptor. Si dos acciones retornan tipos distintos, llevan nombres distintos. ``` action greet with name: text -> text return "Hello {name}" end action greet with name: text, language: text -> text if language = "es" then return "Hola {name}" return "Hello {name}" end greet with "Daniel" # usa la primera — un parámetro greet with "Daniel", "es" # usa la segunda — dos parámetros ``` La regla de especificidad habilita tener una versión genérica como default y especializar concretamente cuando la versión concreta aporta algo no expresable en la genérica — una representación más compacta, una ruta optimizada para un tipo numérico, una rama que evita cómputo redundante: ``` action serialize of T with value: T -> text return Reflect.encode with value # ruta genérica: introspección estructural end action serialize with value: number -> text # versión concreta, especializada para number return value as text # ruta directa: cast del prelude, sin reflexión end serialize with 5 # concreta (más específica) serialize with "hi" # genérica (T=text — no hay concreta para text) ``` El contraste hace tangible la motivación: la genérica funciona para todo type, la concreta evita el costo de reflexión cuando el tipo se conoce estáticamente. Si dos sobrecargas son igualmente específicas para una llamada — por ejemplo, una con `T` en la primera posición y la otra con `T` en la segunda, ambas matcheando — el compilador rechaza por ambigüedad y el programador renombra una de las dos. **Paso de argumentos por nombre — `nombre = valor`.** Los argumentos pueden pasarse por posición o por nombre. La forma por nombre usa la misma sintaxis que el bloque de instanciación: `nombre = valor`. Útil cuando la llamada tiene varios parámetros y la posición sola no explica qué es qué. ``` move_camera with my_point, 30, 90 # todos posicionales move_camera with at = my_point, tilt = 30, facing = 90 # todos nombrados move_camera with my_point, tilt = 30, facing = 90 # mixto: posicional + nombrados ``` **Regla precisa:** los argumentos son posicionales hasta el primer `nombre = expr`. A partir de ahí, todos los siguientes deben ser nombrados. No se puede volver a posicional una vez empezada la zona nombrada. ``` move_camera with at = my_point, 30 # error — posicional después de nombrado ``` Los argumentos nombrados también permiten pasar parámetros en cualquier orden (dentro de la zona nombrada) y saltar opcionales con default (cuando el parámetro lo tenga). El paso por nombre nunca cambia qué sobrecarga elige el compilador — la resolución sigue siendo por cantidad y tipo de parámetros. Esta forma reutiliza el mismo patrón "nombre = valor" del bloque de instanciación de types — una sola idea para asignar a un campo o parámetro nombrado, no una sintaxis nueva. Retorno múltiple — si una acción necesita retornar varios valores, se define un type explícito: ``` Outcome has score: number has won: boolean end action play_round -> Outcome ... end result = play_round status = if result.won then "ganaste" else "perdiste" show with "{result.score} — {status}" ``` **Record vs tagged union.** Cuando los campos del retorno siempre tienen sentido juntos (un puntaje y un flag de "ganó/perdió" del mismo round), el patrón es un record como `Outcome` arriba. Cuando el retorno representa "uno de varios casos, posiblemente con datos distintos por caso" (éxito con valor / error con motivo / cancelado sin razón), el patrón es un **tagged union** — variantes con datos asociados, ver "Enums" en el archivo 05. La diferencia es semántica: record agrupa campos que coexisten; tagged union discrimina alternativas que se excluyen. Acciones que pueden no encontrar resultado: ``` action find_user with id: number -> User or nothing ... end find_user with id on success as user -> show with user.name on nothing -> show with "no encontrado" end ``` (Lista completa de handlers disponibles en el call-site — `on success`, `on nothing`, `on failure`, variantes de enum, formas con `as` binding — en la tabla "Formas de `on`" más abajo.) Cuando la lógica asociada a cada caso es la misma acción con distinto contexto, el patrón natural es encapsularla en una acción que recibe el enum como parámetro: ``` action move_player with player: Player, direction: Direction match 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 end ``` **Las acciones no son valores.** Un nombre de acción en una expresión siempre significa "invocar": `factorial with 5` la invoca con argumento, `game_over` (cero argumentos) también la invoca. No existe forma de tomar una referencia a una acción sin ejecutarla — ni para pasarla, guardarla en variables, ni almacenarla en colecciones. **Excepción acotada — hooks fijos del compilador.** Los hooks enumerados en el archivo 08 (`spawn`, `send`, `ask` del módulo `Parallel`) aceptan el nombre de una acción en posición de argumento (`spawn with counter_actor, initial = 0`). Ahí el nombre se resuelve estáticamente como entry point: el compilador lo trata como referencia inmediata a la acción, no como valor pasable. La regla general se mantiene; la excepción está acotada a esa lista cerrada del compilador, no es algo que una librería pueda extender. **Nota de lectura:** sintácticamente `counter_actor` en esa posición se ve idéntico a una llamada de cero argumentos. Lo desambigua la firma del hook (`spawn` espera nombre de acción, no valor) — el lector que llega frío debe inspeccionar la firma del hook para resolver la lectura local. Es el único lugar de Niell donde la ambigüedad sintáctica se resuelve por contexto del callee, y queda acotada a esos tres nombres. Esta decisión simplifica el modelo: no hay closures, no hay funciones como valores, no hay dispatch dinámico por puntero a función. Las acciones sí acceden a nombres de su scope léxico — constantes e instancias del módulo, sus propios parámetros, sus propias variables locales — pero eso es resolución estática del compilador, no captura de entorno en un valor pasable. Los patrones que en otros lenguajes requieren callbacks se cubren con mecanismos más expresivos: - **Estrategias** → enums + `match` (como `move_player` arriba: el comportamiento se elige por el valor del enum, no pasando una función). - **Observadores / handlers** → `on X changes` y `when`. El listener vive donde corresponde, no se "registra" pasándolo a otra acción. - **Iteración con transformación** → `for each ... -> ...` y `where`. La expresión va inline. - **Polimorfismo de comportamiento por type** → `does` con los cuatro hooks built-in del compilador (`add`, `remove`, `contains` — ver archivo 07; `observable` — ver archivo 08). Cubre que un type extienda la sintaxis del núcleo en esos puntos; protocolos user-defined es decisión futura (ver archivo 11). **Acciones genéricas — parámetros de tipo con `of`.** Una acción que opera sobre cualquier tipo declara sus parámetros de tipo con `of T` después del nombre. El compilador infiere `T` en cada llamada según el tipo de los argumentos. No se introduce sintaxis nueva: se reutiliza la keyword `of` que ya describe la forma de las colecciones (`list of number`). ``` action first_of of T with items: list of T -> T or nothing ... end action index_of of T with items: list of T, value: T -> number or nothing ... end ``` El sufijo `_of` en el nombre de la acción es una **convención de stdlib para acciones genéricas**, no una regla del lenguaje. Su rol es puramente legible: `first_of of T with items` se lee como "primer elemento de T, con items", y mantiene visualmente separado el `of` del parámetro de tipo (que es parte del nombre extendido `first_of of`) del `of` que invoca funciones del módulo `Math` (`floor of x`, `sqrt of n`). El programador puede usar cualquier nombre válido — `head`, `take_first`, etc. — la convención sólo recomienda el sufijo cuando ayuda a leer. Múltiples parámetros de tipo se separan con coma: ``` action combine of K, V with keys: list of K, values: list of V -> map of K to V ... end ``` La declaración explícita de los parámetros con `of T` es obligatoria. Si el compilador encuentra un nombre de tipo en mayúscula que no está definido y no aparece en la cláusula `of`, lo trata como typo y emite error — nunca como un parámetro de tipo implícito. Esto elimina la clase de bugs donde un identificador mal escrito (`Useer` por `User`) pasa silenciosamente como tipo genérico. ## Types genéricos La misma sintaxis aplica a los types: parámetros de tipo declarados con `of T` después del nombre, separados por coma si hay más de uno. La regla de declaración explícita es la misma — un nombre en mayúscula que no aparezca en la cláusula `of` se trata como typo. ``` Stack of T has items: list of T does add to items does remove from items end Cache of K, V has entries: map of K to V has max_size: number end # uso — el parámetro se instancia al declarar nums: Stack of number items = list {} end sessions: Cache of text, User entries = map {} max_size = 1000 end ``` Esto cierra la simetría con las acciones: `of T` declara parámetros de tipo en cualquier definición. `list of T`, `map of K to V` y `set of T` no son magia del compilador — son los mismos types genéricos que el programador puede definir, expresados con la misma sintaxis. ## Casting `as` solo es válido para conversiones **garantizadas y no informativas** — el "tipo nuevo" no agrega ni quita información del valor, solo lo presenta en otra forma. La lista es cerrada y conocida por el compilador: | Origen | Destino | Resultado | | --------- | ------- | -------------------- | | `number` | `text` | `"42"`, `"3.14"` | | `boolean` | `text` | `"true"`, `"false"` | | `nothing` | `text` | `"nothing"` | Cualquier otra forma `X as Y` es error de compilación. En particular: - **`text as number` / `text as boolean`** — no son conversiones, son **análisis sintáctico** (parsing) que puede fallar. Viven en acciones del módulo estándar (`parse_number`, `parse_boolean`) que el compilador exige manejar con `on success` / `on failure`. - **`Vector3 as Point`** y demás casts entre types user-defined — Niell no es estructuralmente típico; dos types con la misma forma son tipos distintos. - **Conversiones entre representaciones internas de `number`** (int64 ↔ float64) — no se exponen como `as`; el compilador las promueve automáticamente cuando hace falta (ver "Formas de literal numérico" en el archivo 02). ``` # garantizadas — as siempre funciona label = 42 as text # → "42" flag = true as text # → "true" mensaje = nothing as text # → "nothing" # puede fallar — acción explícita, el compilador exige manejar el error # (asume `use Numbers` en el módulo; parse_number no está en el prelude) parse_number with "25" on success as n -> ... on failure -> show with "valor inválido" end ``` El programador no puede ignorar el caso de fallo por accidente — el compilador lo rechaza igual que cualquier `fails with`. ## Errores Los errores son resultados posibles, no excepciones que interrumpen el flujo. El compilador exige que el llamador declare qué ocurre en cada caso — no es posible ignorar un error por accidente. > *Nota:* los ejemplos a partir de acá usan `show with "..."` para imprimir a consola. `show` es una acción del módulo `Console`, que forma parte del prelude (ver "Módulos estándar → Prelude" en el archivo 10) y por eso está disponible sin `use` explícito. Caso simple — el error es texto: ``` action login needs email: text, password: text returns User or fails with text end login with email, password on success -> redirect with "home" on failure -> show with "Algo salió mal" end ``` Caso estructurado — el error es un enum, el compilador verifica exhaustividad: ``` LoginError can be invalid_credentials, account_locked, expired_session action login needs email: text, password: text returns User or fails with LoginError end login with email, password on success -> redirect with "home" on invalid_credentials -> show with "Email o contraseña incorrectos" on account_locked -> show with "Cuenta bloqueada" on expired_session -> redirect with "login" end ``` El compilador resuelve cada `on` por lookup dirigido: sabe qué enum puede retornar la acción a partir de su firma. Cuando la firma menciona un solo enum (`fails with LoginError`), no requiere calificación explícita — `on invalid_credentials` basta. Si el valor no existe en el tipo declarado, es error de compilación. **Múltiples tipos de error — una acción puede declarar más de uno con `or`.** Cuando la firma combina **dos o más enums**, el compilador exige calificar todos los handlers de caso con el nombre del enum (`on EnumName.variant`). Esto evita un acoplamiento frágil entre módulos: agregar una variante nueva a un enum nunca rompe call-sites del otro. ``` AuthError can be invalid_credentials, account_locked DbError can be not_found, constraint_violation action register needs email: text, password: text returns User or fails with AuthError or DbError end register with email, password on success -> redirect with "home" on AuthError.invalid_credentials -> show with "Credenciales inválidas" on AuthError.account_locked -> show with "Cuenta bloqueada" on DbError.not_found -> show with "Base de datos no disponible" on DbError.constraint_violation -> show with "El usuario ya existe" end ``` Si más adelante `DbError` agrega una variante que coincide en nombre con alguna de `AuthError` (por ejemplo, `account_locked` o `timeout`), los call-sites siguen compilando sin cambios — la calificación los aísla. La regla evita el problema clásico de "un cambio aparentemente aditivo en un módulo rompe consumidores en módulos no relacionados". Cuando la firma tiene un solo enum, la forma corta `on variant` sigue siendo válida (es la del ejemplo de `login` arriba): no hay otro enum con el que colisionar, así que un nombre de variante identifica unívocamente el caso. Formas de `on` disponibles en el call-site: - `on success` — la acción retornó un valor no-`nothing` sin error - `on success as x` — igual, con binding al valor retornado - `on nothing` — la acción retornó `nothing` (sólo cuando la firma lo declara con `or nothing`) - `on failure` — la acción falló con texto (cuando la firma usa `fails with text`) - `on failure as err` — igual, con binding al texto del error - `on ` — la acción falló con una variante sin datos de un enum declarado en la firma - `on as binding` — la acción falló con una variante **con datos**; el binding sigue la misma regla que en `match` (1 campo → valor directo; N campos → struct con `.field`) Cuando la firma incluye `or nothing`, `on success` y `on nothing` son **ramas disjuntas y exhaustivas** sobre el caso "no falló": `on success` cubre el valor presente, `on nothing` cubre la ausencia. Cuando la firma no menciona `nothing`, `on success` cubre el único caso de éxito posible y `on nothing` no es admisible. **Handlers con binding (`as`)** — son el patrón más útil: capturan el valor retornado o el error y lo exponen como variable local dentro del handler. Permiten encadenar la lógica sin un paso extra de asignación: ``` action login needs email: text, password: text returns User or fails with text end login with email, password on success as user -> show with "Bienvenido {user.name}" on failure as err -> show with "Error: {err}" end ``` La variable de binding (`user`, `err`) solo existe dentro del cuerpo del handler. Fuera, el compilador no la reconoce. **Variantes con datos en el handler.** Cuando una variante de enum lleva campos asociados (ver "Enums" en el archivo 05), el handler puede bindearlos con la misma forma `as`. El binding obtiene el valor directo si la variante tiene 1 campo, o un struct con todos los campos si tiene N: ``` LoginError can be invalid_credentials account_locked rate_limited has retry_after: number end action login needs email: text, password: text returns User or fails with LoginError end login with email, password on success as user -> redirect with "home" on invalid_credentials -> show with "credenciales inválidas" on account_locked -> show with "cuenta bloqueada" on rate_limited as retry_after -> show with "esperá {retry_after} segundos" end ``` ## fails with en el cuerpo de una acción Interrumpe la acción y entrega el control al bloque `on failure` (o `on `) del llamador. La acción no necesita retornar nada después de `fails with` — el control ya salió. ``` action withdraw needs account: Account, amount: number returns nothing or fails with text if amount <= 0 then fails with "monto inválido" if amount > account.balance then fails with "saldo insuficiente" account.balance = account.balance - amount end withdraw with my_account, 500 on success -> show with "retirado" on failure as err -> show with "no se pudo retirar: {err}" end ``` `fails with` solo es válido si la firma de la acción declara `or fails with ...`. El compilador rechaza un `fails with` en una acción cuya firma no lo permite.