21  FUNCIONES DEFINIDAS POR EL USUARIO

En adición a las funciones que R trae incorporadas en los paquetes que se cargan al inicio o en cualquier otro paquete que se cargue en una sesión específica, el usuario puede crear sus propias funciones para realizar de manera ágil y personalizada labores que ejecute consuetudinariamente.

Tales funciones se denominan funciones definidas por el usuario (user-defined functions) o funciones personalizadas.

¡Créela!

Las funciones definidas por el usuario marcan una gran diferencia entre R y otras aplicaciones para análisis estadístico.

Al ser R un lenguaje de programación, sus posibilidades son prácticamente ilimitadas, no siendo necesario que R “tenga” una función destinada a realizar una tarea particular, puesto que el usuario puede crearla, acorde con sus requerimientos y necesidades.

A continuación se detallan algunas diferencias entre las funciones personalizadas y las funciones oficiales que se descargan desde la CRAN o desde algún otro sitio.

El usuario no tiene que preocuparse por la escritura de las funciones oficiales. De esta parte ya se ha encargado algún otro usuario de R; muchos de ellos con gran experiencia. Las funciones personalizadas sí exigen un proceso de creación o escritura.

Las funciones oficiales siempre están dentro de algún paquete (cf. capítulo 4). Las funciones personalizadas, aunque también podrían empaquetarse, usualmente se manejan en archivos de scripts de R.

Para tener acceso a una función oficial hay que empezar por descargar el paquete que la contiene (cf. sección 4.1). Por su parte, las funciones personalizadas suelen distribuirse en archivos de scritps de R, con extensión .R.

El uso de cualquier función —sea oficial o personalizada— exige que esté disponible en una sesión de trabajo particular. Con excepción de las funciones que forman parte de los paquetes básicos de R —que siempre están disponibles en memoria—, cualquier otra función oficial exige la carga del correspondiente paquete (cf. capítulo 4.2). Esto puede hacerse con alguna de las funciones require o library. Asimismo, puede realizarse una carga temporal (cf. tip 4.1). Las funciones personalizadas se cargan en memoria mediante la función source. En este caso no se carga un paquete sino una función.

Una vez la función esté disponible en memoria, sea porque se haya cargado su paquete contenedor o porque se haya cargado la función con source, el llamado o invocación de cualquiera de estas funciones es exactamente igual.

La tabla 21.1 resume los aspectos diferenciadores de estas dos categorías de funciones.

Tabla 21.1: Comparación entre funciones oficiales y funciones personalizadas en R
Función Escritura Empaquetamiento Distribución Uso
Oficial Ya ha sido escrita por un tercero Se descarga el paquete contenedor Nombre_función(argumentos)
Personalizada La realiza el usuario Usualmente no Archivo de scripts de R Nombre_función(argumentos)


La sintaxis general para crear una función personalizada es:

nombre_función <- function (arg1, arg2, argk, ...)
{
  instrucciones
}

El nombre de la función puede ser cualquier identificador sintácticamente válido. Sin embargo, se desaconseja el uso de nombres de funciones ya existentes en R.

La palabra reservada function permite definir funciones personalizadas. Al llamar la función, mediante nombre_función y los argumentos necesarios, se ejecuta el contenido del bloque de instrucciones que se hayan especificado entre las llaves.

Para ilustrar lo anterior, consideremos una función que evalúa cualquier número entre 2 y 100, para determinar si es primo.

¿Primo?

Recuérdese que un número primo es un número natural mayor que 1, que solo es divisible por 1 y por sí mismo.

Código 21.1
primo <- function(x)
{
  if (!is.numeric(x))
    stop("La función solamente admite valores numéricos")
  if (x != floor(x))
    stop("La función solamente admite números enteros")
  if(x < 2 || x > 100)
    stop("La función solo está implementada para valores entre 2 y 100")
  residuos <- x%%(2:(x-1))
  residuos0 <- residuos == 0
  if (x == 2)
    cat("2 es un número primo")
  else if (sum(residuos0) == 0)
    cat(x, "es un número primo")
  else
    cat(x, "no es un número primo")
}

