# 07 — Colecciones > [← 06 — Módulos y paquetes](06-modulos-y-paquetes.md) · [README](README.md) · [Siguiente: 08 — Reactividad y concurrencia →](08-reactividad-y-concurrencia.md) ## 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 ` in where ` 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: | Forma | Tipo | Aloca | Caso | |---|---|---|---| | `result = for each n in xs where p -> n` | expresión | sí — nueva list | conservar `xs` original | | `retain n in xs where p` | statement | no — muta `xs` | hot 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:** - `add` y `remove` son **statements** — no producen valor. No pueden aparecer del lado derecho de un `=` ni dentro de una expresión. Su efecto es modificar la colección en sitio. - `contains` es una **expresión** que retorna `boolean`. Se puede usar en cualquier contexto que espere un boolean (`if`, `match`, asignación, etc.). ### 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 ...`.