10. Colecciones

Junto a las funciones, recorrer una lista de valores forma parte del flujo natural cuando se trabaja con la programación funcional. La gran mayoría de los problemas se pueden resolver iterando un conjunto de elementos con una intención: filtrar, modificar, calcular, etc. Por ello debes conocer las diferentes listas, o colecciones, para desenvolverte con comodidad.

Son las siguientes:

  • Vector: lista donde los nuevos elementos son añadidos al final.

  • List: lista donde los nuevos elementos son añadidos al principio. No puede usarse (get), tan solo (nth). Recomendado para Macros.

  • Map: colección Hashed, cada valor está acompañado por una llave (key).

  • Set: colección Hashed, donde no se permite la repetición.

No olvidemos el principio de la inmutabilidad. Cuando hago referencia a "añadir" a una lista, quiero expresar que crearé una nueva lista con un elemento extra a la original donde se situará al principio o el final. En otras palabras: una lista nueva con un elemento nuevo. Todo son constantes, no podemos modificarla.

Vector

Crear

Tienes dos posibilidades, usando la sintaxis de corchetes.

[1 2 3]

O usando la función vector.

(vector 1 2 3)

Leer

Para obtener un valor necesitarás usar get junto a la posición.

Partamos del siguiente vector.

["abc" false 99]

Si quiero obtener el primer elemento, o el string "abc".

(get ["abc" false 99] 0)
;;; "abc"

En cambio, si quiero quedarme con el valor de la tercera posición, o 99.

(get ["abc" false 99] 2)
;; 99

Puedes dar un valor por defecto si proporcionas una posición que se sale de rango o no existe.

(get ["abc" false 99] 6 "Vikingo")
;; Vikingo

Un atajo popularmente utilizado es usar -> o función de encadenamiento. Lo estudiaremos más adelante en el tema Threading Macros.

(-> 0 ["abc" false 99])
;; abc

Añadir

Usaremos conj, una función que te permite crear una nueva lista con nuevos elementos.

(conj [1 2 3] 4)
;; [1 2 3 4]

(conj [1 2 3] 4 5 6)
;; [1 2 3 4 5 6]

Al ser un Vector siempre se colocarán al final. En caso de que quieras unir 2 o más Vectores, usaremos into.

(into [1 2 3 4 5 6] ["a" "b"])
; [1 2 3 4 5 6 "a" "b"]

Si quieres añadir elementos delante no te quedará otra que transformarlo en una List. La función cons hace la conversión además de actuar como conj.

(cons 4 [1 2 3])
; (4 1 2 3)

Cuidado porque devolverá una colección de tipo List. Habrá que convertirlo a Vector con vec.

(vec (cons 4 [1 2 3]))
; [4 1 2 3]

Quitar

Para eliminar un valor es bastante natural usando filter.

De la colección ["a" "b" "c" "b"] quiero eliminar cualquier "b". De nuevo habría que convertirlo en Vector ya que devuelve un List.

(vec (filter (fn [value] (not= "b" value)) ["a" "b" "c" "b"]))
;; ["a" "c"]

Otra opción es usar subvec para cortar de un punto a otro. Por ejemplo, podríamos obtener un nuevo vector de la posición 1 en adelante.

(subvec ["a" "b" "c" "d"] 1)
;; ["b" "c" "d"]

O de la posición 1 a la 3.

(subvec ["a" "b" "c" "d"] 1 3)
;; ["b" "c"]

List

Crear

Puedes usar la sintaxis de paréntesis iniciando con una comilla simple.

'(1 2 3)

O la función list.

(list 1 2 3)

Leer

En lugar de get, disponemos de nth que es equivalente.

