3.4 Sentencias de control, ciclos y funciones

Todo el conocimiento ya obtenido no sería útil a largo plazo si no se tiene alguna manera de controlar el comportamiento del código bajo ciertas circunstancias, ya sea permitiendo ciertos bloques de código, repitiendo o ejecutando una cantidad determinada o indeterminada de veces un cierto proceso o aplicar ciertas transformaciones a los resultados. Por todas estas razones comenzaremos con la estructura de control más básica.


3.4.1 If & If-else

La estructura if() permite la ejecución de un cierto bloque de código si el parámetro booleano que recibe tiene por valor TRUE, lo cual indicaría que la condición necesaria para la ejecución del bloque es válida. Recordemos que un valor TRUE, y en general un booleano, se puede obtener mediante diferentes expresiones usando operadores lógicos.

if(boolean) {
  código
} 

Ahora, suponiendo que se desea tratar el simple problema de determinar si un usuario, de acuerdo a su edad, puede ver cierto contenido. Se puede actuar de la siguiente manera

if(edad>=18) {
  Permitir_contenido
}
if(edad == 17){
  No_permitir_contenido
}
if(edad == 16){
  No_permitir_contenido
}
.
.
.
if(edad == 0){
  No_permitir_contenido
}

O bien de la siguiente manera

if(edad>=18) {
  Permitir_contenido
}
if(edad < 18){
  No_permitir_contenido
}

En cualquiera de los dos casos es necesario tratar el complemento con otra sentencia if() lo cual no es recomendable cuando no se tenga certeza de todos los casos contrarios a la condición en el primer if. Por lo cual, para tomar el complemento, se tiene la sentencia if-else; donde si sucede la condición, se ejecuta el código dentro de los delimitadores del if y en caso contrario (else) se ejecuta lo correspondiente para el complemento de la condición.

if(edad>=18) {
  Permitir_contenido
}else{
  No_permitir_contenido
}

Existe en R la función ifelse() la cual permite trabajar de manera vectorial. Véase el siguiente ejemplo

#Se crea un vector con 15 edades de personas de manera "aleatoria"
(edades <- sample(1:90, replace = T, size = 15))
 [1] 85 22 58 49 20 18 83 81 36 57 37 77 71 46 87
ifelse(edades<18, yes = "Menor de edad", no = "Mayor de edad")
 [1] "Mayor de edad" "Mayor de edad" "Mayor de edad" "Mayor de edad"
 [5] "Mayor de edad" "Mayor de edad" "Mayor de edad" "Mayor de edad"
 [9] "Mayor de edad" "Mayor de edad" "Mayor de edad" "Mayor de edad"
[13] "Mayor de edad" "Mayor de edad" "Mayor de edad"

Es decir, que en cada entrada del vector evalúa la expresión dada y en caso de ser cierta la condición, se devuelve, en este caso, el caracter “Menor de edad” y en caso contrario “Mayor de edad”. Véase que la función ifelse() regresa un vector.


3.4.2 For, While y Repeat

Cuando un programador ve que un proceso se debe repetir una cantidad de veces, automáticamente piensa en un bucle. En el caso de R se tienen los ciclos for(), while() y repeat().

Recordando que en Java los primeros dos bucles tenían una estructura similar a la siguiente:

for(int i = 0; i<n; i++){
  Ejecución del código n veces
}

while(boolean){
  Ejecución del código hasta que el booleano sea false
}

En el caso de R se tendrá una estructura un poco diferente ya que se iterará directamente sobre un objeto y no usando una variable auxiliar que evalué una expresión para obtener un booleano y determine si el ciclo termino. En el caso del ciclo while se tiene la misma sintaxis.

for(elementos in objeto){
  Ejecución_del_código
}

while(boolean){
  Ejecución_del_código
}

Véase los siguientes ejemplos

for(number in 1:5){
  print(number + 1);
}
[1] 2
[1] 3
[1] 4
[1] 5
[1] 6

Se esta usando directamente cada elemento dentro del vector para ejecutar un cierto bloque de código. Hay que aclarar que no es necesario usar el elemento con el que se está iterando aunque es común hacerlo.

  • ¿Funcionará con una lista o una matriz?