La línea 1 define la función primo, con argumento x.

El cuerpo de la función está comprendido por todas las instrucciones entre llaves, es decir, las que aparecen entre las líneas 3 y 16.

Los condicionales if que aparecen en las líneas 3, 5 y 7 verifican que el argumento sea numérico, entero y que esté entre 2 y 100, respectivamente. En caso de no satisfacerse alguna de estas condiciones, la función se detiene, imprimiendo un mensaje de error.

Las instrucciones de las líneas 9 y 10 habrían podido escribirse de manera condensada en una sola línea. Sin embargo, las desglosamos para analizar los procedimientos internos.

La línea 9 evalúa el residuo de dividir el valor del argumento x entre cada uno de los elementos de un vector de números enteros que va de 2 a x-1 (cf. sección 20.8). Así, si se estuviera evaluando, por ejemplo, x=7, se calcularía el residuo de 7/2, 7/3, 7/4, 7/5 y 7/6; el vector residuos constaría de los elementos 1, 1, 3, 2, 1.

La instrucción de la línea 10 evalúa cuáles de los elementos de residuos son 0, es decir, cuáles dieron lugar a una división exacta sin residuo. El resultado es un vector de valores lógicos, que se almacena en residuos0. Para el caso con x=7, el vector residuos0 constaría de los elementos F, F, F, F, F.

La estructura if...else if...else (líneas 11 a 16) evalúa diferentes posibilidades, para determinar si el argumento es un número primo o si no lo es.

La línea 11 evalúa si el valor del argumento x es 2. De ser así, aparece el aviso especificado mediante la instrucción de la línea 12.

En caso de que x no sea igual a 2 (y haya pasado todos los filtros iniciales), se evalúa si todas las divisiones realizadas mediante la instrucción de la línea 9 dejaron algún residuo, en cuyo caso, el vector residuos0 estaría conformado únicamente por valores FALSE. Y, aprovechando la relación entre las etiquetas lógicos TRUE/FALSE con los valores 0/1 (cf. sección 10.5), basta con averiguar si la suma de las etiquetas lógicas es 0, en cuyo caso, todas las divisiones evaluadas habrían dejado algún residuo, pudiendo clasificarse, por tanto, el número evaluado como primo.

En caso contrario, es decir, si suma(residuos0) no fuera 0 significaría que el vector residuos0 contenía al menos un valor lógico TRUE, lo que significaría que, al menos una de las divisiones evaluadas no dejó residuo. En consecuencia, el número no se considera primo.

Una vez creada y depurada la función, hay dos maneras de cargarla en memoria.

  1. Ejecutando todas las instrucciones que la definen: líneas 1 a 17.

  2. Guardándola en un archivo con extensión .R y usando la función source.

Cualquiera que sea el método utilizado, es posible verificar si la función fue cargada correctamente, revisando el Global Environment (usualmente en la ventana superior derecha de RStudio). Si la función se ha cargado con éxito, su nombre aparecerá en el apartado Functions.

¿Guardar o no guardar?

Aunque las funciones suelen surgir como solución a problemas puntuales, con frecuencia resultan útiles en contextos distintos al que motivó su creación. En tal sentido, lo más recomendable suele ser guardarlas, para facilitar su reutilización posterior.

No obstante, también habrá casos en los que una función solo tenga sentido dentro de un script específico. En tales situaciones, puede dejarse directamente en el cuerpo del script, sin necesidad de crear un archivo adicional.

¿¡Y qué nombre se le pone al archivo!?

Aunque el archivo contenedor de la función puede tener cualquier nombre admisible por el sistema operativo (no está limitado a los nombres sintácticamente válidos en R), se recomienda usar el mismo nombre que se haya usado para la función.

