Tipos de datos y estructuras de datos (Clojure)
José Javier Blanco Rivero
Esta Notebook está hecha para que puedas aprender y/o repasar algunos conceptos y pongas en práctica lo aprendido. Lo primero que debes hacer es hacer click en Remix para clonar la Notebook y comenzar a practicar. Es importante destacar que pueden agregar celdas en cualquier lugar de la Notebook, pueden incluso cambiar su contenido, tomar sus propias notas, etc.
Se sugiere mantener la versión original en otra pestaña, en caso que hayan realizado diferentes cambios y hayan perdido el contenido original.
¿Qué y para qué son los tipos de datos?
Un computador procesa información leyendo y moviendo datos codificados binariamente de una dirección de memoria a otra y/o entre la memoria, los registros y los diferentes niveles de caché.
Sin entrar en detalles demasiado técnicos, es importante destacar que el procesador necesita saber por anticipado la cantidad de bits (binary digit) que van a ocupar en memoria esos datos.
La idea básicamente consiste en codificar la información, tal y como esta se presenta en nuestro mundo social y que nos es inmediatamente comprensible, en una serie de bits que pueda procesar el computador. No es lo mismo trabajar con números enteros que trabajar con números racionales; tampoco es lo mismo trabajar con caracteres como los del alfabeto occidental que trabajar con el alfabeto cirílico -sin mencionar los ideogramas chinos. Por otra parte, a diferencia de los números, las palabras no se puede sumar, restar, multiplicar o dividir. En fin, el computador no sólo necesita saber con qué tipo de dato está trabajando, sino que también necesita saber qué tipo de operaciones se pueden realizar con ellos.
En los inicios de la computación digital se programaba utilizando el lenguaje binario, esto es, unos y ceros. Se podrán imaginar lo difícil y propenso a errores que era esto. Fue así como surgieron lo que se denomina lenguajes de bajo nivel, esencialmente, lenguaje de máquina o Assembly. Se trata de una serie mnemónicos que representan el conjunto de instrucciones del procesador (Instruction Set Architecture). Esto implicaba que el programador debía especificar en cada caso cuántos bytes (cada byte son 8 bits) habrían de utilizarse para almacenar cada pieza de información.
Aunque mucho mejor que programar en código binario, la programación en Assembly también era muy dificultosa, lenta y propensa a errores. Aunado a ello, un programa escrito para un tipo de procesador no era válido en otro, ya que cada modelo o tipo de procesador tiene su ISA.
Entonces surgió Fortran (1957), Formula Translator, el primer lenguaje de alto nivel. Fortran básicamente consiste (aún se usa) en un lenguaje de programación, esto es, un lenguaje formal clausurado (es decir, un conjunto de cadenas de símbolos que utilizan un puñado bien definido de reglas para crear otras cadenas de símbolos que pertenecen al mismo conjunto) y un compilador. Un compilador no es otra cosa que un programa, un software, que traduce en código de máquina un programa escrito en este lenguaje formal del que hablamos. Y fue así como nacieron los lenguajes de alto nivel y, con ellos, una era donde el uso de las computadoras se volvería en algo cotidiano para la realización de casi cualquier tarea.
Con la emergencia de los lenguajes de programación de alto nivel, se logró que los programadores se liberasen del tedioso proceso de tener que ubicar y desplazar los datos en memoria manualmente. El que esto se haya podido lograr depende, entre otras cosas, del desarrollo de la idea de los tipos de datos. Esto es, el compilador se encargaba de definir por nosotros los bytes necesarios para almacenar la información, pero para ello el programador debía especificar en el lenguaje en cuestión, el tipo de dato de las variables que se usaban en cada caso.
Hoy en día, con compiladores más avanzados, los tipos de datos sirven también para verificar la corrección de nuestros programas.
Todo lenguaje de programación cuenta con tres tipos de datos básicos, a saber:
Numéricos
De texto o alfanuméricos
Booleanos (nombre que proviene del álgebra de George Boole y que connota los valores de verdadero y falso)
En esta notebook utilizaremos el lenguaje Clojure, el cual es un lenguaje de tipado fuerte y dinámico. El tipado fuerte, en oposición al débil, indica que el compilador es muy estricto en cuanto a las posibilidades y condiciones para cambiar una variable con tipo de dato a otro tipo de dato. Y tipado dinámico significa que el compilador se encarga de deducir por sí solo el tipo de dato de las variables y estructuras de datos usadas por el programa en cuestión. Existen otros lenguajes con tipado estático, esto significa que el compilador espera que el programa declare con anticipación el tipo de dato de las variables y estructuras de datos que se usan.
¡Comencemos!
Los siguientes son datos numéricos.
Acá tenemos un entero
12344
Un número real
23.902
Y un número racional
1/4
Si queremos saber cómo el compilador interpreta esos datos podemos servirnos de la función type, que tal como su nombre indica, nos dirá su tipo.
(type 12344)
(type 23.902)
(type 1/4)
En primer lugar, habrán notado que pusimos todo entre paréntesis. Clojure como un lenguaje de la familia LISP (List processing) trabaja con expresiones simbólicas, las cuales se escriben entre paréntesis. Dentro de éstas, el primer lugar (de izquierda a derecha) lo ocupa siempre la función y luego le siguen los argumentos de la misma.
¡Volvamos a los tipos de datos!
En Clojure todos los números enteros son del tipo java.lang.Long, esto significa que van desde....ehhh...¡preguntémosle a Clojure! Comencemos por el valor más grande:
(println (Long/MAX_VALUE)) ;; Utilizamos la función println para ver el resultado debido a una
;; particularidad de NextJournal para representar los longs, que nos
;; impide ver su valor directamente
Y luego por el más pequeño:
(println (Long/MIN_VALUE))
Es decir, un número entero en Clojure es cualquiera entre 9.223.372.036.854.775.807 y -9.223.372.036.854.775.807
¿Qué ocurre si quiero obtener un número mayor? Veamos:
(+ Long/MAX_VALUE 1)
Pues bien, lo que vemos allí se llama una excepción, esto es, un mensaje del compilador que nos indica que algo ha ido mal. Nos dice que se ha producido un Integer Overflow, que no es otra cosa que hemos excedido la cantidad de bytes que están reservadas para un número entero en Clojure.
Si queremos realizar operaciones con números muy grandes tenemos que utilizar el tipo BigInteger. Lo único que tenemos que hacer para indicar este tipo de dato es agregarle una N a nuestro número:
(type 9223372036854775808N)
Como vemos, al preguntar por el tipo de dato nos dice clojure.lang.BigInt. Existe otro truco para promover un número a BigInt cuando realizamos alguna operación aritmética:
(+' Long/MAX_VALUE 1)
La función +' promueve automáticamente a BigInt el resultado de la operación. Tenemos también los equivalentes para el resto de operaciones aritméticas, excepto la división ( *' -' ).
Tenemos también los números reales, que en este caso forman parte del tipo java.lang.Double. Para entender qué significa este tipo de dato, debemos pensar cómo representar los números reales en el computador. Pensemos, por ejemplo, en un número real cuya parte decimal es infinita. No podemos representarla tal cual en el ordenador, pues tendríamos que asignarle toda la memoria disponible y aún así faltaría; es absurdo. Por ende, es necesario elegir la precisión que necesitamos. Los números reales se representan a través de una convención que se denomina punto flotante. Este es el tipo de dato float, luego tenemos los números de doble precisión y éstos son justamente los double que vemos acá.
Dentro de los tipos de datos numéricos tenemos algo que se llama promoción. Esto es, si ejecutamos una función que involucre números de diferentes tipos, el resultado de la operación se promoverá a un tipo de dato determinado, el que más tenga sentido.
Veamos el siguiente ejemplo:
(+ 23 10.0)
El resultado es un entero a pesar de que un número es entero y el otro es double, pero como la parte decimal es 0 no hay riesgo en mantenerse en el campo de los enteros. Acá no habría promoción.
(* 12 9.56)
En este caso el resultado es un número del tipo double; hubo promoción.
Clojure también nos permite trabajar con números racionales:
(+ 1/2 2/6)
Como vemos el resultado es otro número racional (NextJournal los presenta como vectores, pero son fracciones). Probemos de nuevo mezclar números de diferente tipo:
(* 1/9 9.34)
Normalmente, en matemáticas no trataríamos de hacer esto, sino que tendríamos una representación racional de 9.34 y luego haríamos la operación. Sin embargo, acá como podemos calcular el resultado inmediatamente y nos devuelve un double, como era de esperarse.
Y para cerrar con los números, casi olvidaba decirles que así como existe BigInt también existe BigDecimal. Para representarlo tienes que agregar una M al número en cuestión.
(type 1.5656565M)
Otro tipo de dato primitivo es el booleano, el cual se trata simplemente de los valores true y false.
false
true
Y si preguntamos por su tipo de dato...
(type true)
Vemos que es java.lang.Boolean. Los booleanos son muy importantes para las estructuras de control, es decir, para controlar la lógica de ejecución de nuestros programas. Luego tendremos ocasión de profundizar sobre esto.
Dentro del tipo de dato alfanúmerico tenemos al String o cadena de caracteres. Para representarlo sólo tenemos que escribir el texto entre comillas.
"Soy un String"
Usemos la función type con él
(type "Soy un String")
Vemos que la cadena "Soy un String" pertenece a java.lang.String. Las cadenas de caracteres son justamente, cadenas, y esto nos viene como anillo al dedo para introducir el tema que viene a continuación: las estructuras de datos.
Que un String sea una cadena de caracteres significa que es un vector que agrupa un conjunto de caracteres, por lo que podríamos manipular a un String con las funciones que manipulan vectores. Esto lo veremos más adelante.
Por ahora volquemos nuestra atención al caracter. En Clojure los caracteres se representan con una barra invertida seguida del símbolo.
\a
(type \a)
Si queremos ver todos los caracteres de un String como un lista, podemos usar la función seq:
(seq "Soy un String")
Y podemos usar cualquier otra función que usaríamos con una colección secuencial, por ejemplo, podemos ordenarlo con la función sort:
(sort "Soy un String")
Podemos obtener los caracteres en orden inverso con la función reverse:
(reverse "Soy un String")
Y ¿qué tal si solo quiero los caracteres únicos?
(distinct "Soy un String")
Si quieren seguir divirtiéndose clonen la Notebook haciendo click en Remix y aquí les paso un cheatssheet para que exploren y descubran nuevas funciones.
Antes de pasar a las estructuras de datos, falta ver cómo creamos en Clojure un tipo de dato compuesto. Para ello utilizamos la función defrecord. Pongamos que queremos crear el tipo de dato Cliente, tendrá los campos nombre, apellido, teléfono e email.
(defrecord Cliente [nombre apellido telefono email])
Luego para crear un cliente hacemos lo siguiente:
(->Cliente "Julian" "Álvarez" "4528-5689" "julianalv2@gmail.com")
Antes de seguir con el próximo tema conviene introducir la forma especial def. Esta nos sirve para definir un valor, de esta forma se lo persiste en memoria y nos podemos referir a él más adelante en nuestro programa. Definamos al cliente de arriba:
(def cliente1 (->Cliente "Julian" "Álvarez" "4528-5689" "julianalv2@gmail.com"))
Hecho esto, si queremos recuperar alguna información del cliente podemos hacer lo siguiente:
(:nombre cliente1)
¡Et voilá!
Si queremos especificar el tipo de dato de cada campo, como por ejemplo para validar que se use correctamente, podemos hacerlo así:
(defrecord Persona [String nombre int dni])
Ahora creemos una persona, pero erremos adrede el tipo de dato correcto del DNI (int indica que esperamos un entero) y veamos qué pasa:
(def persona (->Persona "Mario Balotelli" "123452"))
Dentro de esta larga excepción miremos en message; veamos que el compilador nos dice java.lang.String cannot be cast to class java.lang.Number, que no es sino otra forma de decir "no puedo convertir una cadena de caracteres a número". De esta forma se está cumpliendo la restricción que pusimos: el campo dni debe ser un número entero. ¿Quieren intentar algo? Acá tienen una celda para que prueben (no olviden hacer click en Remix para que puedan editar)
Estructuras de datos
El conocimiento y familiaridad con las estructuras y tipos de datos es fundamental para resolver cualquier problema de programación. Y es que debemos darle expresión a nuestro problema de dominio mediante una o más estructuras de datos concretas, más allá del tipo de dato específico que tome un dato singular.
Una idea importante dentro de estructuras de datos es la de colección. Como el nombre lo sugiere una colección es un conjunto de cosas, esas cosas en programación son tipos de datos nativos o básicos, esto es, enteros, números de tipo flotante, cadenas de caracteres o booleanos (true o false).
Ahora bien, una colección no es un conjunto, es una idea más abstracta. De hecho, un conjunto es un tipo de colección. En Clojure tenemos las siguientes colecciones: listas '( ), vectores [ ], mapas { } y conjuntos #{}. Una de las ventajas de Clojure sobre lenguajes como Java y Scala, por ejemplo, es que puedes crear colecciones usando simplemente el literal que identifica a cada colección, en vez de tener que invocar un clase y crear un objeto respectivo. Otra ventaja consiste en que en Clojure existe una misma API (Application Programming Interface), lo que en este contexto significa un conjunto específico de funciones, que puedes usar para trabajar con todo tipo de colección. Eso sí, distinguiendo entre las asociativas como los mapas y las secuenciales como los vectores y listas.
Listas y vectores
Comencemos con las estructuras de datos secuenciales. En Clojure podemos crear una lista de dos maneras:
(def c (list 1.32 2.3 4.3 4.22 4.023 3.02))
(def l (13 313 "alfa" :final avalon))
Noten que avalon es un símbolo, pero no está citado (') porque toda la lista ya lo está. Si en otra estructura de datos, como la de arriba, no lo citamos, Clojure tratará de buscar la definición avalon, es decir, lo interpretará como una función. Y al no poder encontrarla, nos arrojará una excepción.
Ahora creen algunas listas en la celda de abajo.
También podemos crear un vector de las siguientes maneras:
(def v [1233 31434 456 4546 543 23432])
(def z (vector 213 424 34 2349439 3893428 1034803403 834390 34894328))
También es posible crear un vector a partir de otra colección, incluso si es asociativa. Para esto usamos la función vec:
(def nums (vec (1 13 43 34 542 2 24 1 53 2 423)))
(def nums2 (vec {2 34 21 902 211}))
(def num_map (vec {:a 1 :b 3 :c 334 :d 232}))
;; Cree un vector a partir de otra colección
;; Cree un vector a partir de otra colección diferente a la de arriba
Ahora realicemos las operaciones más comunes con colecciones secuenciales, esto es, insertar y remover elementos u obtener el primer elemento, el último, la cola, etc.
Si deseamos agregar un elemento a nuestra lista o vector, empleamos la función conj (que viene del inglés conjoin):
(conj v 23)
(conj l 123312)
No olviden que abajo de la celda, donde se muestra el resultado, pueden hacer click en la flechita y explorar el resultado. Resulta muy útil especialmente en el caso en que obtengamos estructuras anidadas como output.
Ahora bien, inspeccionen las celdas de arriba y noten lo siguiente: en la lista conj agregó el nuevo elemento en primer lugar, mientras que en el vector lo agregó de último. Aunque se trata de la misma función, su conducta es ligeramente diferente en cada estructura de datos. Las razones tienen que ver con la eficiencia de las operaciones, pero no entraremos en ese tema ahora.
Pero si deseamos que nuestro vector tenga el nuevo elemento en primer lugar, podemos usar la función cons:
(cons 12323 v)
Ahora tratemos de quitar un elemento de nuestras colecciones secuenciales. Para ello utilizamos la función pop:
(pop l)
(pop z)
De nuevo, observen cómo pop tiene una conducta diferente según sea una lista o un vector. Pop remueve el último elemento del vector, mientras que en la lista es el primer elemento el que queda fuera.
Ahora utilice conj en alguna de las estructuras de datos secuenciales que creó arriba:
A continuación utilice cons:
Por último, utilice pop:
Vale la pena destacar que en Clojure las estructuras de datos son inmutables, esto es, no se pueden modificar una vez creadas. Si deseamos transformar una estructura de datos, debemos nombrar la operación de transformación para así obtener la nueva colección (e.g. (def nuevo-vc (conj vc 2323))). En cierto sentido, las funciones que estamos conociendo nos sirven para consultar el contenido de la estructura de datos y/o para realizar ciertas transformaciones temporales sobre ella.
Ahora vamos a ver cómo podemos obtener elementos específicos de nuestra colección. Si queremos el primero utilizamos la función first:
(first nums)
(first c)
Podemos incluso pedir el segundo ¡Adivinen! Sí, second:
(second l)
Si deseamos el último llamamos a la función last y, como venimos haciendo, le pasamos como argumento la colección en cuestión:
(last z)
Pero digamos que soy tan caprichoso que quiero el penúltimo también. ¡No hay problema! :
(-> z
butlast
last)
¡A ver! ¡¿Qué cosa tan extraña hicimos acá?! Básicamente utilizamos la función butlast que, como su nombre nos sugiere, nos devuelve todo menos el último elemento y sobre el resultado de esta operación llamamos a last. Esta operación está envuelta convenientemente en un macro llamado single threading macro (que sería algo así como macro de hilado simple) que básicamente nos simplifica esta expresión => (last (bustlast z)). Ambas expresiones son equivalentes, pero los macros de hilado hacen el código más legible y elegante.
Si deseo el resto de mi colección, es decir, todo menos la cabeza, uso la función rest:
(rest c)
;; Ejercicio: cree un vector cuyos elementos sean otros vectores
;; y obtenga el primer elemento del primer vector
Noten que los nombres de las funciones son muy intuitivos. Y si tienen alguna duda ya saben que pueden utilizar las funciones doc y find-doc.
Si deseamos algún otro elemento podemos acceder a cualquier lugar de una colección secuencial a través de su índice. Digamos que quiero el cuarto elemento del vector v:
(nth v 4)
;; o también
(v 4)
La última expresión es muy interesante. Podría pensarse que es equivalente a la notación de otros lenguajes donde se indica la referencia del objeto y el índice (e.g. v[4]), sin embargo, lo que está ocurriendo acá es que v está ocupando la posición de función; estamos usando a la colección v como una función y le estamos pidiendo que busque en sí misma el valor en el índice 4.
Si yerro en el índice, pues éste no existe, obtendré una excepción. Afortunadamente a la función nth podemos pasarle un valor por defecto para que nos sea devuelto en caso que el elemento buscado no exista. Pongamos que devolveremos -1 si el elemento no existe.
(nth nums 19 -1)
Pero digamos que quiero saber exactamente cuántos elementos tiene mi colección. Para eso está la función count:
(count v)
En este punto creamos tantas colecciones secuenciales que alguno, con derecho, puede estar confundido. ¿Cuál es un vector? ¿Cuál es una lista? ¡Don't panic! Si queremos saber si nuestra colección es un vector llamamos a la función vector?
(vector? z)
(vector? l)
;; prueba con alguna de las colecciones que creaste arriba.
De forma análoga con lista:
(list? nums)
(list? c)
Ahora pasemos a examinar las colecciones asociativas, a saber, los mapas.
Mapas
En Clojure los mapas son una de las colecciones más versátiles y es muy fácil integrarlas en nuestras funciones, por ejemplo, podemos hacer que los argumentos de nuestra función sean las llaves de un mapa. También podemos usar mapas para modelar nuestros problemas de dominio, cosa que en Java u otros lenguajes orientados a objetos haríamos con una clase. Pero eso para más adelante.
Por ahora, lo básico es saber que un mapa almacena los datos en pares de llave y valor. Si queremos un valor, lo obtenemos a través de su llave. Digamos que quiero almacenar la información de un contacto personal:
(def juan {:nombre "Juan"
:apellido "Perez"
:dni 212312
:direccion {:calle "Valle del Rey 48454"
:ciudad "Buenos Aires"
:codigo_postal 1978}
:hijos ["Mario" "Fabiana" "Marina"]})
Para obtener el valor de alguno de los campos podemos sencillamente colocar la llave en la posición de función y pasar como argumento el nombre del mapa en cuestión.
(:dni juan)
Si tenemos un mapa anidado, como en el ejemplo, podemos utilizar el macro de hilado simple (noten que la colección siempre va en primer lugar):
(-> juan
:direccion
:calle)
También podemos emplear la función get-in:
(get-in juan [:direccion :codigo_postal])
Pudimos alternativamente haber utilizado get para obtener el valor asociado a una llave:
(get juan :apellido)
;; obtenga el valor de la llave ciudad
Si se confunden con el orden de los argumentos que se le pasan a alguna función de las que hemos visto (y cualquier otra, por supuesto) utilicen la función doc para que los asista.
En un mapa se pueden combinar distintos tipos de colecciones. Por ejemplo, acá vemos cómo una de las llaves está asociada a un vector. Así que podemos usar una combinación de criterios de búsqueda asociativos y secuenciales en la medida en que esto tenga sentido en nuestra estructura de datos. Por ejemplo, digamos que queremos saber el nombre del tercer hijo de Juan:
(get-in juan [:hijos 2])
¿Por qué colocamos el índice número 2 en vez de 3? Porque los índices comienzan a contar desde cero. Esto vale para casi todos los lenguajes ( Julia es una excepción).
¡Genial! Ahora deseo agregar nueva información en el mapa. La mascota no puede faltar. Para eso tenemos la función assoc (del inglés associate):
(assoc juan :mascota "Bobby")
Y ¿si deseo agregar nueva información dentro del mapa anidado? Pongamos la comuna y su número. ¡No hay problema!
(assoc-in juan [:direccion :comuna] 11)
¡Juan tuvo un nuevo hijo! Agregémoslo:
(update juan :hijos (conj % "Ricardo"))
La función update recibe como argumentos el mapa, la llave y una función que nos permitirá actualizar el valor en cuestión. Arriba utilizamos una función anónima. Existen dos sintaxis para las funciones anónimas, una larga y otra corta. La larga es muy similar a la de una función nombrada, a saber, (fn [argumentos] cuerpo_de_la_función). De hecho, defn es una contracción de def y fn. La sintaxis corta es la siguiente: #(cuerpo_de_la_funcion). Los argumentos se pasan a través del literal %. Si hay más de uno se suelen numerar así %1, %2... Así que no hicimos otra cosa que llamar a nuestra conocida función conj y pasarle al vector como parámetro o argumento.
¿Cómo saber cuál sintaxis de función anónima usar? Depende. Hay veces en que resulta más claro usar la sintaxis larga. Lo cierto es que las funciones anónimas son muy útiles cuando necesitamos crear una nueva función ad hoc dentro de una función. En todo caso, no se preocupen si aún no les cae la ficha con este tema, lo profundizaremos más adelante en el curso.
Si deseamos cambiar un valor, en este caso el dni, podemos crear lo que se llama una función constante, esto es, aquellas que independientemente del parámetro devuelven siempre el mismo valor.
(update juan :dni (fn [x] 19324190))
¿Complicado? Usa entonces assoc :
(assoc juan :dni 900002)
;; Agregue una llave cualquier al mapa 'juan' con su respectivo valor
Update deberíamos usarlo en realidad cuando la transformación del valor implique alguna clase de cálculo, mientras que para una simple sustitución deberíamos usar assoc.
Por supuesto también tenemos update-in. Digamos que el nuevo valor del código postal es el viejo dividido entre dos:
(update-in juan [:direccion :codigo_postal] (/ % 2))
No olvidemos que en los lenguajes de la familia Lisp la notación matemática es prefija en vez de infija, esto es, el operador va primero. En una expresión simbólica la función siempre va en primer lugar.
Si deseamos eliminar algo de nuestro mapa, quitamos la llave. Para esto usamos la función dissoc. Juan nos denunció por invasión a su privacidad, así que tendremos que eliminar los datos de su dirección:
(dissoc juan :direccion)
Pero ¡recuerden! En Clojure las estructuras de datos son inmutables, así que sillamamos al mapa juan, toda la información original estará allí. Si deseamos sacar alguna definición de nuestro espacio de nombres (namespace) tendremos que hacer lo siguiente:
(ns-unmap *ns* juan)
;arroja excepción si lo des-comentan
;juan
¿Se dieron cuenta que la función se llama un-map? No debe extrañar que el namespace sea una especie de mapa.
Digamos que, como arriba, tengo muchísimas estructuras de datos ¿Cómo sé si la que me interesa es asociativa? Simple:
(associative? juan)
Y ¿los conjuntos? ¿qué son? Preguntémosle a Clojure:
(associative? {21 3 1223 2312})
¿Serán secuenciales entonces?
(seq? {989 10 91})
No. Tampoco. ¿Qué son?
Conjuntos
Los conjuntos en Clojure constituyen un tipo de colección particular. Un conjunto es una colección de valores únicos (no repetidos). Se pueden aplicar algunas de las funciones que vimos arriba sobre los conjuntos, pero los conjuntos cuentan con su propia API para soportar operaciones que son típicas para los conjuntos tales como unión, diferencia, intersección, etc. Veamos algunos ejemplos.
(def cartas {:bastos :espadas :corazones :copas})
;; Cree un conjunto cuyos elementos sean del tipo string
Podemos usar algunas operaciones secuenciales como first:
(first cartas)
(last cartas)
;; obtenga la cola (es decir, todos los elementos menos el primero) del conjunto creado
Pero si queremos obtener el enésimo (nth) elemento obtendremos una excepción, ya que un conjunto no está indexado.
(nth 3 cartas)
Pero podemos agregar un elemento a un cojunto con conjoin (conj)
(conj cartas :joker)
Para quitar un elemento podemos usar disj (del inglés disjoin)
(disj cartas :bastos)
¡Atención! disj sólo se puede usar con conjuntos.
Podemos contar los elementos de un conjunto:
(count cartas)
Existen dos tipos de conjuntos, los hashsets y los sorted sets. La diferencia radica en que los segundos pueden ordenar sus elementos. Si creamos un mapa con los literales obtendremos un hashset.
(type cartas)
También podemos crearlo con la función hash-set:
(hash-set 2213 213 2131 901)
Pero si deseamos un sorted-set tendremos que crearlo específicamente con la función homónima:
(sorted-set 900 1 23 48 90 67)
Noten que introduje los números sin ningún orden y en la celda del resultado aparecen ordenados.
Cree a continuación un sorted-set:
Con los conjuntos podemos usar la función contains? para consultar su contenido:
(contains? cartas :bastos)
Si deseamos utilizar las funciones propias de los conjuntos de las que hablamos arriba, debemos importar su librería. Podemos hacerlo así:
(use clojure.set)
Ahora creemos dos hashsets para hacer los ejemplos:
(def ab {32423 423 332 3 3442423 34223})
(def cd {3 423 303 2303934 3201930 240312 3129032 1 343})
Apliquemos la unión de ambos conjuntos:
(union ab cd)
La intersección:
(intersection ab cd)
Y la diferencia
(difference ab cd)
Más ejercicios
A continuación plantearemos algunos ejercicios para que practiquen lo aprendido.
Vamos a escribir algunas funciones que nos resolverán un problema determinado. Es importante tener en cuenta que el programador que escribe la función establece el contrato por el que esta se va a regir, es decir, qué tipo de parámetros obtendrá como input y qué arrojará como output.
Ejercicio 1
En un curso de programación se realizó la primera evaluación y el profesor prometió un premio para la calificación más alta. Ayudemos al profesor a saber quién obtendrá el premio. Programe una función que, recibiendo una cantidad n de calificaciones, devuelva la más alta.
TIPS:
Puede utilizar el single threaded macro (->)
Téngase en cuenta que el retorno está definido por la última expresión simbólica en la función y que su tipo será el producto de tal operación;
Piense en el algoritmo como una secuencia de pequeñas transformaciones que tiene que realizar sobre el input para llegar al output. Resulta de mucha ayuda anotar estos pasos primero en un papel y/o escribirlas como comentario en el cuerpo de la función.
(defn calificacion_max
"Escriba aquí la documentación de su función"
[] ;; <= Escriba dentro del vector el nombre del o de los parámetros o argumentos
;; <= Escriba acá el cuerpo de la función. Idealmente, una expresión simbólica
)
Pruebe su función:
;; Cree una estructura de datos de prueba y luego pásela como argumento a su función.
;; Asegúrese que devuelve el resultado correcto.
Ejercicio 2
Un equipo de baloncesto necesita conocer la estatura promedio de sus 12 jugadores. Defínase, a través de la documentación y de la implementación, el input y el output. Establézcase un algoritmo que resuelva el problema (cálculo de un promedio).
TIPS:
Utilice la función reduce como paso intermedio;
Recuérdese que en Clojure, como lenguaje dinámico que es, normalmente no se utiliza la declaración de tipos de datos;
No olviden que se usa notación prefija, es decir, el operador matemático va en primer lugar (e.g. para sumar cuatro números: (+ 1 33 334 32) ).
Utilice algunos bindings para nombrar el resultado de operaciones parciales y así poder emplear esos valores para realizar cálculos en otras operaciones. Ejemplo, supongamos que quiero devolver un vector con el resultado de la suma, multiplicación y división de dos números:
(let [a 10.1
b 89.9
suma (+ a b)
mul (* a b)
div (/ a b)]
[suma mul div])
Ahora, ¡a resolver el ejercicio!
(defn estatura_promedio
"Escriba aquí la documentación de su función"
[] ;; <= Escriba dentro del vector el nombre del parámetro o de los parámetros
(let [] ;; <= Escriba aquí los bindings que necesite
;; <= Escriba aquí la expresión simbólica (s-exp) cuyo resultado será el valor de retorno de la función
))
Pruebe su función:
;;Cree un input según el contrato que definió y páselo a su función. Asegúrese que el resultado es el correcto
Ejercicio 3
El Gobierno de la Ciudad nos otorgó un contrato para actualizar el sistema de la SUBE. Nuestra tarea consiste en programar una función que actualice el saldo de la tarjeta (debitar) al realizar un viaje. Abajo, el input que recibirá nuestra función:
(def sube-id-32d21asa {:nombre "Miguel Rodriguez"
:dni 18565723
:nro_sube 1215415464511
:saldo 350})
Cree una función que reciba el mapa anterior como input y devuelva como output el saldo actualizado. No olvide reflexionar sobre cuántos parámetros serán ingresados en nuestra función y cuál es el tipo de dato de cada cual.
(defn debitar-saldo
"Escriba la documentación de su función"
[]
)
Pruebe su función:
Ejercicio 4
Una tienda de moda por departamentos aplicará un descuento del 20% sobre todas sus prendas. Se nos pide que actualicemos el programa que marca los precios. Cree una función que recibiendo una lista de precios, devuelva una lista con el descuento correspondiente.
TIPS:
Utilice una función anónima, preferiblemente en su versión larga o extendida para mayor claridad (e.g. una función que tome dos parámetros y los multiplique sería así: (fn [a b]
(* a b)) )
(defn aplicar-descuento
"Documente su función"
[]
)
;; Utilice esta celda para probar su función. La solución de este ejercicio se discutirá en clases.
Soluciones
¿Te trabaste? No hay problema. Aquí puedes mirar las soluciones (téngase en cuenta que una solución correcta no tiene que lucir exactamente como éstas)
Ejercicio 1
(defn calif-max
[vc]
(-> vc
sort
last))
Prúebenla:
Ejercicio 2
(defn alt_avg
[altrs]
(let [suma (reduce + altrs)
cant (count altrs)]
(/ suma cant)))
Prueben:
(def alts [1.232 1.89 1.27 1.90 2.05])
(alt_avg alts)
Ejercicio 3
(defn deb-saldo
[sube costo]
(:saldo ;; Como update devuelve un mapa, utilizamos la llave que nos interesa para devolver el resultado.
(update sube :saldo (- % costo))))
¡A probar!