for(letra in letters[1:5]){
  print(paste("Letra ", letra))
}
[1] "Letra  a"
[1] "Letra  b"
[1] "Letra  c"
[1] "Letra  d"
[1] "Letra  e"

En el anterior código se esta usando otro vector, el cual es no numérico, dando un mejor ejemplo de que el iterador no esta dependiendo de valores numéricos.

edad <- c(20,24,41,17,20)
n_cliente <- 1
control_parental <- TRUE
while(control_parental){
  if(edad[n_cliente]>=18){
    print("Apto para la película")
    n_cliente <- n_cliente+1
  }else{
    print("Menor de edad")
    control_parental <- F
  }
}
[1] "Apto para la película"
[1] "Apto para la película"
[1] "Apto para la película"
[1] "Menor de edad"

Aquí se están usando expresiones de control para determinar la salida del bucle.

Hay que recordar que en un ciclo while se corre el peligro de entrar en un ciclo infinito, el cual sería lo deseado en algunos casos. Un ciclo que se trata con un bucle tipo for siempre puede ser tratado como uno tipo while pero no siempre el caso contrario. Y que la elección entre un ciclo while y uno for depende de si se conoce el número de veces que se ejecutará un bloque o no.

  • ¿Existe algo equivalente al switch-case visto en Java?

Para estos dos ciclos se puede hacer uso de las sentencias break y next, las cuales permiten la interrupción de un ciclo y la exclusión de alguna iteración.

for(letra in letters){
  if(letra == "f") break
  print(letra)
}
[1] "a"
[1] "b"
[1] "c"
[1] "d"
[1] "e"

Cuando se ejecuta repeat el ciclo termina sin importar la existencia de más iteraciones sobre el objeto.

for(letra in letters){
  if(any(letra == letters[4:24])) next
  print(letra)
}
[1] "a"
[1] "b"
[1] "c"
[1] "y"
[1] "z"

Para el caso de next, véase que se saltó ciertas iteraciones más no termino el ciclo.

Finalmente, R proporciona la estructura break, en la cual puede encontrarse cierta similitud con un ciclo while o do-while.

repeat{
  .
  .
  ejecución_código
  .
  .
  break
}

Este bucle se detiene cuando se encuentra a la sentencia break.

n_saludos <- 5
repeat{
  print(rep("Hola", n_saludos))
  #Se detiene el repeat cuando n_saludos sea igual a 1
  if(n_saludos==1){
    break
  }
  n_saludos <- n_saludos-1
}
[1] "Hola" "Hola" "Hola" "Hola" "Hola"
[1] "Hola" "Hola" "Hola" "Hola"
[1] "Hola" "Hola" "Hola"
[1] "Hola" "Hola"
[1] "Hola"

Existe un “problema” con los ciclos en R debido a las configuraciones internas para el almacenamiento de objetos. Por el momento es suficiente saber que trabajar de manera vectorial o aplicar funciones que están en R de manera predeterminada es una mejor y más rápida forma de trabajar. Más adelante se verá en que casos los ciclos pueden ser “lentos” además de otros problemas que pueden surgir, por ejemplo en la recursión, y cuando será la mejor opción usar un ciclo que alguna función que trabaje de manera vectorial.


3.4.3 Funciones

Como ya se había mencionado, R es un lenguaje de programación orientado a objetos aunque también tiene ciertas características de un lenguaje funcional ya que las funciones en R pueden considerarse funciones de primera clase; es decir que pueden ser asignadas a variables, ser almacenadas en ciertas estructuras como las listas, servir como parámetros de otras funciones y ser el retorno de otras funciones. Aunque estas no pueden ser consideradas, como bien dice Handley Wickham en su libro RAdvance, puras; es decir funciones de orden superior ya que, para que una función sea considerada de orden superior, de acuerdo a un input dado, se debe tener una única salida (inyectividad) lo cual no sucede con muchas funciones en R como sample(), runif(), print() y hasta en <-().

Sin importar que las funciones no sean de orden superior, lo cual otorga cierta flexibilidad, es indudable la importancia de crear funciones para optimizar el trabajo, dar estructura, limpieza e incluso claridad; ya que al usar una función, el resultado será algo esperado sin importar su comportamiento interno; en cambio al visualizar un ciclo, este puede no quedar claro en su funcionamiento.

