Escribir funciones es una de las actividades más importantes en cualquier lenguaje de programación. Su finalidad es poder encapsular fragmentos de código que necesiten ser ejecutados en múltiples ocasiones, con la posibilidad de ejecutarlos en cada ocasión con diferentes parámetro. A su vez, las funciones aumentan la legibilidad de un programa, de modo que es más fácil de entender para uno mismo y para los demás en caso de compartirlo.

 

r_funciones

 

Funciones


Introducción

Las funciones en R son tratadas como cualquier otro objeto. Para crearlas utilizamos el comando function(), el cual crear objetos de tipo function, de la siguiente manera:

f <- function(<argumentos>) 
{
  ## Código de la función (function body)
}

 

Luego, para llamar a la función simplemente escribimos el nombre de esta:

f <- function() 
{
  cat("Hola Mundo")
}

f()
Hola Mundo

class(f) 
"function"

 

Las funciones poseen 3 partes:

  • El cuerpo (body)
  • Los argumentos (formals)
  • El ambiente (environment)

 

f <- function(x, y) { x + y }

body(f)
{
 x + y
}

formals(f)
$x
$y


environment(f)
<environment: R_GlobalEnv>

 

Argumentos

Los argumentos de una función son una serie de valores (opcionales) que se pasan a la función, de modo que ciertas variables dentro de estas posean diferentes valores en cada llamada a la función.

area_rectangulo <- function(lado1, lado2) 
{
  area <- lado1 * lado2
  print(paste("el área es ", area))
}

area_rectangulo(2, 3)
"el área es  6"

formals(area_rectangulo)
$lado1
$lado2

 

Cuando se hace un llamado a una función, el orden en que se pasan los valores de los argumentos corresponde con la ubicación de estos en la delación de la función. Por ejemplo, en el caso anterior, el valor 2 se pasa a la variable lado1, mientras que el valor 3 se pasa a la variable lado2. Si queremos indicar explícitamente que valor asignar a cada argumento debemos indicar el nombre de este al llamar a la función.

area_rectangulo <- function(lado1, lado2) 
{
  area <- lado1 * lado2
  print(paste("el área es ", area))
}
area_rectangulo(lado1 = 2, lado2 = 3)
"el área es  6"

 

Este ultimo método es el más recomendable cuando las funciones tienen un gran numero de argumentos. Algo a notar cuando asignamos valores a los argumentos es que se suele utiliza el operador =, en vez del <-. Esto es simplemente una cuestión de estilo, ya que de otro modo seguiría funcionando correctamente la código.

Ahora supongamos que hacemos una llamada a una función que tenga una serie de argumentos, pero uno de estos no lo pasamos. Lo que sucederá es que la función se ejecutará hasta el punto en que debe ser utilizado el argumento faltante, y es en este momento que se genera un error. Este modo de funcionamiento se debe a que R evalúa las instrucciones al momento de ejecutarse (Lazy Evaluation), y no antes. En caso de que la función nunca utilice el argumento faltante, esta terminará su ejecución sin generar error.

f <- function(x, y, z)
{
 print(x+y)
 print(z)
}

f(x = 2, y = 0)
2
Error in print(z) : argument "z" is missing, with no default

 

Para evitar este tipo de comportamientos podemos asignar valores por defecto (default value) a las variables en la declaración de las funciones. Por ejemplo:

f <- function(x = NULL, y = NULL)
{
  if (!is.null(x) & !is.null(y)){
    print(x+y)
  }else{
    print('faltan valores')
  }
}

f(x = 2, y = 0)
2
f(x = 2)
"faltan valores"
f(y = 0)
"faltan valores"

 

Para ejemplificar esto en un caso real veamos la función rnorm. Esta genera números aleatorios a partir de una distribución normal.

str(rnorm)
function (n, mean = 0, sd = 1)

 

Como vemos, tiene 3 argumento: la cantidad de muestras a generar (n), la media de la distribución (mean) y el desvió estándar (sd). Si indicamos solo un número, este representa la cantidad de muestras a generar, dejando por defecto los otros dos argumentos con valores por defecto (lo que haría que la función genere numero a partir de una distribución normal estándar)