(nth '("PC" "MAC") 1)
;; "MAC"

Añadir o quitar

No hay diferencia. Podemos usar las mismas herramientas que los Vectores.

(conj '(1 2 3) 4)
;; (4 1 2 3)

(conj '(1 2 3) 4 5 6)
;; (6 5 4 1 2 3)
(filter (fn [value] (not= "b" value)) ["a" "b" "c" "b"])
;; ("a" "c")

Set

Crear

Como en los casos anteriores, puedes apoyarte en la sintaxis: llaves con una # al inicio.

#{"Alice" "Fred" "Bob" "Kelly"}
;; #{"Alice" "Fred" "Bob" "Kelly"}

O la función hash-set.

(hash-set "Alice" "Fred" "Bob" "Kelly")
;; #{"Alice" "Fred" "Bob" "Kelly"}

Es interesante utilizar la función ya que si incluyes un elemento duplicado, lo eliminará de forma automática. Mientras que si usas la sintaxis devolverá un error.

Quitar

Para eliminar disponemos de disj.

Puede usarse con un solo elemento.

(disj #{"Alice" "Fred" "Bob" "Kelly"} "Bob")
;; #{"Alice" "Kelly" "Fred"}

O varios, aunque alguno de ellos no exista.

(disj #{"Alice" "Fred" "Bob" "Kelly"} "Bob" "Batman" "Fred")
;; #{"Alice" "Kelly"}

Algunas ayudas

Comprobar si existe

Una característica interesante es que podemos preguntar si existe algún elemento con contains?.

(contains? #{"Alice" "Fred" "Bob" "Kelly"} "Bob")
;; true
(contains? #{"Alice" "Fred" "Bob" "Kelly"} "Truman")
;; false
Ordenar

Utilizando sorted-set y conj podemos ordenar un contenido de una manera rápida.

(conj (sorted-set) "Bravo" "Charlie" "Sigma" "Alpha")
;; #{"Alpha" "Bravo" "Charlie" "Sigma"}

Maps

Crear

Si usamos la sintaxis, nos recordará a un JSON con algunas diferencias.

Envolviendo en llaves, las posiciones impares son las claves y las pares los valores.

{"Fred" 1400 "Bob" 1240 "Angela" 1024}
;; {"Fred" 1400, "Bob" 1240, "Angela" 1024}

Si te resulta más cómodo puedes usar comas, aunque es opcional.

{"Fred" 1400, "Bob" 1240, "Angela" 1024}
;; {"Fred" 1400, "Bob" 1240, "Angela" 1024}

O saltos de línea.

{
  "Fred" 1400
  "Bob" 1240
  "Angela" 1024
}
;; {"Fred" 1400, "Bob" 1240, "Angela" 1024}

Por otro lado pueden ser útiles para representar datos estructurados, o cuando necesitas reutilizar un formato de mapa.

Las claves se acompañan de : al inicio. Este tipo de claves se denominan keywords y son el estilo idiomático en Clojure.

(def person
  {:first-name "James"
   :last-name "Bond"
   :age 32
   :occupation "Agente secreto"})

Leer

Para obtener un valor usamos get con la clave, o directamente la keyword como función.

(get person :occupation)
;; "Agente secreto"

(:occupation person)
;; "Agente secreto"

Podemos dar un valor por defecto en caso de no existir la clave.

(:favorite-color person "beige")
;; "beige"

Añadir

(assoc {"Fred" 1400, "Bob" 1240, "Angela" 1024} "Sally" 0)
;; {"Fred" 1400, "Bob" 1240, "Angela" 1024, "Sally" 0}

Modificar

(assoc {"Fred" 1400, "Bob" 1240, "Angela" 1024} "Fred" 100)
;; {"Fred" 100, "Bob" 1240, "Angela" 1024}

Borrar

(dissoc {"Fred" 1400, "Bob" 1240, "Angela" 1024} "Bob")
;; {"Fred" 1400, "Angela" 1024}

Ayudas comunes

Conocer la longitud

(count ["a" "b" "c"])
;; 3

Obtener primer elemento

(first ["a" "b" "c"])
;; "a"

Obtener último elemento

(last ["a" "b" "c"])
;; "c"

Varios niveles (Anidación)

Crear

(def company
  {:name "WidgetCo"
   :address {:street "123 Main St"
             :city "Springfield"
             :state "IL"}})

Leer

(get-in company [:address :city])
;; "Springfield"

Actualizar

(assoc-in company [:address :street] "303 Broadway")

Records

Para el caso anterior tiene un mejor desempeño usar Records en lugar de Maps.

Definir.

(defrecord Person [first-name last-name age occupation])

Generar.

(def kelly (->Person "Kelly" "Keen" 32 "Programmer"))

Generar indicando las claves.

(def kelly (map->Person
             {:first-name "Kelly"
              :last-name "Keen"
              :age 32
              :occupation "Programmer"}))

Resumen

  • Vector ([]): lista ordenada, añade al final. Usa get para leer.

  • List (’()): lista ordenada, añade al principio. Usa nth para leer.

  • Set (#{}): colección sin duplicados. Usa contains? para comprobar.

  • Map ({}): pares clave-valor. Usa get o la keyword como función.

  • conj añade elementos, assoc en Maps.

  • filter y disj para eliminar elementos.

  • get-in y assoc-in para trabajar con estructuras anidadas.

  • defrecord para estructuras con mejor rendimiento.

Ejercicios

  1. Crea un vector con los nombres de 5 personajes del Quijote. Obtén el tercero.

  2. Crea un Map con las claves :nombre, :edad y :profesion. Añade una nueva clave :ciudad con el valor "La Mancha".

  3. Dado el Set #{"espada" "lanza" "escudo" "yelmo"}, comprueba si existe "lanza" y elimina "escudo".

  4. Crea una estructura anidada que represente un caballero con su nombre y su caballo (nombre y raza). Lee el nombre del caballo.

Este trabajo está bajo una licencia Attribution-NonCommercial-NoDerivatives 4.0 International.

¿Me invitas a un café?

Visitantes en tiempo real

Estás solo: 🐱