La creación de funciones es sencilla

nombre_function <- function(parámetros){
  código
}
  • Los parámetros pueden ser cualquier objeto e incluso funciones. En este último caso, a estas funciones se les llama funcionales.
  • El retorno de una función puede quedar explícitamente dado con la función return() aunque si no se utiliza esta función, la última línea de código en la función es considerada como el retorno de la función. El retorno puede ser cualquier tipo de objeto e incluso pueden devolver funciones. En este último caso, a estas funciones se les llama function factory.
  • Cuando una función tiene como valor de retorno un booleano, a esta función se le conoce como predicate y cuando una función acepta una o más funciones como parámetros y a su vez regresa funciones, se les conoce a estas funciones como function operator.

Los parámetros de una función pueden tener valores por defecto, los cuales pueden ser reescritos cuando se den explícitamente otros valores.

suma <- function(v1 = c(1,2), v2 = c(3,4)){
  sum(v1, v2)
}
suma()
[1] 10
suma(v1 = 4, v2 = 1:20)
[1] 214

Algo interesante de las funciones en R, al igual que en Python, es que se pueden dar una cantidad indeterminada de parámetros si así se indica en la función con el argumento .... Véase el siguiente ejemplo.

#v1 y v2 vectores numéricos
suma <- function(v1, v2, ...){
  sum(v1, v2, ...)
}

Es decir, que se está considerando ... como un parámetro más de la función aunque nótese que no es necesario agregar más de dos variables en la función.

suma(c(1,2), 1:4)
[1] 13
suma(c(1,2), 1:4, 1:20)
[1] 223

Ejercicios

  1. Crear un nuevo operando y utilizarlo con dos vectores.

  2. En el siguiente enlace se puede encontrar información sobre la función replicate. Teniendo en mente que la anterior función puede remplazar a un ciclo for, realice el ejercicio 16 utilizando dicha función. Vea que sucede al utilizar el parámetro simplify con los valores FALSE y TRUE.

  3. Aplicar las funciones min() y max() en la matriz del ejercicio 22 sobre los renglones y columnas.

  4. Con la lista creada al momento de explicar la función lapply(): list(a = seq_matrix, b = seq_matrix+1, c = seq_matrix+3), utilizar tal lista, la función lapply() y una función anónima, para sumarle 5 al elemento [2,3] de cada matriz.

  5. Con base en el ejercicio anterior, hacer una función que acepte una lista (como la del anterior ejercicio), un parámetro tipo String; los posibles valores de este serán “suma”, “resta”, “mutl”, “div”; también debe aceptar tres parámetros numéricos, el primero servirá como operando de las operaciones anteriores y los siguientes como coordenadas para ubicar el elemento en las matrices. De acuerdo a las operaciones anteriores, realizar pora cada caso con la función lapply y una función anónima la operación correspondiente al elemento designado por los parámetros de la función.

  6. Observese que sucede al ejecutar (1:10)[-1]. Obtener de la lista del ejercicio 13 los últimos 5 elementos de cada vector.

  7. Crear una función que, dada una lista, se determine que tipo de datos son sus elementos.

  8. Imprimir una sequencia de vectores con sapply().

  9. Crear un nuevo operando que sea capaz de concatenar un número con una letra.

  10. Utilizar la función del ejercicio anterior para concatenar a los elementos de una matriz de caracteres un número.

  11. Utilizar el operando anterior para concatenar un número a todos los elementos de la matriz del ejercicio 26.

  12. Usando la función which(), obtener los resultados solicitados del ejercicio 11.

  13. Usando alguna función de la familia apply y una función anónima, obtener los resultados solicitados del ejercicio 11.

  14. Obtener el máximo y mínimo de cada uno de los elementos en la lista del ejercicio 13.

  15. Crear una función que acepte una matriz y devuleva una lista con dos elementos: “StatsColumns” y “StatsRows”; los cuales deben ser listas y contener los resultados de aplicar las funciones sum(), prod(), sqrt(sum()) y cumsum() por renglones y columnas.