Estructuras de control

En esta lección vamos a estudiar las estructuras de control utilizando el lenguaje Julia. Antes de empezar cabe subrayar que la sintaxis de las estructuras de control, es sorprendentemente homogénea entre todos los lenguajes de programación. Sólo existen diferencias muy ligeras de un lenguaje a otro, por lo que les resultará muy fácil trasladar lo aprendido aquí al lenguaje que hayan elegido aprender.

Julia es un lenguaje moderno desarrollado en el MIT (Massachusetts Institute of Technology), muy veloz y diseñado para ser muy práctico para el álgebra lineal, lo que lo hace idóneo para áreas de la Inteligencia Artificial tales como el machine learning y el deep learning. Y, por supuesto, también se presta muy bien para la ciencia de datos.

Es importante destacar que si bien algunos lenguajes cuentan con un entorno particularmente propicio para la ciencia de datos y la Inteligencia Artificial, como he subrayado en otras ocasiones, estas tareas se pueden llevar a cabo con cualquier lenguaje de alto nivel. Aunque, en efecto, ciertos lenguajes (como Python, R, Clojure y Julia mismo) resultan más convenientes para este tipo de tareas que otros.

¿Qué es una estructura de control?

Un CPU lee y ejecuta instrucciones en modo secuencial y, de acuerdo con este hecho, tiene sentido que el código se organice también en modo secuencial. No obstante, incluso la realización de tareas simples no se puede llevar a cabo sin cierta repetición, es decir, existen tareas que, al diagramarlas de un modo u otro, exhiben una estructura circular. Por otra parte, también resulta necesario que el procesador ejecute ciertas porciones del código cuando se cumpla determinado conjunto de condiciones, ya que de otra manera nuestra capacidad de ejecutar tareas complejas se vería seriamente limitada.

De esta manera nacen los primeros constructos de los lenguajes de programación, como lo son los bucles y las estructuras condicionales.

En un principio, existía una palabra clave que codificaba una instrucción que hacía las veces de condicional. Me refiero al go to. Con esta instrucción se le indicaba al intérprete que debía dirigirse a determinada línea dentro del código fuente y ejecutarla. Sin embargo, el go to dio lugar a un antipatrón denominado código spaguetti (tarea: averiguar qué es código spaguetti) y es en este contexto donde E. Dijsktra y otros colegas dieron el puntapie inicial al movimiento de la programación estructurada (véase Dijkstra, Hoare, & Dahl (1972). Structured Programming. Disponible en nuestra biblioteca virtual)

Dijsktra argumentaba que el uso excesivo de go to's era signo de una pobre estructuración del código, lo que se traducía en programas difíciles de mantener, de entender y propensos a error. Su propuesta implica el uso de abstracciones como el if-then-else y el bucle while.

Así pues, en resumidas, una estructura de control es una abstracción que nos permite determinar cuándo y bajo qué condiciones se ejecutan determinados trozos de código. La ventaja de las estructuras de control, en comparación con lo que existía antes, es que brindan la posibilidad de escribir programas con mayor rigurosidad lógica, más comprensibles y más confiables.

Condicionales (if-then-else)

El dominio de los condicionales requiere una buena comprensión del álgebra booleana y una familiaridad bastante práctica con las llamadas tablas de verdad (sobre este tema ver los vídeos correspondientes en nuestra aula virtual). A modo de repaso, debemos recordar que las operaciones lógicas son operaciones aritméticas en el sistema binario. Para sumar dígitos binarios debemos tener en cuenta que se trata de una operación clausurada en el conjunto de los números binarios, lo que quiere decir que una operación aritmética entre números binarios sólo puede dar lugar a otro número binario. En este orden de ideas, 1+1=1 y 1+0=1. La suma representa la operación lógica del OR u O. Y en la multiplicación: 1*1=1 y 1*0=0, donde esta operación representa la operación lógica AND ó Y.

La estructura de un condicional es simple: comenzamos con la palabra clave if, seguida de una cláusula condicional también llamada predicate (en algunos lenguajes como Haskell, tenemos que escribir también la palabra clave then) y, por último, la palabra clave else y el predicado que le corresponde.

Pero mejor veámoslo en el código:

