03 — Declaraciones e instancias

← 02 — Tipos · README · Siguiente: 04 — Acciones y errores →

Formas de declaración

Niell tiene tres formas distintas de introducir un nombre nuevo. La forma elegida — no una keyword aparte — determina la disciplina: si el nombre es rebindable, si el tipo se infiere o se restringe, y si lo que se construye es una instancia ligada al nombre.

FormaSignificadoReasignable
nombre = exprVariable. Tipo inferido del RHS.
nombre: Tipo = exprVariable con tipo explícito (restricción).
nombre: Tipo + bloque ... endInstancia ligada al nombre (el nombre no puede reapuntar a otra instancia; sólo los campos mutan).no

Caso rechazado:

FormaPor qué falla
nombre: Tipo sin = ni bloqueError de compilación. Toda variable necesita valor inicial.

Las primeras dos formas son intercambiables salvo por el tipo como restricción explícita. La tercera es estructuralmente distinta: la presencia del bloque ... end después de nombre: Tipo significa "construir esta instancia y ligarla permanentemente al nombre". La variable apunta para siempre a esa instancia dentro de su scope; lo único que puede cambiar son sus campos.

n = 5                          # variable, inferida como number, rebindable
n: number = 5                  # variable, restringida a number, rebindable
result = play_round            # variable rebindable (play_round retorna un Outcome — ver archivo 04)
result: Outcome = play_round   # idem, con restricción explícita

camera: Camera
  position = point with 0, 0
  ...
end                            # instancia ligada — camera no es reasignable

n: number                      # error — falta valor inicial
camera: Camera                 # error — falta bloque ... end o = expr

La disciplina sigue a la forma sintáctica, no al tipo del valor. Una variable creada con = puede contener una instancia de un type — result = play_round produce un result: Outcome rebindable, donde result = otro_outcome es legal (mismo tipo). La ligadura permanente solo aparece cuando usas la forma con bloque, donde la intención de "construir y ligar" es explícita.

Esta es la base de dos reglas que aparecen más adelante: las instancias locales no son reasignables (solo sus campos mutan), y los campos opcionales que no se asignan en el bloque quedan en nothing (ver "Creación de instancias" más abajo).

Creación de instancias

nombre: Tipo declara e instancia un tipo. No existe separación entre declaración e inicialización — toda variable tiene un valor desde su creación.

El bloque que sigue asigna los campos. El compilador rechaza la creación si algún campo obligatorio queda sin asignar.

background: Background
  style = gaussian_splatter
  infinite = true
end

Cuatro categorías de campos:

Receipt
  has items_total: number
  has note: text or nothing
  has currency: text = "USD"
  tax derives from items_total * 0.21
end

new_receipt: Receipt
  items_total = 100               # obligatorio
  # note queda en nothing (no se asigna; default implícito)
  # currency toma "USD" por default
  # tax lo calcula el compilador
end

other_receipt: Receipt
  items_total = 50
  currency = "EUR"                # override del default
end

Cuándo se evalúa el default. La expresión del default se evalúa al momento de cada instanciación, no al momento de declarar el type. Esto importa cuando el default no es una constante:

Session
  has id: text
  has created_at: number = now    # cada Session creada toma el momento actual
end

Qué puede referenciar. El default es lógica de inicialización — se evalúa una sola vez por instanciación, así que admite efectos. La regla es más flexible que la de derives from (que es una relación continua y exige la whitelist sintáctica estricta del archivo 02):

Lo que sigue fuera: acceso a otras instancias por nombre, y mutación directa de estado en el RHS. Si el default necesita lógica suficientemente compleja como para no caber legiblemente en una expresión, conviene mover la creación entera a una acción constructora del módulo y dejar el bloque de instanciación libre.

El default debe ser total. Una acción usada como default no puede declarar or fails with E en su firma — el compilador rechaza el type. La razón es composicional: si el default pudiera fallar, todo bloque de instanciación heredaría un fallo implícito que el call-site tendría que manejar, contaminando con on failure cualquier nombre: Tipo … end. Cuando la inicialización legítimamente puede fallar (parsear un input, abrir un recurso), el patrón canónico es mover la lógica a una acción constructora del módulo (action create_session with raw -> Session or fails with SessionError) que el llamador invoca explícitamente y maneja el error donde corresponde. La restricción no aplica a or nothing: una acción que retorna T or nothing sí puede ser default si el campo declara también or nothing.

Orden de evaluación. Los defaults se evalúan en orden textual de declaración. Un default puede referenciar campos del mismo type ya declarados arriba en el bloque, pero no campos posteriores. Si el programador escribe los defaults en orden de dependencia, ambos requisitos coinciden; si no, el compilador rechaza por "campo X usado antes de declararse". Coherente con la lectura lineal del resto del lenguaje, y especialmente importante cuando los defaults tienen efectos: el código se lee en el mismo orden en que se ejecuta.

