07 — Colecciones

← 06 — Módulos y paquetes · README · Siguiente: 08 — Reactividad y concurrencia →

list, set y map

Los literales de colección usan un prefijo de keyword seguido de {}. Sin prefijo, {} es error de compilación.

# list — ordenada, permite duplicados
items: list of number = list { 1, 2, 3 }
items = list {}    # vacía

# set — sin orden, sin duplicados
tags: set of text = set { "niell", "language", "compiler" }
tags = set {}      # vacía
tags contains "niell"    # true
tags add "new-tag"      # si ya existe, no hace nada — silencioso
tags remove "niell"      # si no existe, no hace nada — silencioso

# map — pares clave → valor
scores: map of text to number = map { "Alice" -> 95, "Bob" -> 87 }
scores = map {}                  # vacío
scores["Alice"] = 95             # asignación por clave
player_score = scores["Alice"]   # acceso por clave — tipo: number or nothing

Acceso indexado en colecciones

list y map soportan acceso indexado con []. Las semánticas son intencionalmente distintas, según el modelo conceptual de cada estructura.

Map — `map[k]` retorna `V or nothing`. Buscar una clave que no está es estado esperado (los maps son sparse, key-driven). El compilador exige tratar el resultado como opcional antes de usarlo:

score = scores["Alice"]
if score is nothing then return
# de acá en adelante, score es number — type narrowing
total = total + score

`map[k]` colapsa la unión cuando `V` ya es opcional. Si el value type del map incluye or nothing (map of text to (number or nothing)), m[k] produce (number or nothing) or nothing que colapsa a number or nothing. Eso significa que nothing no distingue entre "la clave no está" y "la clave está y su valor es nothing". Para distinguir los dos casos, usar m contains k — devuelve boolean sin pasar por la unión:

notes: map of text to (text or nothing)
notes["alice"] = nothing            # "alice" presente, sin nota
notes["bob"]                        # nothing — pero ¿bob no está o sí está sin nota?
present = notes contains "bob"      # false — bob no está
present = notes contains "alice"    # true — alice está aunque su valor sea nothing

List — `list[i]` retorna `T`, aborta si `i` está fuera de rango. Los índices de lista son típicamente computados o conocidos en rango (fields[1] después de chequear count of fields >= 5). Devolver T or nothing obligaría a guardar cada acceso con is nothing, incluso cuando el programador acaba de verificar el bound — verboso y ruidoso.

Out-of-bounds aborta el contexto aislado actual, igual que un 7 / 0 o sqrt of -1. Es decir: tratado como bug, no como fallo esperado. El programador puede prevenir el abort con un count of items previo:

if i < count of items then item = items[i]   # item: T, sin or nothing

Para el caso minoritario donde la presencia es legítimamente incierta y se quiere T or nothing, una stdlib List.at with items, i -> T or nothing cubre el patrón sin contaminar el operador base.

Asignación. Tanto list[i] = v como map[k] = v son statements de mutación. Para map, asigna o crea la entrada. Para list, asigna solo si 0 ≤ i < length; out-of-bounds aborta. Para agregar al final de una lista, se usa add (no list[length]).

Operaciones sobre colecciones

doubled = for each n in numbers -> n * 2
even    = for each n in numbers where n % 2 = 0 -> n
total   = sum of numbers

Filtrado in-place — retain

retain es un statement que filtra una colección modificándola en sitio. Conserva los elementos que satisfacen la condición, en el orden original, y descarta el resto. La forma reusa la sintaxis <var> in <colección> where <cond> que ya aparece en for each:

retain n in numbers where n % 2 = 0
retain user in users where user.age >= 18
retain x in candidates where x = p or x % p != 0

El contrato es: después del statement, la colección contiene exactamente los elementos donde la condición evalúa a `true`, en el orden en que aparecían. El nombre retain se lee como "retener X donde se cumpla la condición" — la condición es de retención, no de descarte.

Diferencia con `for each ... where ... -> x`. Las dos formas filtran, pero el shape es distinto:

FormaTipoAlocaCaso
result = for each n in xs where p -> nexpresiónsí — nueva listconservar xs original
retain n in xs where pstatementno — muta xshot path o ya no interesa la original

La elección la fija la intención del programador, no el lenguaje: si el código de aguas abajo necesita ambas (la original y la filtrada), la forma expresión es la única opción. Si la colección se va a consumir filtrada y la versión sin filtrar no vuelve a usarse, retain evita la alocación.

`for each ... where ... -> x` también puede usarse para "reasignar":

xs = for each n in xs where n % 2 = 0 -> n     # equivalente a retain, pero aloca

Es válido — produce una list nueva y rebindea xs. La diferencia con retain es operacional, no semántica: ambas formas dejan xs con los mismos elementos. La que conserva la asignación in-place (retain) evita la alocación intermedia.

Statement, no expresión. retain no produce valor — no aparece del lado derecho de un = ni dentro de una expresión. Para "filtrar y obtener el resultado", se usa la forma expresión de for each.