if (5 > 8)
  5*8
else
  5-8
end  
    
0.2s

Es una particularidad de Julia que los bloques lógicos del lenguaje como las estructuras de control, los structs (equivalente a las clases de Java), entre otros, culminen con la palabra clave end.

Arriba creamos una estructura condicional bastante simple que se explica por sí misma. Para probar la valía de los condicionales debemos subir el nivel de abstracción. Así que, ya que tenemos algo de práctica en el tema, hagamos una función que nos indique si un usuario de nuestra aplicación hipotética es menor de edad o no.

function es_menor_de_edad(edad)
  if (edad < 18)
    return true
  else
    return false
  end
end
0.6s
es_menor_de_edad(21)
0.4s
es_menor_de_edad(10)
0.1s

¡Genial! Tenemos una función que parece trabajar correctamente. Ahora bien, la minoría de edad es un concepto relativo a los sistemas de axiomáticos de las diversas culturas y que también varía de un sistema legal a otro. Así que hagamos una función que reciba como parámetro la edad considerada como el límite entre la minoría de edad y la mayoría de edad.

function es_menor_de_edadII(edad, edad_limite)
  if (edad >= edad_limite)
    false
  else
    true
  end
end
0.4s
es_menor_de_edadII(12, 15)
0.1s
es_menor_de_edadII(21, 18)
0.1s

Noten dos cosas: a) nombrar las variables y las funciones adecuadamente es muy importante. Es una convención nombrar las funciones que devuelven un booleano (es decir, true o false) es_tal_cosa, por otra parte, los parámetros también deben nombrarse evitando ambigüedades, sobretodo en lenguajes dinámicos donde no tenemos directamente información sobre el tipo de dato. Y b), algo más particular del lenguaje que nos ocupa, fíjense que no usamos la palabra clave return. Es opcional en Julia.

Es hora de que lo intenten ustedes.

Digamos que se nos pide programar una función de login para regular el acceso a la página web de la empresa. El usuario ingresará solamente un número de identificación de 8 dígitos. Si el input no tiene exactamente 8 dígitos, el ingreso es inválido. Si el número de id termina en 3, dará acceso e imprimirá por pantalla "Empleado administrativo". Si el número de id termina en 6, dará acceso e imprimirá por pantalla "Personal Ejecutivo". Si el número de id termina en 9, dará acceso e imprimirá por pantalla "Personal de servicio". (Pista: conviertan el input a String y exploren las funciones disponibles en la biblioteca base del lenguaje para trabajar con este tipo de dato)

0.2s

Bucles

Un bucle se usa para recorrer o atravesar una estructura de datos, concretamente una colección de algún tipo, y efectuar una operación sobre cada elemento de la misma. Si tenemos una lista de nombres y queremos organizarla alfabéticamente, empleamos un bucle; si queremos filtrar los elementos de una colección que cumplan con ciertos criterios, utilizamos un bucle; si deseamos pasar los elementos de una colección a otra de diferente tipo, usamos un bucle.

Existen básicamente dos tipos de bucle, a saber, el for y el while. En distintos lenguajes podrán encontrar otras variedades, pero básicamente se trata de variaciones de estos dos.

Un bucle while se caracteriza por la creación de un contexto o bloque de código que se va a ejecutar repetidamente hasta que se cumpla una condición de salida.

Veamos un ejemplo. Hagamos algo bastante simple como un programa que cuente hasta diez.

function hastaDiez()
  cnt = 0
  while cnt <= 10
    println(cnt)
    cnt+=1
  end
end
0.4s
hastaDiez()
0.3s

De una función que sólo imprime en pantalla, se dice que su tipo de retorno es void o vacío. Claro, esto puede variar dependiendo de las sutilezas de cada lenguaje, sin embargo, la idea que deben llevarse es que hay funciones que no retornan nada -y que tampoco reciben ningún argumento, como la presente.

A la hora de emplear un while es muy importante pensar primero en la condición de salida, ya que de lo contrario nuestro código se ejecutará ad infinitum, lo que nos obligará a matar el proceso.