Aunque R no tendría ningún problema en procesar una función llamada primo que esté contenida en un archivo llamado, por ejemplo, números primos.R, esto probablemente sí generaría confusión en el usuario y daría lugar a posibles errores.

¡Se recomienda usar el mismo nombre para la función y para su archivo contenedor!

Suponiendo que función primo se hubiera guardado en un archivo llamado primo.R, se procedería a cargarla en memoria así:

source("primo.R")

La anterior instrucción lee el contenido del archivo primo.R y ejecuta la totalidad de su contenido, dejando disponible en memoria la función primo.

¡Que esté en la ruta!

Para que la carga de una función mediante source se realice adecuadamente, es necesario que el archivo contenedor de la función esté en el directorio de trabajo (cf. capítulo 3).

El llamado o invocación de una función se realiza de la misma manera que se hace con las funciones oficiales, es decir, usando su nombre y sus argumentos.

Así, por ejemplo, para evaluar si 53 es número primo, se usaría la siguiente instrucción:

primo(53)
53 es un número primo
Nota 21.1: En resumen…

A continuación se resumen los pasos de la creación y uso de la función primo:

  1. Escribir la función en el editor de scripts.

  2. Guardarla en un archivo con extensión .R, idealmente con el mismo nombre de la función (primo.R)

  3. Ubicar el archivo primo.R en el directorio de trabajo y cargarlo en memoria con source("primo.R")

  4. Evaluar la función sobre cualquier número entre 2 y 100, llamándola por su nombre, por ejemplo: primo(53).

Esperamos que próximamente usted escriba sus propias funciones personalizadas. De momento lo invitamos a seguir el proceso para la función primo. El paso 1 puede resolverlo copiando el código 21.1 y pegándolo en el editor de scripts.

O, si lo desea, también puede partir del punto 3, descargando el archivo primo.R:

La escritura de funciones definidas por el usuario es un arte que va de la mano con la programación. El texto de Santana y Mateos (2014) constituye una muy buena referencia en español sobre este tema, manteniéndose en un nivel relativamente básico.

Si se desea profundizar, pueden consultarse textos más especializados como The R Inferno de Burns (2011), quien —mediante un sentido del humor muy particular— establece un paralelo con la Divina Comedia de Dante (¡a quien le agradece por sus útiles comentarios!), presentado un mapa para moverse en el ‘infierno’ que R puede representar para muchos usuarios.

También es recomendable consultar la definición del lenguaje R, la cual es escrita y actualizada por el equipo nuclear de R (2022).

Un excelente recurso para captar la lógica y el estilo de las funciones consiste en revisar funciones existentes en R. Para ver el contenido de una función, en muchas ocasiones basta con escribir el nombre de dicha función y presionar Enter.

Veamos cómo obtener, por ejemplo, el código de la función factor:

factor
function (x = character(), levels, labels = levels, exclude = NA, 
    ordered = is.ordered(x), nmax = NA) 
{
    if (is.null(x)) 
        x <- character()
    nx <- names(x)
    matchAsChar <- is.object(x) || !(is.character(x) || is.integer(x) || 
        is.logical(x))
    if (missing(levels)) {
        y <- unique(x, nmax = nmax)
        ind <- order(y)
        if (matchAsChar) 
            y <- as.character(y)
        levels <- unique(y[ind])
    }
    force(ordered)
    if (matchAsChar) 
        x <- as.character(x)
    levels <- levels[is.na(match(levels, exclude))]
    f <- match(x, levels)
    if (!is.null(nx)) 
        names(f) <- nx
    if (missing(labels)) {
        levels(f) <- as.character(levels)
    }
    else {
        nlab <- length(labels)
        if (nlab == length(levels)) {
            nlevs <- unique(xlevs <- as.character(labels))
            at <- attributes(f)
            at$levels <- nlevs
            f <- match(xlevs, nlevs)[f]
            attributes(f) <- at
        }
        else if (nlab == 1L) 
            levels(f) <- paste0(labels, seq_along(levels))
        else stop(gettextf("invalid 'labels'; length %d should be 1 or %d", 
            nlab, length(levels)), domain = NA)
    }
    class(f) <- c(if (ordered) "ordered", "factor")
    f
}
<bytecode: 0x0000029002c3a270>
<environment: namespace:base>