Aplica a `list` y `set`. En map, el binding sería ambiguo (key, value, o entry); cuando el patrón aparezca el spec definirá la forma — probable retain entry in m where ... paralelando la iteración. Hasta que aparezca, filtrar un map se hace con for each entry in m where ... -> ... produciendo entradas y reconstruyendo.

Cross-module. retain cuenta como mutación del campo que apunta a la colección, igual que add/remove/list[i] = v. Solo el módulo dueño puede ejecutar retain sobre una colección expuesta — para que un módulo externo filtre, el dueño expone una acción que internamente hace el retain (ver "Mutación cross-module" más arriba).

Funciones matemáticas

Viven en el módulo Math, que forma parte del prelude — auto-importado en todo módulo, sin necesidad de use. Patrón: operación of valor. (El of acá es el operador de aplicación de función matemática, no el de anotación de tipo que aparece en declaraciones como list of number — son dos roles del mismo keyword desambiguados por contexto, ver "Algunas keywords tienen más de un rol" en el archivo 01.)

floor of 3.7        # 3
ceil of 3.2         # 4
abs of -5           # 5
sqrt of 16          # 4
sin of angle
cos of angle

Agregaciones sobre colecciones (sum, min, max, average, count) también viven en Math y siguen el mismo patrón:

total   = sum of numbers
biggest = max of numbers
n       = count of numbers

La regla de parseo de of es la misma que la de with (greedy hasta el primer delimitador). Ver "Precedencia de operadores" en el archivo 01.

Protocolo de colecciones (add, remove, contains)

add, remove y contains son operadores infijos del lenguaje, no acciones llamables. Por eso no se usan con with:

numbers add 42          # correcto — sintaxis infija
numbers add with 42     # error — no son acciones

Semántica de retorno:

Mutación cross-module

add, remove, list[i] = v y map[k] = v son operaciones que mutan el valor referenciado, y cuentan como mutación del campo que apunta a esa colección a los efectos de la visibilidad cross-module. Cuando el valor llega vía acceso a un campo de otro módulo, aplica la misma regla que para = sobre el campo: solo el módulo dueño puede mutar. El compilador rechaza motor.sprites.items add new_sprite desde un módulo externo aunque items esté expose has — la exposición habilita lectura y observabilidad, no mutación. Para que un módulo externo pueda modificar la colección, el módulo dueño expone una acción (add_sprite with sprites_layer, new_sprite) que internamente hace el add. Ver "Reglas de visibilidad" en el archivo 06 y en el anexo 12.

numbers add 42                   # statement
numbers remove 42                # statement
has_it = numbers contains 42     # expresión — has_it: boolean
if tags contains "niell" then ... # expresión en condición

list, set y map los soportan por defecto. Un type de usuario los adopta declarando does add, does remove o does contains — el compilador verifica que exista la implementación correspondiente. Como son keywords, los identificadores add, remove y contains no pueden usarse como nombres de acciones ni de variables.

numbers add 42
numbers remove 42
numbers contains 42     # boolean

sprites.items add new_sprite
sprites.items remove old_sprite

Type de usuario con soporte de colección — delegación de los hooks de protocolo a un campo colección interno:

Inventory
  has items: list of text
  does add to items
  does remove from items
  does contains in items
end

inv: Inventory
  items = list {}
end

inv add "compass"          # delega: inv.items add "compass"
inv contains "compass"     # delega: inv.items contains "compass"
inv remove "compass"

El compilador genera el código de delegación automáticamente: inv add X se traduce a inv.items add X. El campo destino debe ser una colección built-in (list, set o map) y su tipo de elemento debe coincidir con lo que se intenta agregar — si no, error de compilación.

Las preposiciones to, from e in no aportan semántica distinta entre sí; existen solo para que la declaración se lea en inglés natural (add to, remove from, contains in). El compilador acepta cualquier combinación a propósito — la convención de estilo es usar la preposición que mejor lee con la operación (add to, remove from, contains in), pero el chequeo es laxo para no convertir un detalle estilístico en error de compilación. Una combinación como does contains to items parsea, aunque suene raro.

`does add/remove/contains` es API pública del type, cross-module incluido. Declarar does add to items es decisión deliberada del módulo dueño de exponer la operación, independiente de la visibilidad del campo destino. Cross-module, inv add "compass" compila aunque items no esté expose has — el campo destino sigue privado a efectos de acceso directo (inv.items add "compass" desde afuera se rechaza, igual que cualquier otra mutación cross-module) pero la operación delegada vía does se admite porque ese es exactamente su propósito. Si el módulo no quiere exponer la mutación, no declara does add — usa una acción nombrada (add_item) cuya visibilidad gobierna como cualquier otra acción.

Cuándo no alcanza la delegación. Si el type necesita lógica extra al agregar — notificar listeners, validar, actualizar campos relacionados — la delegación no aplica. En ese caso se define una acción explícita con otro nombre:

action add_item
  needs inventory: Inventory, item: text
  returns nothing or fails with text
  if inventory.items contains item then fails with "duplicado"
  inventory.items add item
  notify_change
end

Se pierde el azúcar de la sintaxis infija a cambio de claridad: queda explícito que hay lógica extra detrás del agregar.

El compilador rechaza X add Y si X no es una colección built-in y no declara does add to ....