En este punto un observador avezado podrá preguntarse dónde está la colección que supuestamente todo bucle debe atravesar. Justamente una de las características del while es que es agnóstico respecto a la colección, esto es, no hay ningún elemento en su estructura que nos de información sobre la colección que se atraviesa. Pero eso no significa que no exista, en este caso, la colección la hemos creado en la medida en que la hemos atravesado.

Bien podríamos haber agregado cada elemento en una estructura de datos como un vector y devolverla tras completada la operación.

function vectorDeDiez()
  cnt = 0
  arr=[]
  while cnt <= 10
    append!(arr,cnt)
    cnt+=1
  end
  arr #Si no escribimos acá la variable arr, la función no devolvería nada.
end
0.5s
vectorDeDiez()
0.4s

Se habrán fijado que escribimos una pequeña nota dentro de nuestro código. Se les llama comentarios y siguen una convención más o menos generalizada (// ;; -- ó # como en este caso). Consulten en la documentación del lenguaje que hayan decidido aprender cómo hacer comentarios en una sola línea y en múltiples líneas.

Volviendo con nuestra función, estoy seguro que habrán notado algo: una función como esta es algo aburrida y hasta inútil, así que podríamos pasarle como argumento el número de elementos que deseemos que nuestro vector tenga. Incluso podríamos crear otro parámetro que indique la medida en que incrementa el contador en cada paso (de uno en uno, de dos en dos, y así sucesivamente).

Veamos.

function crearVector(paso, limite)
  cnt=0
  arr=[]
  while cnt <= limite
    append!(arr, cnt)
    cnt+=paso
  end
  return arr #La palabra clave return es opcional en Julia
end
0.6s
crearVector(10, 100)
0.2s
crearVector(2,10)
0.1s
crearVector(3,33)
0.1s

Pero, como les decía, un bucle lo utilizamos usualmente para recorrer una colección. Digamos que quiero una función que me filtre una lista de nombres según la letra por la que empieza cada nombre.

function comienzaCon(letra, coleccion)
  long = length(coleccion)
  idx = 1 # En Julia los índices comienzan en 1, pero en otros lenguajes comienzan en 0
  res = []
    while idx <= long
      coincidencia = startswith(coleccion[idx], letra)
      if coincidencia
        push!(res,coleccion[idx])
      end
      idx += 1
    end
  res
end
0.4s
comienzaCon("J", ["Ilian","Marina", "José", "Trina", "Juliana"])
# Presten atención a lo siguiente: en Julia los caracteres se instancian entre comillas simples e.g. 'a', mientras que las cadenas de caracteres o strings con comillas dobles, e.g. "ejemplo"
0.2s

Repasemos lo que hicimos. En primer lugar, necesitamos saber el tamaño de la colección que recibimos como parámetro, ya que si llegamos a pedir un índice inexistente obtendremos una excepción. El tamaño nos sirve también para indicar cuándo debemos salir del bucle. En segundo lugar, marcamos el índice, que no es otra cosa que una variable que servirá de indicador mientras atravesamos la colección en cuestión. En tercer lugar, creamos un vector vacío donde depositaremos los strings que coincidan con nuestra búsqueda. En cuarto lugar, aplicamos dentro del bucle una función para trabajar con strings, la que nos devuelve un booleano de acuerdo si el string comienza con determinado caracter. Utilizamos una variable para capturar este booleano, el cual empleamos como predicado de un condicional. Si la condición se cumple, es decir, si coincidencia es true, agregamos el elemento a la colección res.

Habrán notado que usamos push! en vez de append! como en la ocasión anterior. La razón es que append! nos devolverá una colección de caracteres.

Cuando vamos a recorrer una colección debemos tener en cuenta que estaremos trabajando con un iterable, por lo que debemos asegurarnos que nuestra estructura de datos implemente o herede de la interfaz o clase iterable (según sea el caso en el lenguaje respectivo) y, asimismo, debemos buscar métodos o funciones que deriven de las interfaces o clases iterables. En Clojure, por ejemplo, todas las colecciones implementan la interfaz ISeq la cual representa una abstracción parecida al iterable como lo es la secuencia.

En el caso de Julia esta es la documentación respectiva. Aunque si la ojean, verán que provee otros métodos más avanzados que hacen superfluo o completamente innecesario el uso de un while y, en algunos casos, inclusive de un for. En efecto, se trata de funciones versátiles y eficientes que debemos preferir antes de implementar nuestros propios bucles.

¿Por qué no intentas utilizar algunas de estas funciones del módulo Iterators?

0.1s

Ahora nos corresponde hablar de los bucles for. En muchos lenguajes de programación modernos el bucle for es una estructura de control algo primitiva, por lo que nos encontramos con for mejorados o fors convertidos en list comprehensions.

Por ejemplo, en Java un bucle for simple luce de la siguiente manera:

for (int i = 0; i <= 10; i++){ //cuerpo del bucle }

Esta estructura de control se constituye de tres elementos: un índice, un límite (<= 10) y un contador que va incrementando el índice a cada paso. Mientras el for simple trabaja con índices, el for mejorado trabaja con objetos.

Digamos que tenemos una lista de artículos que queremos recorrer. El for mejorado luce así:

for (Articulo art : listaArticulos) { //cuerpo del bucle}

En primer lugar, indicamos el tipo de datos, en este caso, Articulo. Después, viene la referencia al objeto artículo (art) que servirá para representar cada artículo de la colección. Y finalmente, la colección como tal, listaArticulos.

A pesar de la incorporación de nuevas variantes, el bucle for simple aún se encuentra en uso (en lenguajes como Java, por ejemplo). Esta persistencia nos dice mucho de su importancia y es que, cuando necesitamos realizar operaciones de muy bajo nivel manipulando índices, la mejor opción es el for simple.

En Julia, sin embargo, no contamos con for simple, sino con list comprehesions (ya veremos de qué se trata) y con fors mejorados. Veamos cómo luce un bucle for en Julia:

for i in [1,23,4,4566,76,345,6465,7643,654,343]
  if mod(i,2)==0
    println(i)
  end
end
0.7s

Si se fijan, la sintaxis del for en Julia es casi idéntica en Python. ¿Qué hemos hecho acá? Hemos tomando un iterable llamado i el cual es una variable que se renovará en cada ciclo para adquirir el valor de siguiente elemento de la colección. A continuación, utilizamos una estructura condicional para implementar un filtro, a saber, queremos imprimir solamente los números pares (por eso empleamos la función mod(), que nos devuelve el módulo de una división). Y ya está. Así de simple.

Un list comprehension no es otra cosa que la definición de un conjunto en teoría de conjuntos. Es decir, es una estructura como la del siguiente ejemplo: 'Para toda x que pertenece al conjunto A, tal que x sea múltiplo de 2'.

Traduzcamos esta definición a una expresión computable con Julia:

x = [1,2,3,4,5]
A = [x for x = x*2]
1.2s

Para hacerlo en un solo paso, en vez de declarar la colección primero y después la comprehensión, pudimos haber hecho lo siguiente:

A = [x*2 for x in 1:5]
0.4s

Noten que en Julia podemos expresar un rango con la siguiente sintaxis begin:end. También en un for mejorado pudimos haber hecho lo siguiente:

for i in 1:100
  if mod(i,2)==0
    println(i)
  end
end
0.7s

Y así tenemos todos los números pares del 2 al 100.

Habíamos dicho que en Julia no existen bucles for simples, ¿cómo hacemos entonces cuando necesitamos manipular índices? Para eso utilizamos la función eachindex() de la siguiente manera:

for i in eachindex([90,1,43,632,246,0,234,353,45])
  println(i)
end
0.5s

Noten que lo que se imprimió fueron los índices del array y no sus valores. Así con eachindex() podemos realizar en el cuerpo del bucle las operaciones que deseemos con los índices de nuestra estructura de datos.

¡Bien! Es hora de hacer algunos ejercicios.

Utilicen un list comprehension para generar un colección de números impares del 1 al 100.

A continuación, utilizando un bucle for, generen una colección de diez elementos y sumen dos a cada elemento.

Finalmente, creen una colección de 15 elementos y multipliquen por 3 a cada elemento ubicado en un índice impar.

Para terminar, les dejamos una celda (recuerden que, de igual modo, cuando hacen click en Remix y se generan un copia de la notebook, pueden agregar cuantas celdas deseen) para practicar lo visto en esta lección:

Runtimes (1)