Session
  has token: text = generate_token         # OK — no depende de nada
  has token_hash: text = hash with token   # OK — token ya declarado arriba
end

Bad
  has token_hash: text = hash with token   # error — token aún no declarado
  has token: text = generate_token
end

A diferencia de derives from, que se resuelve topológicamente (es relación pura y no admite efectos), los defaults priorizan predictibilidad lineal por sobre permisividad.

El campo derivado se define con los campos propios del type (items_total), no con estado externo. Cuando un campo debería reaccionar a otra instancia (por ejemplo, un Sprite cuyo ángulo depende de la Camera), no se usa derives from — se usa on changes (ver "Relaciones reactivas entre instancias distintas" en el archivo 02).

Aplica en cualquier contexto: nivel módulo o dentro de una acción.

Dentro del bloque de instanciación, el lado izquierdo de cada asignación siempre es un campo del type. El lado derecho es una expresión del scope circundante — puede ser un parámetro, una variable local o cualquier valor accesible en ese punto. No hay ambigüedad aunque compartan nombre:

action open_account
  needs owner: text
  returns Account
  new_account: Account
    owner = owner    # izquierda: campo del type — derecha: parámetro de la acción
    balance = 0
  end
  return new_account
end

Para chequear si un campo opcional tiene valor, se usa is nothing:

if sprite.label is nothing then show with "sin etiqueta"

if sprite.label is nothing
  assign_default_label with sprite
else
  show with sprite.label
end

La forma negativa es not X is nothing — no existe un operador is not nothing. El paréntesis ayuda cuando la expresión se encadena con otros operandos lógicos:

if not (sprite.label is nothing) and sprite.visible then show with sprite.label

Type narrowing: cuando el compilador ve un guard if X is nothing then ... que interrumpe el flujo (con return, fails with o similar), entiende que el resto del bloque trata a X como el tipo concreto, no como T or nothing. No es necesario ningún cast explícito:

action describe_label
  needs sprite: Sprite
  returns text or fails with text

  if sprite.label is nothing then fails with "sin etiqueta"
  # a partir de aquí, sprite.label es text — el compilador lo sabe
  return "etiqueta: {sprite.label}"
end

Si el guard no interrumpe el flujo, el narrowing aplica solo dentro de la rama:

if sprite.label is nothing
  assign_default_label with sprite
else
  show with sprite.label    # aquí sprite.label es text
end

Type narrowing en `match`. El mismo análisis niell-sensitive aplica a match. Para una expresión match x donde x: T or nothing:

match maybe_user
  nothing -> show with "no encontrado"
  else    -> show with "Hola {maybe_user.name}"   # maybe_user: User aquí
end

Por qué `nothing` aparece como pattern y no como `is nothing`. En if/when, la condición es una expresión booleana — por eso se escribe if maybe_user is nothing (un test). En match, las ramas son patrones (valores o variantes), no expresiones booleanas. Por eso nothing aparece directamente como el pattern que matchea contra el valor. La asimetría sintáctica refleja la diferencia de mecanismo: condicional vs. pattern matching.

La regla generaliza a tagged unions: cubrir una variante específica en una rama narrowea las demás ramas al complemento (excluyen esa variante). Para T or nothing, nothing funciona como una variante implícita: cubrirla habilita el narrowing a T en las demás. La regla no requiere que la rama interrumpa el flujo — match ya tiene exhaustividad como disciplina propia.

Types recursivos y mutuamente recursivos

Un type puede referenciarse a sí mismo, o referenciar a otro que a su vez lo referencia. La regla de construcción ("todo campo obligatorio debe asignarse") fuerza una restricción: todo ciclo en el grafo de dependencias de campos obligatorios debe romperse por al menos un campo opcional o colección.

Una arista del grafo va de T a U cuando T tiene un campo has x: U sin or nothing y sin envolver en colección. Si un ciclo se compone exclusivamente de aristas de este tipo, el type no es construible — no existe forma de asignar todos los obligatorios — y el compilador lo rechaza al declararlo.

Romper el ciclo significa que al menos una arista sea opcional (or nothing) o pase por una colección (list of, set of, map of), porque en ambos casos el campo admite el valor base (nothing o colección vacía) que termina la cadena.

# Válidos — el ciclo está roto por or nothing o por una colección
Node has parent: Node or nothing,    has children: list of Node
Tree has root: Tree or nothing
A    has b: B or nothing,            B has a: A or nothing
A    has b: B,                       B has a: list of A      # roto por list of A
# Inválidos — el compilador los rechaza al declararlos
Node has next: Node                  # ciclo de un salto, todo obligatorio
A    has b: B,                       B has a: A              # ciclo de dos saltos

La regla aplica al grafo entero, no solo a ciclos directos. Tres types A → B → C → A con todos los campos obligatorios se rechazan igual que un ciclo de dos. Para construirlos, basta que una arista del ciclo se vuelva opcional o pase por colección — no hace falta romper todas.