En otros casos resulta un tanto más complejo obtener el código de una función determinada.

t.test
function (x, ...) 
UseMethod("t.test")
<bytecode: 0x0000029002e9f850>
<environment: namespace:stats>

Este resultado indica que la función de interés es genérica y que usa métodos o subfunciones diferenciadas, dependiendo la clase del primer argumento. En tales casos, casi siempre hay un método por defecto (default), cuyo código es el que se quiere visualizar.

Para ello se usa la siguiente instrucción:

getAnywhere(t.test.default) 

En otros casos, no existe un método por defecto, al que pueda accederse agregando el sufijo default al nombre de la función:

TukeyHSD
function (x, which, ordered = FALSE, conf.level = 0.95, ...) 
UseMethod("TukeyHSD")
<bytecode: 0x0000029002ebd188>
<environment: namespace:stats>

Este resultado indica que para buscar los métodos disponibles, debe usarse la función methods.

methods(TukeyHSD)
[1] TukeyHSD.aov*
see '?methods' for accessing help and source code

Una vez ubicado el método de interés, se obtiene su código mediante la función getAnywhere:

getAnywhere(TukeyHSD.aov)

21.1 Funciones sin argumentos

Cuando todos los argumentos de una función tienen valores por defecto, es posible correrla sin especificar ningún argumento, usando únicamente los paréntesis. Así, por ejemplo, cuando se ejecuta la instrucción box(), se crea una caja, en la que se usan todos los valores por defecto (línea continua, negra, de ancho 1), como las que se muestran en el capítulo 19. Si se deseara otro tipo de caja, podría especificarse, mediante los correspondientes argumentos.

box(col = "red", lty = 3, lwd = 2)

En otros casos, la función se ejecuta siempre sin argumentos, como sucede con la función getwd (cf. sección 3.1).

getwd()

21.2 El argumento especial triple punto “…”

Este argumento tiene dos usos, dependiendo de si ocupa la primera posición o la última. Cuando se ubica en la primera posición, indica que la función tiene un número indefinido de argumentos de la misma índole. Tal es el caso de la función cat, que concatena un número cualquiera de argumentos.

args(cat)
function (..., file = "", sep = " ", fill = FALSE, labels = NULL, 
    append = FALSE) 
NULL

En estas situaciones, puesto que los argumentos adicionales no tienen una posición definida, es necesario nombrarlos siempre que vayan a usarse, no siendo posible asociarlos con la posición (cf. sección 2.4). Así, por ejemplo, si se quisiera usar un separador diferente al que viene por defecto (un espacio en blanco), sería necesario presentarlo explícitamente, mediante el argumento sep.

Asimismo, puede ubicarse este argumento al final de la función, cuando se desea dejar abierta la posibilidad de que el usuario incorpore argumentos adicionales para uso en las funciones internas que forman parte de la función principal. En la función que se presenta en el capítulo 23 se ilustra este aspecto.

Referencias Bibliográficas

Burns, P. 2011. «The R inferno». https://www.burns-stat.com/pages/Tutor/R_inferno.pdf.
R Core Team. 2022. R Language Definition. Vienna, Austria: R Foundation for Statistical Computing. https://cran.r-project.org/doc/manuals/r-release/R-lang.pdf.
Santana, E., J. S. y Mateos. 2014. El arte de programar en R: un lenguaje para la estadística. 1st ed. México: Instituto Mexicano de Tecnología del Agua.