rnorm(3)
-0.2262557 0.6465698 2.3460583
rnorm(3, 1)
-0.8456187 1.4596911 1.1237811
rnorm(3, 1, 10)
2.331487 -19.407747 -11.676089

 

Cuando un argumento es pasado por nombre, este es quitado de la lista de orden de asignación de valores al llamar a una función. De este modo se puede hacer una combinación de asignación de argumentos por posición y por nombre.

rnorm(5, sd = 10, 100) # equivalente a: n = 5, mean = 100, sd = 10
106.67484 91.98517 109.96486 113.93423 97.70806

 

Para indicar los nombre de los argumentos al llamar a una función, R permite que haya coincidencia parcial en estos. De este modo podemos llamar a una funciona e indicar solo una parte del nombre de cada argumento.

rnorm(5, s = 10, m = 100)
101.70763 91.48214 103.48912 107.92323 98.50723 

 

El orden en que se hace la comprobación de argumentos es:

  1. Coincidencia exacta del nombre del argumento
  2. Coincidencia parcial del nombre del argumento
  3. Asignación por posición

En general no es buena practica la utilización de coincidencias parciales, por lo que debemos tratar de evitar su uso.

 

El argumento …

Hay un argumento que tiene un uso especial en R, denominando … (tres puntos). Este tiene la capacidad de capturar todos los valores pasados a la función que no coinciden con ningún argumento. De este modo, podemos pasar a una función una cantidad no prefijada de valores.

sumar_pares <- function(...)
{
  valores <- c(...)
  if(!is.numeric(valores)) return('NaN')
 
  contador <- 0
  for(n in valores){
    if(n%%2 == 0){
      contador <- contador + n
    }
  }
contador
}

sumar_pares(1:10)
30

Como vemos, para trabajar con los valores capturado por … podemos convertirlos a un vector con c(...) o una lista con list(...).

 

Retorno de valores

Las funciones anteriores solamente realizan una seria de pasos y finalizan sin devolver ningún valor. En muchas ocasiones deseamos que las funciones al finalizar su ejecución devuelvan algún valor. Para esto tenemos dos posibilidades.

La primea es hacer que la ultima linea de código evaluada dentro de una función sea el valor que queremos que sea devuelto.

## Función que cuenta la cantidad de vocales en una cadena que
## se pasa como argumento 
contar_vocales <- function(frase) 
{
  cantidad_vocales <- 0 
  frase <- tolower(frase)
  frase <- strsplit(frase, "")[[1]]
 
  for (i in frase)
  {
    if (i %in% c("a", "e", "i", "o", "u"))
    {
      cantidad_vocales <- cantidad_vocales + 1
    } 
  }
  cantidad_vocales 
}

resultado <- contar_vocales("Hola mundo, nuevamente")
resultado
9

 

La segunda alternativa es indicarlo explícitamente mediante el comando return(). En este ultimo caso, cuando se ejecuta esta instrucción dentro de una función, esta finaliza inmediatamente devolviendo el valor indicado. Lo común es reservar esta alternativa para devolver “señales” en caso de que la función tenga inconvenientes. Por ejemplo, en caso de que un argumento no sea consistente con lo esperado podemos devolver algún valor que nos indique de esta situación.

f <- function(<argumentos>) 
{
  if (<alguna_condición>) return(<señal>)
  # Código de la función 
  ...
  x # objeto que devuelve la función 
}

# Ejemplo
calcular_raiz2 <- function(n)
{
  # Verifico que el que número pasado no sea negativo
  if (n < 0) return("Numero negativo")
 
  # En caso de que los argumentos sean consistentes,
  # continuo con la ejecución de la función.
  sqrt(n)
}

calcular_raiz2(2)
1.414214

calcular_raiz2(-2)
"Numero negativo"

 

Reglas de alcance (Scoping Rules)


