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

11 Comments

  1. sofia

    Hola, soy nueva en R

    tengo un dataframe similar al siguiente:

    Chr orden
    1
    1
    1
    1
    2
    2
    2

    Necesitaria una función que, añada el número de registro dentro de cromosoma

    Como se haria?

    Muchas gracias de antemano

    Reply
  2. Oscar

    como se puede resolver el siguiente problema.

    Escribe una función que dados dos vectores numéricos realice el siguiente cálculo: Contar cuántos números estrictamente positivos aparecerían como resultado de multiplicar (uno por uno) todos los elementos del primer vector por todos los elementos del segundo. Recuerda: Dado un vector x, length(x) nos devuelve el número de elementos de x.

    Reply
  3. Joaquin A

    Mi buena explicación. Se complementa muy bien con las demás presentaciones del blog

    Reply
  4. Tomeu

    Muy buen blog, estoy aprendiendo algo de R y lo he encontrado muy ameno y didáctico.

    Reply
  5. Izás Candil

    Hola soy nuevo en esto de R. En un ejercicio que me pedían creé esta función:
    intercambia <-function(a,b) {
    a <- a + b
    b <- a – b
    a <- a – b
    }
    a <- 1; b <- 2
    intercambia(a,b)
    print(a); print(b)
    No se produce intercambio de valores. Cambiando las variables para que no entren en conflicto, tampoco.
    Al final he añadido a la función un return(c(a,b)) y la llamada retorna el intercambio pero las variables siguen igual.
    Supongo que no he entendido nada. He probado con el operador <<- y tampoco. Si me podéis orientar, os lo agradecería.

    Reply
    • Tomeu

      El ejemplo que comentas de la función intercambia es interesante, pero no está explicado en esta lección. No me extraña que te tenga desconcertado.
      Aquí el lío está en utilizar los mismos identificadores para los parámetros de la función y variables externas. Pero no solo eso, existen dos formas de pasar parámetros en los diferentes lenguajes de programación, una por valor y la otra por referencia. En el paso de parámetros por valor, se crea una copia del valor del argumento en el momento de la llamada, puedes jugar y hacer lo que quieras con la copia, intercambiarlas etc pero siempre estarás manejando una copia, no el original. De ahí que dentro de la función los valores se intercambian, pero al salir de la función, las copias se destruyen y a los originales no les ha pasado nada. Desconozco como o si tiene paso por referencia el lenguaje de programación R, en ese caso no se crearía una copia y se pasaría a la función la dirección de las variables a y b, y todo lo que se hiciera dentro de la función en realidad se estaría haciendo a las variables. El caso que cuentas es el ejemplo típico de un paso de parámetros por valor, y si lo entiendes verás que es lógico.
      El paso por referencia implica que una función que hace algo o devuelve un valor también puede ocasionar efectos colaterales (modif de variables) y eso se intenta evitar en todo lo posible. La función intercambia con paso de parámetros por valor es imposible, se debería realizar sin pasar ningún parámetro y modificar directamente las variables de afuera. El código de la función es también singular ya que es el ejemplo de intercambiar los valores de unas variables sin el uso de una variable auxiliar (no siempre posible).

      Reply
  6. Omar Jesus Hurtado

    se pueden pasar otras funciones como argumento?

    Reply
  7. Ramiro

    Conciso y efectivo
    Gracias

    Reply
  8. Maricela

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

    Reply
  9. yutaro

    Buen aporte

    Reply
  10. 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.