El ultimo componente de una función es su ambiente (environment). Un ambiente es una colección de pares (símbolo, valor). Por ejemplo, si hacemos x <- 3, esta sentencia crea el par (x, 3) en el ambiente donde se ejecutó la instrucción. Si luego queremos recuperar el valor de la variable x, R busca el valor en el ambiente y encuentra que vale 3. Cada ambiente tiene un ambiente padre, y es posible para un ambiente tener múltiples ambientes hijos. El único ambiente que no tiene padre (raíz) es el empty environment.

De esta forma, el ambiente de una función controla como se encuentra el valor asociado con una variable dentro de ella. Por ejemplo,

f <- function(x) { x + z }

En la mayoría de los lenguajes de programación, esta declaración conduciría a un error debido a que la variable z no está definida dentro de la función. Pero en R esto es valido debido a que utiliza lo que se conoce como lexical scoping para encontrar el valor asociado. Como z no está definida dentro de la función (a estas variables se las conoce como free variables), R irá a buscar su valor en el ambiente donde la función fue definida.

f(10)
Error in f(10) : object 'z' not found

z <- 1
f(10)
11

Básicamente, lo que hace R al necesitar evaluar el valor de z es ir a buscarlo dentro la función f. Como no lo encuentra definido, pasa a buscar el valor de la variable en el ambiente padre. En el primer caso, z sigue sin estar definido, mientras que en el segundo caso sí lo está.

Las reglas de alcance de un lenguaje determinan justamente como un valor es asociado con una variable. Veamos unos ejemplos:

z <- 1

f <- function(x) 
{
  z <- 10
  2 * z + g(x)
}

g <- function(x) 
{
  x * z
}

¿Qué valor devolverá f(5)? Veamos.

Cuando llamamos a la función f lo primero que se evalúa es z <- 10. Esta acción crea un par (z, 10) en el ambiente de la función f. A continuacion se evalúa 2*z + g(x). En este paso, R busca el valor de la variable z en el ambiente de la función f, y encuentra el par (z, 10). Luego busca a la función g, pero no la encuentra definida dentro de la función f, por lo que pasa a buscarla en el ambiente padre y encuentra:

g <- function(x) 
{
  x * z
}

Ahora evalúa g(5), y se encuentra con x * z. Acá vemos claramente que x vale 3, pero ¿qué valor toma la variable z? Como dijimos, R utiliza lexical scoping. Estos significa que se va a buscar el valor de la variable al ambiente donde esta fue definida (y no donde fue llamada). Por lo tanto, el valor de z será 1, y por lo tanto:

f(5)
25

Ahora vemos que sucede si la función g la hubiésemos definido dentro de la función f:

f <- function(x) 
{
  g <- function(x) 
  {
    x * z
  }
  z <- 10
  2 * z + g(x)
}

Lo único que cambia al momento de evaluar f(5) es al momento de que R va a busca el valor de z cuando evalúa g(5). Como ahora g está definida dentro de f, R va a buscar el valor de z en su nuevo ambiente padre, que es justamente el ambiente de la función f, y encuentra que ahora el valor de z es 10, y por lo tanto:

f(5)
70

 

Este comportamiento permite realizar operaciones, sin las cuales muchos paquetes no podrían existir. No obstante, hay que ser cuidadoso al trabajar. Un típico problema aparece cuando creamos una función, la cual no devuelve el valor incorrecto. Luego de debuggear el código nos damos cuenta de que en la función está participando una variable que, sin darnos cuenta, no está definida en la función y toma su valor de otro ambiente. El peor caso surge cuando no nos damos cuenta de que el valor que obtenemos es incorrecto, y dado que no aparec ningún error (porque la función toma su valor de otro ambiente) podemos continuar trabajando y llegar a resultado erróneos más adelante.

 

Comentarios

3 Comments

  1. Maricela

    Si, muchísimas gracias por tu aporte.. me ayudó mucho.
    Saludos.

    Reply
  2. yutaro

    Buen aporte

    Reply
  3. Eduardo

    Muy buen blog, tiene una explicacion sencilla y didactica

    Reply

Submit a Comment

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

This site uses Akismet to reduce spam. Learn how your comment data is processed.