Programación orientada a objetos Ocaña Rebull, Jordi Sánchez Pla, Alex 8 de novembre de 2007 1 Planteamiento y motivación En este capı́tulo se presentan algunos conceptos básicos de programación orientada a objetos (OOP) en R, tomando en consideración solamente el modelo de objetos de R (y S) S4, no el antiguo modelo de objetos conocido habitualmente como S3. Para ello se empieza planteando un problema estadı́stico relacionado con los modelos mixtos y su implementación mediante funciones “estándar” y se muestra cómo, a medida que la situación se complica, la OOP podrı́a ayudar a disponer de una solución elegante y extensible. 1.1 Generación de un modelo lineal mixto simple Supongamos que para realizar un estudio de simulación es preciso generar conjuntos de datos a partir de un modelo lineal mixto sencillo, del estilo de: yi = (α0 + Ai ) + (β0 + Bi )x + i yi = (yi1 , . . . , yim ) representará, en general, un vector de m observaciones de una variable, posiblemente correlacionadas, obtenidas para el “individuo” (o “grupo”) i bajo las condiciones x = (x1 , . . . , xm ). Los valores de x representan, a menudo, instantes sucesivos de tiempo. Dentro de cada individuo se puede considerar que la trayectoria observada depende linealmente de x, pero la función lineal concreta depende de cada individuo. Esta dependencia se modeliza mediante unos parámetros de regresión aleatorios (α0 + Ai β0 + Bi ), con una parte constante, fija, α0 , β0 y una parte que representa una realización de dos variables aleatorias Ai ∼ N (0, σα ), Bi ∼ N (0, σβ ) que por simplicidad se considerarán independientes. Finalmente, i = (i1 , . . . , im ) representan también residuos aleatorios, ij ∼ N (0, σ) mutuamente independientes e independientes de Ai y de Bi . Por ejemplo, el código siguiente genera 4 réplicas independientes (individuos) según el modelo anterior: 1 > > > > > > > > > alfa0 <- 3 beta0 <- 2.5 sAlfa <- 1.5 sBeta <- 2.5 sRes <- 2 nDatos <- 4 x <- 1:10 k <- length(x) x [1] > > > > > 1 2 3 4 5 6 7 8 9 10 set.seed(127) a <- alfa0 + rnorm(nDatos, sd = sAlfa) b <- beta0 + rnorm(nDatos, sd = sBeta) y <- a + x %o% b + matrix(rnorm(nDatos * k, sd = sRes), ncol = nDatos) y [1,] [2,] [3,] [4,] [5,] [6,] [7,] [8,] [9,] [10,] [,1] 7.827101 11.143801 15.695551 22.412437 24.922213 28.517056 33.833978 41.124718 43.033940 45.787576 [,2] 5.503796 12.431905 15.903439 24.408809 29.557917 33.835794 33.612725 39.176276 43.400738 51.105699 [,3] 7.343257 13.508364 18.914433 22.468927 22.495091 24.086158 32.426765 37.865164 40.938131 44.820739 [,4] 7.982633 4.904760 7.058215 10.802503 15.548872 15.585602 16.861647 19.408568 22.363922 23.300257 Los resultados de la generación se muestran en el gráfico. > plot(matrix(x, ncol = nDatos, nrow = k), y, type = "o") 2 50 ● ● ● ● ● 40 ● ● ● ● 30 ● 20 y ● ● ● ● ● ● ● ● ● ● ● ● ● ● 10 ● ● ● ● ● ● ● ● ● ● ● ● 2 4 6 8 10 matrix(x, ncol = nDatos, nrow = k) La utilidad general del código anterior puede mejorarse presentándolo en forma de función: > + + + + + + > > genMix <- function(n = 1, x, alfaFix = 0, betaFix = 0, sigmaAlfa = 1, sigmaBeta = 1, sigmaRes = 1) { a <- alfaFix + rnorm(n, sd = sigmaAlfa) b <- betaFix + rnorm(n, sd = sigmaBeta) a + x %o% b + matrix(rnorm(n * length(x), sd = sigmaRes), ncol = n) } set.seed(127) genMix(nDatos, x, alfa0, beta0, sAlfa, sBeta, sRes) [1,] [2,] [3,] [4,] [5,] [6,] [7,] [8,] [9,] [10,] [,1] 7.827101 11.143801 15.695551 22.412437 24.922213 28.517056 33.833978 41.124718 43.033940 45.787576 [,2] 5.503796 12.431905 15.903439 24.408809 29.557917 33.835794 33.612725 39.176276 43.400738 51.105699 [,3] 7.343257 13.508364 18.914433 22.468927 22.495091 24.086158 32.426765 37.865164 40.938131 44.820739 3 [,4] 7.982633 4.904760 7.058215 10.802503 15.548872 15.585602 16.861647 19.408568 22.363922 23.300257 La necesidad de la OOP surge de forma natural al complicar y generalizar el código, al pretender convertirlo en algo de utilidad general y no algo puramente de “usar y tirar”. Supongamos que deseamos generalizar el código anterior permitiendo que los residuos y/o los factores aleatorios se generen de acuerdo con una distribución absolutamente continua cualquiera. Tenemos varias alternativas (a cual peor): Una posibilidad serı́a crear una función para cada posible combinación de distribuciones escogidas para ambos efectos aleatorios y para los residuos: > + + + + + + > + + + + + + genMixNormNormCauchy <- function(n = 1, x, alfaFix = 0, betaFix = 0, sigmaAlfa = 1, sigmaBeta = 1, sigmaRes = 1) { a <- alfaFix + rnorm(n, sd = sigmaAlfa) b <- betaFix + rnorm(n, sd = sigmaBeta) a + x %o% b + matrix(rcauchy(n * length(x), scale = sigmaRes), ncol = n) } genMixNormCauchyCauchy <- function(n = 1, x, alfaFix = 0, betaFix = 0, sigmaAlfa = 1, sigmaBeta = 1, sigmaRes = 1) { a <- alfaFix + rnorm(n, sd = sigmaAlfa) b <- betaFix + rcauchy(n, scale = sigmaBeta) a + x %o% b + matrix(rcauchy(n * length(x), scale = sigmaRes), ncol = n) } etc... Algunas distribuciones como la gamma plantean dificultades adicionales, es necesario centrarlas y reescalarlas adecuadamente para que tengan sentido como un efecto aleatorio o un residuo: > genMixNormNormGamma <- function(n = 1, x, alfaFix = 0, betaFix = 0, + sigmaAlfa = 1, sigmaBeta = 1, sigmaRes = 1, shape = 1, scale = 1) { + mediaGamma <- shape * scale + sigmaGamma <- sqrt(shape) * scale + a <- alfaFix + rnorm(n, sd = sigmaAlfa) + b <- betaFix + rnorm(n, sd = sigmaBeta) + a + x %o% b + matrix(((rgamma(n * lenth(x), shape = shape, + scale = scale) - mediaGamma)/sigmaGamma), ncol = n) * + sigmaRes + } La proliferación de argumentos empeora si, por ejemplo, intervienen varias gamma, como en una posible genMixNormGammaGamma. Otras distribuciones necesitarı́an sus argumentos particulares... Y ası́ hasta el infinito... Una posibilidad es parametrizar mucho las funciones, es decir, encapsular la generación de estos valores aleatorios en una función que admita como argumento alguna manera de codificar la distribución deseada: 4 > + + + + + + + + + > + + + + + + + genMix <- function(n = 1, x, alfaFix = 0, betaFix = 0, sigmaAlfa = 1, sigmaBeta = 1, sigmaRes = 1, codiDistriAlfa = "norm", codiDistriBeta = "norm", codiDistriRes = "norm", ...) { a <- alfaFix + genera(n, sigma = sigmaAlfa, codiDistri = codiDistriAlfa, ...) b <- betaFix + genera(n, sigma = sigmaBeta, codiDistri = codiDistriBeta, ...) a + x %o% b + matrix(genera(n * length(x), sigma = sigmaRes, codiDistri = codiDistriRes, ...), ncol = n) } genera <- function(n = 1, sigma, codiDistri = "norm", ...) { if (codiDistri == "norm") return(rnorm(n, sd = sigma)) else if (codiDistri == "cauchy") return(rcauchy(n, sigma = sigma)) else if (codiDistri == "gamma") return(escalaGamma(rgamma(n, ...), ...)) } El código utiliza los parámetros “...’ y sigue con una interminable lista de opciones if ... else .... Este enfoque es muy ineficiente tratándose de algo que se va a ejecutar repetidamente. Tampoco es muy elegante la solución para añadir una distribución no prevista previamente. Además esta aproximación implica la proliferación de funciones auxiliares para cada distribución concreta como por ejemplo la función escalaGamma asociada a los generadores de la distribución Gamma > escalaGamma <- function(y, shape, scale) { + media <- shape * scale + desv <- sqrt(shape) * scale + return((y - media)/desv) + } > escalaGamma(3, 2, 1.5) [1] 0 > escalaGamma(3, 4, 1.5) [1] -1 > genera(sigma = 3, codiDistri = "gamma", shape = 4, scale = 1.5) [1] 1.927914 > genera(10, sigma = 3, codiDistri = "gamma", shape = 2, scale = 1.5) [1] -0.22055242 -0.42705329 -1.09947202 0.67747385 -0.79676198 [7] -0.75023007 -1.03941319 -0.79414007 -0.81379519 5 0.05292618 > genera(10, sigma = 2.5) [1] 0.1728964 1.9723468 0.5993931 -0.6624949 [7] -3.4581458 -2.5352884 -2.7833121 -2.1287941 0.4291814 3.4684306 > set.seed(127) > genMix(nDatos, x, alfa0, beta0, sAlfa, sBeta, sRes) [1,] [2,] [3,] [4,] [5,] [6,] [7,] [8,] [9,] [10,] [,1] 7.827101 11.143801 15.695551 22.412437 24.922213 28.517056 33.833978 41.124718 43.033940 45.787576 [,2] 5.503796 12.431905 15.903439 24.408809 29.557917 33.835794 33.612725 39.176276 43.400738 51.105699 [,3] 7.343257 13.508364 18.914433 22.468927 22.495091 24.086158 32.426765 37.865164 40.938131 44.820739 [,4] 7.982633 4.904760 7.058215 10.802503 15.548872 15.585602 16.861647 19.408568 22.363922 23.300257 > genMix(nDatos, x, alfa0, beta0, sAlfa, sBeta, sRes, codiDistriRes = "gamma", + shape = 2, scale = 1.5) [1,] [2,] [3,] [4,] [5,] [6,] [7,] [8,] [9,] [10,] [,1] 7.732842 16.804009 25.846702 33.917161 37.037952 46.285301 56.524983 65.344616 68.832289 76.678248 [,2] 7.89982 11.87682 11.95843 17.45725 24.75232 29.75103 29.01189 34.09549 40.26083 46.40580 [,3] [,4] 3.586113 14.08124 8.641406 24.29313 13.840130 29.11604 16.013519 41.22939 14.909064 52.56894 18.754935 63.03309 25.155685 66.96379 26.981861 81.77348 25.730369 90.66699 32.295908 101.31290 1.2 Una posible solución usando Environments Puede encontrarse una alternativa a la sobreparametrización en funciones de generación utilizando la capacidad de lexical scoping de R, asociada a su concepto de “environment”. Nótese sin embargo que esta solución es aplicable a R y no a otras implementaciones de S, como Splus. > creaGenNormStandard <- function() { + return(function(n) { + rnorm(n, mean = 0, sd = 1) + }) + } 6 > + + + + > + + + + + + creaGenCauchyStandard <- function() { return(function(n) { rcauchy(n, location = 0, scale = 1) }) } creaGenGammaStandard <- function(shape, scale) { media <- shape * scale desv <- sqrt(shape) * scale return(function(n) { (rgamma(n, shape = shape, scale = scale) - media)/desv }) } ... etc Serı́a necesario crear una función creaGen<Distribución>Standard por cada distribución que interesase. Estas funciones no tienen por que utilizar forzosamente las funciones incorporadas en R (rnorm, runif, etc), es decir que podemos basarlas en nuestros propios algoritmos lo que resulta indispensable, por ejemplo para distribuciones no previstas, como por ejemplo para generar la distribución de Laplace. De esta manera la función genera quedarı́a extremadamente corta y general, mediante una llamada a una función “creadora de funciones que generan la distribución estandarizada adecuada”, bastarı́a crear una instancia de una función adecuada para generar la distribución deseada, y pasarla como el argumento stdGenerador: > genera <- function(n = 1, sigma, stdGenerador) { + stdGenerador(n) * sigma + } Ahora una función genMix adecuada y mucho más general serı́a: (por defecto se generan factores aleatorios y residuos normales) > genMix <- function(n = 1, x, alfaFix = 0, betaFix = 0, sigmaAlfa = 1, + sigmaBeta = 1, sigmaRes = 1, stdGenAlfa = creaGenNormStandard(), + stdGenBeta = creaGenNormStandard(), stdGenRes = creaGenNormStandard()) { + a <- alfaFix + genera(n, sigmaAlfa, stdGenAlfa) + b <- betaFix + genera(n, sigmaBeta, stdGenBeta) + a + x %o% b + matrix(genera(n * length(x), sigmaRes, stdGenRes), + ncol = n) + } El código siguiente muestra como se utilizarı́an estas funciones: > rn <- creaGenNormStandard() > rn 7 function (n) { rnorm(n, mean = 0, sd = 1) } <environment: 0x01b2b218> > rn(10) [1] -0.45618218 [7] -0.84810970 0.35550018 0.50101130 -1.32365771 -1.08500359 -0.01433392 0.91355252 -1.34965843 0.89109798 > rg <- creaGenGammaStandard(shape = 4, scale = 1.5) > rg function (n) { (rgamma(n, shape = shape, scale = scale) - media)/desv } <environment: 0x01a43f8c> > rg(10) [1] [7] 0.5508530 -1.0356482 -0.8366514 -0.6474291 -0.7156479 -0.6152633 0.1404982 0.4379852 -0.6353478 -0.8292362 > set.seed(237) > rg(10) [1] -0.02529546 -0.26041851 [7] 0.60631690 0.92880150 0.71239415 -1.42826501 -0.18677365 -0.93835653 0.47756935 0.52375029 > set.seed(237) > (rgamma(10, shape = 4, scale = 1.5) - 6)/3 [1] -0.02529546 -0.26041851 [7] 0.60631690 0.92880150 0.71239415 -1.42826501 -0.18677365 -0.93835653 0.47756935 0.52375029 > set.seed(127) > genMix(nDatos, x, alfa0, beta0, sAlfa, sBeta, sRes) [1,] [2,] [3,] [4,] [5,] [6,] [7,] [8,] [9,] [10,] [,1] 7.827101 11.143801 15.695551 22.412437 24.922213 28.517056 33.833978 41.124718 43.033940 45.787576 [,2] 5.503796 12.431905 15.903439 24.408809 29.557917 33.835794 33.612725 39.176276 43.400738 51.105699 [,3] 7.343257 13.508364 18.914433 22.468927 22.495091 24.086158 32.426765 37.865164 40.938131 44.820739 8 [,4] 7.982633 4.904760 7.058215 10.802503 15.548872 15.585602 16.861647 19.408568 22.363922 23.300257 > rgRes <- creaGenGammaStandard(shape = 2, scale = 1.5) > set.seed(127) > genMix(nDatos, x, alfa0, beta0, sAlfa, sBeta, sRes, stdGenRes = rgRes) [1,] [2,] [3,] [4,] [5,] [6,] [7,] [8,] [9,] [10,] [,1] 7.081417 10.407223 15.020807 24.369851 23.622664 28.139039 37.823764 37.823510 41.143164 47.833136 [,2] 13.18403 15.30297 15.28664 19.50172 26.36411 37.07451 37.63681 41.30462 54.37624 52.52497 [,3] 6.484199 8.277999 13.994917 18.934581 23.165119 27.352900 34.568167 40.241594 41.669352 44.071954 [,4] 2.366396 5.786712 7.079205 9.936757 14.069186 15.848008 16.310054 16.908162 23.333111 25.289833 > rgAlfa <- creaGenGammaStandard(shape = 1.5, scale = 2.5) > genMix(nDatos, x, alfa0, beta0, sAlfa, sBeta, sRes, stdGenAlfa = rgAlfa, + stdGenRes = rgRes) [1,] [2,] [3,] [4,] [5,] [6,] [7,] [8,] [9,] [10,] [,1] 3.993835 9.523836 7.049225 9.779640 11.828346 14.919247 13.213827 16.448773 16.713026 25.196619 [,2] -3.843840 -6.028921 -9.491458 -11.824187 -10.732692 -23.736369 -25.956210 -30.783663 -36.060808 -38.436519 [,3] 9.248763 10.976857 13.661364 22.053730 25.858836 30.400311 32.190840 35.218472 40.815992 50.213557 [,4] 0.2703673 4.6747903 5.0825236 3.7583501 5.7498410 5.0696633 8.2649067 9.8778705 4.5316417 7.4920335 > rgBeta <- creaGenCauchyStandard() > genMix(nDatos, x, alfa0, beta0, sAlfa, sBeta, sRes, stdGenAlfa = rgAlfa, + stdGenBeta = rgBeta, stdGenRes = rgRes) [,1] [,2] [1,] 122.9116 4.493933 [2,] 241.0379 9.305928 [3,] 362.3312 9.962917 [4,] 478.9690 10.452612 [5,] 603.6862 15.918488 [6,] 721.2250 14.550723 [7,] 845.6951 19.276152 [8,] 961.3749 22.790262 [9,] 1082.3863 23.212232 [10,] 1200.2598 26.671966 [,3] 3.5066204 -0.2756740 1.9882551 -0.9024622 3.2437591 -2.1378910 -0.2557622 -4.7738150 -1.4563197 -1.7727417 9 [,4] 7.539529 9.921513 29.076111 24.711437 30.109254 33.740766 42.986800 44.164043 51.620688 54.678262 El principal inconveniente de esta solución, basada en el concepto R de environment, es su poca portabilidad a otras implementaciones de S. También encontrarı́amos dificultades crecientes al querer dotarla de otras capacidades, como la posibilidad de controlar de alguna manera las secuencias de números aleatorios. Al fin y al cabo, esta solución se basa en el empleo de unos “objetos” que se asocian a la definición de toda función en R y que “encapsulan” información referente al entorno en el que fue creada. Parece más razonable una solución que nos permita un mayor control sobre dichos objetos (o similares), la cual, como parece lógico, seguramente se halla en la “programación orientada a objetos”, OOP. 2 2.1 Primera aproximación a la OOP Clases y objetos Retomando las ideas anteriores, nuestro objetivo es crear una función cuya estrucura ya está bastante clara: > genMix <- function(n = 1, x, alfaFix = 0, betaFix = 0, sigmaAlfa = 1, + sigmaBeta = 1, sigmaRes = 1, distriAlfa, distriBeta, distriRes) { + a <- alfaFix + generaStd(distriAlfa, n) * sigmaAlfa + b <- betaFix + generaStd(distriBeta, n) * sigmaBeta + a + x %o% b + matrix(generaStd(distriRes, n * length(x)) * + sigmaRes, ncol = n) + } Dentro de genMix, mediante la función generaStd, se darı́a la orden de generar n valores estandarizados de acuerdo con la distribución deseada, para posteriormente multiplicarlos por la escala adecuada. Esta distribución corresponderı́a al valor de los argumentos distriAlfa, distriBeta o distriRes de genMix, “algo” (¡un objeto!) que tendrı́a que representar una distribución continua, con sus parámetros, con toda la información necesaria. Evidentemente, quien mejor podrı́a conocer los detalles internos de cómo generar(se) serı́a este “objeto” distribución, no la función generaStd. Un primer paso es definir nuevas clases de datos, que representen distribuciones. De momento, parece razonable suponer que la información más significativa asociada a una distribución son sus parámetros y los correspondientes valores de dichos parámetros: La creación de nuevas clases se realiza mediante la instrucción setClass. Haciendo ? setClass se obtiene ayuda sobre su funcionamiento. > setClass("Normal", representation(media = "numeric", sigma = "numeric"), + prototype(media = 0, sigma = 1)) [1] "Normal" 10 > setClass("Cauchy", representation(mediana = "numeric", escala = "numeric"), + prototype(mediana = 0, escala = 1)) [1] "Cauchy" > setClass("Gamma", representation(forma = "numeric", escala = "numeric", + media = "numeric", sigma = "numeric"), prototype(forma = 1, + escala = 1)) [1] "Gamma" La función setClass admite numerosos argumentos. De momento solamente nos centraremos en los tres primeros. El primero, Class, es indispensable. Especifica el nombre de la clase que se está creando. El segundo, representation especifica la representación, la estructura en el sentido de los “slots” o campos de datos, que van a tener los “objetos” o “instancias” concretas de la clase. En una primera aproximación, estos “slots” se pueden asimilar a los campos de una lista de un objeto de la clase predefinida list. El tercer argumento, prototype, especifica los valores iniciales de los campos en cualquier instancia u objeto recién creado (a no ser que, de las posibles maneras que luego discutiremos, explı́citamente indiquemos que van a tener otro valor distinto). El valor asignado a los argumentos representation y prototype es en realidad una lista o algo convertible en una lista. De todas maneras lo habitual es emplear las funciones representation y prototype (que tienen el mismo nombre que los correspondientes argumentos de setClass) y que generan representaciones y prototipos de una forma ordenada, con la sintaxis indicada en los ejemplos, es decir, indicando la clase de cada slot (predefinida, como numeric en los ejemplos, o una nueva clase definida en otra llamada a setClass) o el valor inicial de cada slot, respectivamente. Es decir, una llamada a setClass como: > setClass(Class = "Normal", representation = representation(media = "numeric", + sigma = "numeric"), prototype = prototype(media = 0, sigma = 1)) [1] "Normal" serı́a equivalente, pero más prolija, a la del ejemplo inicial. Los argumentos representation y prototype son opcionales. Si se prescinde de prototype (y de otras maneras de especificar valores iniciales que veremos más adelante) o determinados slots no se incluyen en prototype, dichos slots tienen el valor “nulo” propio de su clase, a no confundir con el valor “cero”. Por ejemplo, si en la definición de la clase Normal se hubiese prescindido de prototype, el valor del campo media serı́a numeric(0), un vector numérico de longitud 0, que evidentemente no es lo mismo que media = 0. Aunque pueda parecer de momento extraño, una clase puede carecer también de representación. Para crear una variable que contenga datos de aquella clase, es decir, una variable cuyo nombre represente a una “instancia” u “objeto” emplearemos la función new. Ası́, para crear una instancia de la clase Normal indicaremos: 11 > normal <- new("Normal") > normal An object of class "Normal" Slot "media": [1] 0 Slot "sigma": [1] 1 Otra instancia de la clase “Normal”, ahora de parámetros -1 y 3 se crearı́a ası́: > normal2 <- new("Normal", media = -1, sigma = 3) > normal2 An object of class "Normal" Slot "media": [1] -1 Slot "sigma": [1] 3 etc. Para acceder a los campos de un objeto de cierta clase, emplearemos el operador @, de forma similar a como emplearı́amos el operador $ para listas. (Cuidado: AMBOS OPERADORES NO SON INTERCAMBIABLES. @ sirve para acceder a los campos de un objeto de una clase, $ se utiliza en listas.) > normal@sigma [1] 1 > normal2@sigma [1] 3 > normal2@media [1] -1 Hay una importante diferencia entre @ y $, y entre el concepto de lista y de clase y sus instancias: en una lista, la definición de los “slots” o “campos” está asociada a cada variable concreta (varias listas que tuviesen exactamente los mismos campos tendrı́an cada una de ellas una copia de las definiciones de estos campos), en cambio, las instancias de una clase definida mediante setClass no contienen la definición de estos campos, esta definición está asociada a la clase. Obsérvese que si los slots que definen una clase y sus instancias son de una clase ya definida por R (numeric, list, etc), si al crear un objeto no se les ha asignado un valor inicial mediante prototype, o mediante argumentos 12 a new o mediante un procedimiento más elaborado que veremos más adelante (sobreescribir el método initialize), dichos slots tienen valor inicial “nulo” (lo que signifique dicho valor para cada clase): fijémonos en el valor de media y sigma de los objetos de clase Gamma: > cauchy <- new("Cauchy") > cauchy An object of class "Cauchy" Slot "mediana": [1] 0 Slot "escala": [1] 1 > gamm <- new("Gamma") > gamm An object of class "Gamma" Slot "forma": [1] 1 Slot "escala": [1] 1 Slot "media": numeric(0) Slot "sigma": numeric(0) > gamm2 <- new("Gamma", forma = 2, escala = 3) > gamm2 An object of class "Gamma" Slot "forma": [1] 2 Slot "escala": [1] 3 Slot "media": numeric(0) Slot "sigma": numeric(0) Es evidente que todavı́a queda trabajo por hacer para que la clase Gamma tenga un comportamiento adecuado. 13 2.2 Los métodos describen el comportamiento de las clases De momento las clases y los objetos anteriores sirven de bien poco, solamente para almacenar los valores de parámetros, y para eso ya tenemos las listas. Hay que asociarlas a comportamientos. Para ello, en primer lugar se define una “función genérica” generaStd: > setGeneric("generaStd", function(distri, ...) standardGeneric("generaStd")) [1] "generaStd" setGeneric es una función que crea otras funciones, similar a ejemplos que hemos visto anteriormente. La llamada es un poco rara pero es la manera más cómoda y segura de crear funciones “genéricas”. La llamada anterior ha creado una función genérica de nombre generaStd. Esta función en realidad solamente es una especie de interfaz abstracta. Espera recibir un argumento, distri, y posibles argumentos adicionales, de momento sin concretar (...). Según cual sea la clase real de distri, se ejecutará una función especı́fica, un “método” que implementará esta función genérica para cada distribución concreta. El código siguiente muestra cómo se utiliza la función setMethod para crear el método generaStd asociado a distri de clase Normal: > setMethod("generaStd", signature("Normal"), function(distri, + n = 1) { + rnorm(n) + }) [1] "generaStd" De manera análoga, para las otras clases: > setMethod("generaStd", signature("Cauchy"), function(distri, + n = 1) { + rcauchy(n) + }) [1] "generaStd" > setMethod("generaStd", signature("Gamma"), function(distri, n = 1) { + (rgamma(n, shape = distri@forma, scale = distri@escala) + distri@media)/distri@sigma + }) [1] "generaStd" 14 Los tres primeros argumentos de setMethod, los más importantes, corresponden al nombre del método, a su “signatura” y a la definición de la función que realmente va a implementar el método. La signatura establece a que clase (o a que clases, ya que el modelo de objetos S4 implementa “despachado múltiple”, una caracterı́stica que no trataremos de momento) se asocia el método. getMethods("generaStd") lista los métodos asociados esta función genérica. Finalmente veamos cómo se utiliza la función genérica generaStd con objetos de clases ’Normal’ y ’Cauchy’: > generaStd(normal, 25) [1] 0.20759794 0.35422699 -0.04298372 -0.52484457 0.41128229 -0.56953489 [7] -0.94738810 -0.40783714 0.01902878 0.50074461 0.82340699 0.72844425 [13] -1.65343978 -2.17757446 0.02775334 -0.49487201 -0.83308335 2.43341694 [19] 0.03756152 0.91065071 -0.46258200 1.81222765 -0.19598847 -0.72454848 [25] 0.83344619 > generaStd(normal2) [1] 0.4505594 > generaStd(cauchy, 10) [1] -0.479049541 6.472337442 [6] 1.353520511 -0.006085034 1.856003970 -4.831934684 0.924744519 0.859242677 3.074901767 -3.251580586 Si se emplea del depurador de R o simplemente se intercala una instrucción print con un mensaje adecuado en la definición de los métodos definidos mediante SetMethod, se puede comprobar que en realidad generaStd(normal, 25) se ha traducido en la ejecución del método generaStd asociado a la clase Normal mientras que generaStd(cauchy, 10) ha ejecutado el correspondiente método de la clase Cauchy. De momento lo anterior no funcionarı́a con la clase Gamma: > generaStd(gamm) numeric(0) > generaStd(gamm2, 5) numeric(0) El problema está en que los slots media y sigma no tienen un valor adecuado: > gamm@media numeric(0) 15 > gamm2@sigma numeric(0) Para solucionarlo, debemos saber algo más de la inicialización de instancias de una clase. Resumiendo los conceptos anteriores, el código básico necesario para crear una clase y sus métodos consiste en: Cada funcionalidad que se desea asociar con una o más clases debe corresponder a una función genérica. Cada una de ellas se define mediante una llamada a setGeneric. La funcionalidad definida en abstracto mediante la función genérica será posteriormente implementada de forma explı́cita en cada clase, al definir sus métodos asociados. Mediante una llamada a la función setClass que define cada clase y su estructura. Mediante setMethod se definen los métodos especı́ficos que implementan las funcionalidades definidas genéricamente en setGeneric. El código siguiente ilustra estos pasos para la clase Normal: > setGeneric("generaStd", function(distri, ...) standardGeneric("generaStd")) [1] "generaStd" > setClass("Normal", representation(media = "numeric", sigma = "numeric"), + prototype(media = 0, sigma = 1)) [1] "Normal" > setMethod("generaStd", signature("Normal"), function(distri, + n = 1) { + rnorm(n) + }) [1] "generaStd" Si la única funcionalidad deseada es generaStd, al crear nuevas clases sólo nos hará falta definirlas mediante setClass y definir sus métodos especı́ficos para generaStd mediante setMethod. > setClass("Cauchy", representation(mediana = "numeric", escala = "numeric"), + prototype(mediana = 0, escala = 1)) [1] "Cauchy" > setMethod("generaStd", signature("Cauchy"), function(distri, + n = 1) { + rcauchy(n) + }) 16 [1] "generaStd" > setClass("Gamma", representation(forma = "numeric", escala = "numeric", + media = "numeric", sigma = "numeric"), prototype(forma = 1, + escala = 1)) [1] "Gamma" > setMethod("generaStd", signature("Gamma"), function(distri, n = 1) { + (rgamma(n, shape = distri@forma, scale = distri@escala) + distri@media)/distri@sigma + }) [1] "generaStd" 2.3 Métodos de inicialización Como ya se ha indicado, los problemas detectados con la clase Gamma residen principalmente en que los slots media y sigma no tienen un valor inicial adecuado. De nada sirve darles valor inicial fijo mediante prototype, ya que estos slots tienen un valor que debe calcularse a partir de forma y escala (en realidad solamente enmascaramos un error; ahora funcionará, pero mal): > setClass("Gamma", representation(forma = "numeric", escala = "numeric", + media = "numeric", sigma = "numeric"), prototype(forma = 1, + escala = 1, media = 1, sigma = 1)) [1] "Gamma" > gamm2 <- new("Gamma", forma = 2, escala = 3) > gamm2 An object of class "Gamma" Slot "forma": [1] 2 Slot "escala": [1] 3 Slot "media": [1] 1 Slot "sigma": [1] 1 > generaStd(gamm2, 5) [1] 1.3746805 8.3677438 0.9432062 4.4109584 8.1955630 17 Por desgracia, no son admisibles expresiones como la siguiente, en prototype: > setClass("Gamma", + representation( + forma = "numeric", + media = "numeric", + ), + prototype(forma = 1, + ) Error in .prototype(...) > escala = "numeric", sigma = "numeric" escala = 1, media = forma*escala, sigma = sqrt(forma)*escala) : object "forma" not found Una solución obvia serı́a inicializar “a mano” media y sigma al crear el objeto: > gamm2 <- new("Gamma", forma = 2, escala = 3, media = 2 * 3, sigma = sqrt(2) * + 3) > gamm2 An object of class "Gamma" Slot "forma": [1] 2 Slot "escala": [1] 3 Slot "media": [1] 6 Slot "sigma": [1] 4.242641 > generaStd(gamm2, 5) [1] -0.30678167 3.10735412 0.07901555 -0.52918493 0.66619903 Aunque es una solución muy poco adecuada: obliga al usuario de la clase y de la función genérica a acordarse de (y a aplicar) las expresiones de la media y de la desviación tı́pica de una gamma. Demasiado complicado y propenso a errores Otra solución obvia estarı́a en calcular media y sigma dentro del método generaStd: > setMethod("generaStd", signature("Gamma"), function(distri, n = 1) { + media <- distri@forma * distri@escala + sigma <- sqrt(distri@forma) * distri@escala + (rgamma(n, shape = distri@forma, scale = distri@escala) + media)/sigma + }) 18 [1] "generaStd" > gamm <- new("Gamma") > gamm An object of class "Gamma" Slot "forma": [1] 1 Slot "escala": [1] 1 Slot "media": [1] 1 Slot "sigma": [1] 1 > gamm2 <- new("Gamma", forma = 2, escala = 3) > gamm2 An object of class "Gamma" Slot "forma": [1] 2 Slot "escala": [1] 3 Slot "media": [1] 1 Slot "sigma": [1] 1 > generaStd(gamm) [1] -0.931548 > generaStd(gamm2, 5) [1] 0.3203107 1.0549065 0.5712505 0.6911388 -0.2689783 La generación funciona bien a pesar de los valores absurdos de los slots media y sigma. En realidad no se utilizan, ahora media y sigma son variables locales del método; bajo este enfoque podrı́amos prescindir completamente de los slots correspondientes. Lo anterior funcionarı́a pero no es lo más eficiente, es mejor disponer de slots media y sigma ya previamente calculados (de una vez por todas, al crear el objeto), no perder tiempo recalculándolos cada vez que se activa el método. 19 Una solución mucho mejor es crear un método initialize. Dicho método forzosamente debe tener este nombre, initialize, y forzosamente debe tener un primer argumento de nombre .Object seguido de los argumentos que necesitarı́a para una adecuada inicialización -los mismos que pondrı́amos en new. > setMethod("initialize", signature("Gamma"), function(.Object, + forma = 1, escala = 1) { + .Object@forma <- forma + .Object@escala <- escala + .Object@media <- forma * escala + .Object@sigma <- sqrt(forma) * escala + .Object + }) [1] "initialize" El método initialize NO se suele llamar directamente a través de su función genérica (es decir, no se escribe initialize(distri, etc) si no que se llama automáticamente al hacer new, con los argumentos que se indicaron a new después del nombre de la clase. En realidad lo que hemos hecho ha sido substituir un método initialize, que R crearı́a por defecto, por el nuestro propio. Al crear un objeto mediante new, en primer lugar se ejecuta lo indicado en prototype (si existe) y a continuación se ejecuta initialize. Ambas formas de inicialización pueden coexistir, pero a menudo un método initialize adecuado hace innecesario prototype. Ciertamente en la definición de la clase Gamma podemos suprimir prototype: > setClass("Gamma", representation(forma = "numeric", escala = "numeric", + media = "numeric", sigma = "numeric")) [1] "Gamma" ... y devolver el método generaStd a su forma original: > setMethod("generaStd", signature("Gamma"), function(distri, n = 1) { + (rgamma(n, shape = distri@forma, scale = distri@escala) + distri@media)/distri@sigma + }) [1] "generaStd" > gamm <- new("Gamma") > gamm An object of class "Gamma" Slot "forma": [1] 1 20 Slot "escala": [1] 1 Slot "media": [1] 1 Slot "sigma": [1] 1 > gamm2 <- new("Gamma", forma = 2, escala = 3) > gamm2 An object of class "Gamma" Slot "forma": [1] 2 Slot "escala": [1] 3 Slot "media": [1] 6 Slot "sigma": [1] 4.242641 > generaStd(gamm) [1] 0.3192866 > generaStd(gamm2, 5) [1] 1.44488888 -0.04631383 0.18740485 -0.28510912 -1.15902442 initialize tal como está definido anteriormente nos protege de errores de definición de slots: > gamm2 <- new("Gamma", forma = 2, escala = 3, media = 1, sigma = 1) Error in .local(.Object, ...) : unused argument(s) (media = 1, sigma = 1) > Ahora no es posible (erróneamente) modificar slots no previstos como argumentos de initialize. 2.4 Validez de las instancias de una clase La mejor forma de controlar que los objetos creados mediante new sean instancias válidas de su clase es la creación de un método de validación. Se puede asociar un método de validación a una clase en el momento de crearla, mediante el argumento validity de setClass, o con posterioridad a la creación de la clase. En el momento de definir la clase serı́a: 21 > setClass("Gamma", representation(forma = "numeric", escala = "numeric", + media = "numeric", sigma = "numeric"), validity = function(object) { + if ((object@forma <= 0) || (object@escala <= 0)) + return("Los parámetros de una Gamma deben ser positivos") + else return(TRUE) + }) [1] "Gamma" > gamm <- new("Gamma", escala = -1) > validObject(gamm) Error in validObject(gamm) : invalid class "Gamma" object: Los parámetros de una Gamma deben ser positivos > Con posterioridad a la definición de la clase serı́a: > setClass("Gamma", representation(forma = "numeric", escala = "numeric", + media = "numeric", sigma = "numeric")) [1] "Gamma" > setValidity("Gamma", function(object) { + if ((object@forma <= 0) || (object@escala <= 0)) + return("Los parámetros de una Gamma deben ser positivos") + else return(TRUE) + }) Slots: Name: forma escala media sigma Class: numeric numeric numeric numeric > gamm <- new("Gamma", escala = -1) > validObject(gamm) Error in validObject(gamm) : invalid class "Gamma" object: Los parámetros de una Gamma deben ser positivos > Nótese que un método de validación debe devolver un valor lógico TRUE si todo ha ido bien, o una o más cadenas de caracteres si ha habido algún problema. Nótese también que new no llama automáticamente al método de validación, hay que hacerlo explı́citamente mediante la función validObject. El método initialize creado por R automáticamente a partir de la cláusula prototype de setClass llama al método de validación automáticamente, en cambio nuestra initialize tiene que llamar explı́citamente dicho método, mediante validObject: 22 > setMethod("initialize", signature("Gamma"), function(.Object, + forma = 1, escala = 1) { + .Object@forma <- forma + .Object@escala <- escala + .Object@media <- forma * escala + .Object@sigma <- sqrt(forma) * escala + validObject(.Object) + .Object + }) [1] "initialize" > setValidity("Normal", function(object) { + if (object@sigma < 0) + return("La desviación estándar no puede ser negativa") + else return(TRUE) + }) Slots: Name: media sigma Class: numeric numeric > setValidity("Cauchy", function(object) { + if (object@escala < 0) + return("El parámetro de escala no puede ser negativo") + else return(TRUE) + }) Slots: Name: mediana escala Class: numeric numeric > normal <- new("Normal", sigma = -2) Error in validObject(.Object) : invalid class "Normal" object: La desviación estándar no puede ser negativa > Nótese que para las clases Normal y Cauchy, para las cuales no se ha definido explı́citamente initialize, dentro de new se activa automáticamente el método de validación, cosa que no ocurre con Gamma. En esta clase necesitamos llamar explı́citamente validObject. 2.5 Recapitulación del código anterior Mezclar definiciones de clases y métodos con código que ejecuta instrucciones con estas clases y métodos (como estamos haciendo aquı́) es algo muy confuso 23 y propenso a errores. Aunque R (o S) no nos impone ninguna restricción en este sentido, es mejor procurar organizar las definiciones en ficheros aparte, de forma similar a lo propuesto en el fichero classesDistr1.r. Para probarlo serı́a recomendable dejar el área de trabajo en blanco: > rm(list = ls()) y a continuación procesar dicho fichero: > source("classesDistr1.r") > ls() [1] "generaStd" Finalmente podemos comprobar que el código anterior funciona razonablemente bien: > genMix <- function(n = 1, x, alfaFix = 0, betaFix = 0, sigmaAlfa = 1, + sigmaBeta = 1, sigmaRes = 1, distriAlfa = new("Normal"), + distriBeta = new("Normal"), distriRes = new("Normal")) { + a <- alfaFix + generaStd(distriAlfa, n) * sigmaAlfa + b <- betaFix + generaStd(distriBeta, n) * sigmaBeta + a + x %o% b + matrix(generaStd(distriRes, n * length(x)) * + sigmaRes, ncol = n) + } > genMix(5, x = 1:10) [1,] [2,] [3,] [4,] [5,] [6,] [7,] [8,] [9,] [10,] > > > > > > > > [,1] 2.0697840 -0.7607013 1.4491276 -1.1827059 -0.1283908 0.9900569 1.4123577 1.3320416 -1.2460791 -1.1112227 [,2] [,3] [,4] 2.277017 0.2158243 2.0184514 4.043109 -0.4983071 -2.5836715 7.838791 -0.2435911 3.1205457 5.734360 -5.9404333 -0.3997516 8.582717 -6.9567295 -1.7199717 14.630149 -3.0219838 2.4031796 13.814263 -7.6887180 2.3002664 14.564912 -7.4611774 2.4285752 15.234465 -11.9306318 2.4311031 17.600898 -11.8033975 -0.0928880 [,5] 1.497798 2.213557 4.306410 2.832477 1.949985 6.520411 6.596844 9.678610 6.067136 8.018764 alfa0 <- 3 beta0 <- 2.5 sAlfa <- 1.5 sBeta <- 2.5 sRes <- 2 nDatos <- 5 x <- 1:10 genMix(nDatos, x, alfa0, beta0, sAlfa, sBeta, sRes) 24 [1,] [2,] [3,] [4,] [5,] [6,] [7,] [8,] [9,] [10,] [,1] 2.455668 7.954966 5.381788 15.944346 18.490598 15.096101 22.641620 27.662357 30.937799 29.076595 [,2] 2.124608 7.835064 10.810310 12.712536 12.225715 11.060056 15.168481 21.374174 22.309157 21.728526 [,3] 3.592765 4.102866 7.785552 6.734521 6.529494 11.441761 13.337335 11.023567 10.200572 16.334061 [,4] 6.532528 9.092708 14.344240 14.491853 21.779590 22.985215 28.580660 36.863379 34.328924 37.173899 [,5] 0.1596465 2.1254504 -4.5172319 -3.3507111 -3.9570104 -3.6273872 -3.0148443 -7.2811764 -2.4567795 -8.7206779 > genMix(nDatos, x, alfa0, beta0, sAlfa, sBeta, sRes, distriRes = new("Gamma", + forma = 4, escala = 2)) [,1] [,2] [,3] [,4] [,5] [1,] 5.828626 4.085989 8.81076441 8.641802 7.128532 [2,] 4.596794 14.146652 3.72308561 20.660674 11.869066 [3,] 4.493374 8.581463 2.59470386 24.234933 15.709161 [4,] 7.521117 13.191167 1.01267708 31.475014 15.653900 [5,] 7.578287 16.236669 1.18592002 39.819608 19.739553 [6,] 6.802906 20.311844 2.06658199 48.358449 25.939607 [7,] 8.748973 24.225701 4.30246307 53.151488 31.828219 [8,] 7.316266 24.686034 -3.40939134 60.232537 34.361657 [9,] 12.809737 26.882822 1.64069669 65.170838 31.782248 [10,] 10.915724 30.924208 -0.06626341 73.497244 36.444531 > alfas <- new("Cauchy") > resids <- new("Gamma", forma = 4, escala = 2) > genMix(nDatos, x, alfa0, beta0, sAlfa, sBeta, sRes, distriAlfa = alfas, + distriRes = resids) [1,] [2,] [3,] [4,] [5,] [6,] [7,] [8,] [9,] [10,] [,1] 16.290104 7.421282 11.322315 14.231306 23.223106 33.860229 23.974477 29.022385 29.394093 40.065105 [,2] 13.881975 4.268405 10.529196 12.121528 17.185228 29.922209 19.402730 23.379986 25.723306 36.658547 [,3] 14.641736 2.539107 1.684035 4.477441 10.968053 19.883294 13.648584 7.717834 10.234341 15.072369 [,4] 19.32294 16.26572 22.41910 35.28531 39.67759 57.82842 52.58550 61.86270 69.14995 82.18378 [,5] 20.05648 12.90337 18.33077 26.96214 34.18940 49.22408 44.56638 50.89345 57.58529 68.78922 >badGamma <- new("Gamma", forma = -1, escala = 3) Error in validObject(.Object) : invalid class "Gamma" object: Los parámetros de una Gamma deben ser positivos 25 In addition: Warning message: NaNs produced in: sqrt(forma) 2.6 Unas clases distribución algo más completas Para finalizar esta parte, vamos a crear unas clases que implementarán distribuciones y que tendrán unas funcionalidades algo más ricas. Si nos fijamos en las funciones que R proporciona para manejar distribuciones, parece claro que se considera que las funcionalidades básicas asociadas a este concepto son el cálculo de la función de densidad, el cálculo de la función de distribución, el cálculo de cuantiles y la generación de valores aleatorios. Vamos a reproducir este esquema en las clases a definir. En realidad, “generar un valor estandarizado” puede ser interesante pero no parece tanto una competencia básica de estas clases, aunque la mantendremos. Similarmente podrı́amos pensar en multitud de competencias (en el fondo, funciones genéricas y sus correspondientes métodos) interesantes: cálculo de la esperanza, cálculo de la varianza, función de verosimilitud y un largo etc. En general un buen diseño implica, entre otras cosas, una cierta contención en las competencias asociadas a cada clase. Posiblemente las clases distribución se podrı́an limitar a las cuatro competencias listadas antes, otros cálculos, tales como generar valores estandarizados y operadores diversos sobre distribuciones, podrı́an ser competencia de otras clases que interaccionasen con las clases “distribución”. Como nuestro propósito es introducirnos en la orientación a objetos (OOP), y no impartir un curso de análisis y diseño OOP, no consideraremos estas posibilidades. Fijémonos en que casi todo consistirá en crear nuevas funciones genéricas e implementarlas en cada clase concreta mediante los métodos correspondientes. > setGeneric("dens", function(distri, ...) standardGeneric("dens")) [1] "dens" > setGeneric("distri", function(distri, ...) standardGeneric("distri")) [1] "distri" > setGeneric("cuantil", function(distri, ...) standardGeneric("cuantil")) [1] "cuantil" > setGeneric("genera", function(distri, ...) standardGeneric("genera")) [1] "genera" > setGeneric("generaStd", function(distri, ...) standardGeneric("generaStd")) [1] "generaStd" A continuación es necesario definir las clases y los métodos asociados a ellas, que implementan las funciones genéricas: 26 > setClass("Normal", representation(media = "numeric", sigma = "numeric"), + prototype(media = 0, sigma = 1)) [1] "Normal" > setValidity("Normal", function(object) { + if (object@sigma < 0) + return("La desviación estándar no puede ser negativa") + else return(TRUE) + }) Slots: Name: media sigma Class: numeric numeric > setMethod("dens", signature("Normal"), function(distri, x) { + dnorm(x, mean = distri@media, sd = distri@sigma) + }) [1] "dens" > setMethod("distri", signature("Normal"), function(distri, x) { + pnorm(x, mean = distri@media, sd = distri@sigma) + }) [1] "distri" > setMethod("cuantil", signature("Normal"), function(distri, p) { + qnorm(p, mean = distri@media, sd = distri@sigma) + }) [1] "cuantil" > setMethod("genera", signature("Normal"), function(distri, n = 1) { + rnorm(n, mean = distri@media, sd = distri@sigma) + }) [1] "genera" > setMethod("generaStd", signature("Normal"), function(distri, + n = 1) { + rnorm(n) + }) [1] "generaStd" > setClass("Cauchy", representation(mediana = "numeric", escala = "numeric"), + prototype(mediana = 0, escala = 1)) 27 [1] "Cauchy" > setValidity("Cauchy", function(object) { + if (object@escala < 0) + return("El parámetro de escala no puede ser negativo") + else return(TRUE) + }) Slots: Name: mediana escala Class: numeric numeric > setMethod("dens", signature("Cauchy"), function(distri, x) { + dcauchy(x, location = distri@mediana, scale = distri@escala) + }) [1] "dens" > setMethod("distri", signature("Cauchy"), function(distri, x) { + pcauchy(x, location = distri@mediana, scale = distri@escala) + }) [1] "distri" > setMethod("cuantil", signature("Cauchy"), function(distri, p) { + qcauchy(p, location = distri@mediana, scale = distri@escala) + }) [1] "cuantil" > setMethod("genera", signature("Cauchy"), function(distri, n = 1) { + rcauchy(n, location = distri@mediana, scale = distri@escala) + }) [1] "genera" > setMethod("generaStd", signature("Cauchy"), function(distri, + n = 1) { + rcauchy(n) + }) [1] "generaStd" > setClass("Gamma", representation(forma = "numeric", escala = "numeric", + media = "numeric", sigma = "numeric")) [1] "Gamma" 28 > setValidity("Gamma", function(object) { + if ((object@forma <= 0) || (object@escala <= 0)) + return("Los parámetros de una Gamma deben ser positivos") + else return(TRUE) + }) Slots: Name: forma escala media sigma Class: numeric numeric numeric numeric > setMethod("initialize", signature("Gamma"), function(.Object, + forma = 1, escala = 1) { + .Object@forma <- forma + .Object@escala <- escala + .Object@media <- forma * escala + .Object@sigma <- sqrt(forma) * escala + validObject(.Object) + .Object + }) [1] "initialize" > setMethod("dens", signature("Gamma"), function(distri, x) { + dgamma(x, shape = distri@forma, scale = distri@escala) + }) [1] "dens" > setMethod("distri", signature("Gamma"), function(distri, x) { + pgamma(x, shape = distri@forma, scale = distri@escala) + }) [1] "distri" > setMethod("cuantil", signature("Gamma"), function(distri, p) { + qgamma(p, shape = distri@forma, scale = distri@escala) + }) [1] "cuantil" > setMethod("genera", signature("Gamma"), function(distri, n = 1) { + rgamma(n, shape = distri@forma, scale = distri@escala) + }) [1] "genera" > setMethod("generaStd", signature("Gamma"), function(distri, n = 1) { + (rgamma(n, shape = distri@forma, scale = distri@escala) + distri@media)/distri@sigma + }) 29 [1] "generaStd" El fichero classesDistr1Complet.r contiene todas las definiciones anteriores. El siguiente código realiza distintas pruebas sobre este código: > > > > > > normal <- new("Normal") normal2 <- new("Normal", media = -1, sigma = 3) cauchy <- new("Cauchy") gamm <- new("Gamma") gamm2 <- new("Gamma", forma = 2, escala = 3) dens(normal, 0) [1] 0.3989423 > distri(normal, 0) [1] 0.5 > cuantil(normal, 0.5) [1] 0 > genera(normal, 10) [1] -0.37374819 [7] 1.56906982 0.64942865 -0.29007155 0.53817866 -0.70395722 0.06967089 0.71524575 -1.56264541 2.59823846 > dens(normal2, -1) [1] 0.1329808 > distri(normal2, -1) [1] 0.5 > cuantil(normal2, 0.5) [1] -1 > genera(normal2, 10) [1] [6] -3.17392355 2.38978188 -0.04667653 -1.39782558 -2.86283696 -10.08171511 > dens(cauchy, 0) [1] 0.3183099 > distri(cauchy, 0) [1] 0.5 30 2.20209959 -2.50947288 -2.63208891 -2.12430239 > cuantil(cauchy, 0.5) [1] -6.123032e-17 > genera(cauchy, 10) [1] [7] 2.5553133 -1.8795099 0.5129506 -0.5512028 0.9686760 -0.1901093 -0.4878202 0.4135581 0.3192422 2.5662124 > dens(gamm, 1) [1] 0.3678794 > distri(gamm, 1) [1] 0.6321206 > cuantil(gamm, 0.5) [1] 0.6931472 > genera(gamm, 10) [1] 0.94230581 1.46280818 1.28735629 0.41175790 0.07865940 3.63623306 [7] 0.04200269 1.11798145 0.26932689 1.51616092 > dens(gamm2, 1) [1] 0.07961459 > distri(gamm2, 1) [1] 0.04462492 > cuantil(gamm2, 0.5) [1] 5.035041 > genera(gamm2, 10) [1] 3.830175 8.138981 1.469722 [8] 18.901635 13.584714 10.818630 3 5.574794 4.531878 3.465197 Si es OOP tiene herencia En los ejemplos anteriores no hemos utilizado una de las caracterı́sticas definitorias y más útiles de la OOP (y también la más sobreutilizada). Nos estamos refiriendo a que entre clases se puede establecer una relación que a veces se la denomina “herencia”, otras veces “especialización” y curiosamente otras veces “generalización” –de hecho no es ninguna contradicción, depende de en qué sentido se considere dicha relación. También se habla a veces de “extensión”, en el sentido que una clase que especializa a otra lo hace, normalmente, añadiendo slots y métodos. Otro término frecuente es el inglés “subclassing”, de difı́cil traducción, entre otras maneras de denominar este concepto. 31 8.659923 3.1 Una normal es una absolutamente continua Tras un curso de estadı́stica general bien aprovechado, en general queda claro que la Gamma es una distribución absolutamente continua, de la misma manera que lo son la Normal y la distribución de Cauchy. En otros términos, las tres distribuciones anteriores son especializaciones (o derivan) del concepto de distribución absolutamente continua (o a la inversa, dicho concepto generaliza los de Gamma, Cauchy y Normal). También es razonable decir que estas tres distribuciones “heredan” las caracterı́sticas de “absolutamente continua”, en el sentido de que todo acción (¡función genérica y/o método!) que tenga sentido para una absolutamente continua, deberá continuar teniendo sentido para cualquiera de ellas. Y no solamente para las acciones, todo slot o campo con información relevante en una hipotética clase “absolutamente continua” tendrı́a que tener sentido en los objetos de sus clases derivadas. Es por lo tanto razonable que una clase como Gamma herede todos los slots y métodos que se hayan definido en la clase más general, es decir, que sus instancias posean dichas caracterı́sticas. En general esto es ası́ y funciona bien. En ocasiones se tienen algunas situaciones paradójicas que casi siempre se pueden asociar a un mal uso de la relación de herencia. El fichero classesDistr2.r ilustra los conceptos anteriores en R. En este ejemplo se introduce la relación de herencia en la cláusula representation de las clases derivadas. Fijémonos en el siguiente fragmento de código: > setClass("DistribucionAbsContinua") [1] "DistribucionAbsContinua" > setClass("Normal", representation("DistribucionAbsContinua", + media = "numeric", sigma = "numeric"), prototype(media = 0, + sigma = 1)) [1] "Normal" La construcción representation("DistribucionAbsContinua", media = "numeric", etc indica que la clase Normal“extiende”DistribucionAbsContinua, ya que incorpora todas sus funcionalidades (en este primer ejemplo, bien pocas) añadiendo otras nuevas. Otra forma válida de indicar lo anterior serı́a mediante el argumento contains de setClass: > setClass("Normal", representation(media = "numeric", sigma = "numeric"), + prototype(media = 0, sigma = 1), contains = "DistribucionAbsContinua") [1] "Normal" Un ejemplo de lo anterior está en el siguiente código: 32 > > > > > > source("classesDistr2.r") normal <- new("Normal") normal2 <- new("Normal", media = -1, sigma = 3) cauchy <- new("Cauchy") gamm <- new("Gamma") gamm2 <- new("Gamma", forma = 2, escala = 3) Aparentemente poco ha cambiado, aunque algunas instrucciones, si las hubiésemos utilizado en secciones anteriores, darı́an una respuesta algo diferente: > getAllSuperClasses(getClass("Gamma")) [1] "DistribucionAbsContinua" > superClassDepth(getClass("Normal")) $label [1] "DistribucionAbsContinua" $depth [1] 1 $ext $ext$DistribucionAbsContinua An object of class "SClassExtension" Slot "subClass": [1] "Normal" Slot "superClass": [1] "DistribucionAbsContinua" Slot "package": [1] ".GlobalEnv" Slot "coerce": function (from, strict = TRUE) from <environment: namespace:methods> Slot "test": function (object) TRUE <environment: namespace:methods> Slot "replace": function (from, to, value) { 33 if (!is(value, "DistribucionAbsContinua")) stop(gexttextf("the computation: as(object,\"%s\") <- value is valid when object has "DistribucionAbsContinua", "Normal", "DistribucionAbsContinua", class(value)), domain = NA) value } Slot "simple": [1] TRUE Slot "by": character(0) Slot "dataPart": [1] FALSE Slot "distance": [1] 1 > getClass("Cauchy") Slots: Name: mediana escala Class: numeric numeric Extends: "DistribucionAbsContinua" > class(getClass("Cauchy")) [1] "classRepresentation" attr(,"package") [1] "methods" > isVirtualClass("DistribucionAbsContinua") [1] TRUE Instrucciones como las anteriores permiten cierta “introspección” en las clases y sus instancias, lo que posibilita cierto grado de programación en el propio lenguaje, como la creación de clases “sobre la marcha”. No profundizaremos en este tema. 3.2 Clases virtuales Tal como se ha podido comprobar mediante isVirtualClass, DistribucionAbsContinua es una clase “abstracta” o “virtual”, como clase existe y puede tener interés como 34 generalización de otras (por ejemplo en el sentido que veremos luego), pero no es instanciable, no existen objetos de dicha clase. En el fichero classesDistr3.r tenemos un ejemplo más interesante del uso de la herencia: La clase abstracta DistribucionAbsContinua posee un método que ya es operativo para calcular la función de distribución. Simplemente implementa la definición general de función de distribución absolutamente continua. Por otra parte se ha añadido una nueva clase, Exponencial (también especialización directa de DistribucionAbsContinua, decisión que revisaremos más adelante), para la cual NO se ha definido el método distri: > setClass("DistribucionAbsContinua", representation("VIRTUAL")) [1] "DistribucionAbsContinua" > setMethod("distri", signature("DistribucionAbsContinua"), function(distri, + x) { + f <- function(x) { + dens(distri, x) + } + integrate(f, -Inf, x)$value + }) [1] "distri" > setClass("Exponencial", representation("DistribucionAbsContinua", + escala = "numeric")) [1] "Exponencial" > setValidity("Exponencial", function(object) { + if (object@escala <= 0) + return("Los parámetros de una Exponencial deben ser positivos") + else return(TRUE) + }) Slots: Name: escala Class: numeric Extends: "DistribucionAbsContinua" > setMethod("dens", signature("Exponencial"), function(distri, + x) { + dexp(x, rate = 1/distri@escala) + }) [1] "dens" 35 > setMethod("cuantil", signature("Exponencial"), function(distri, + p) { + qexp(p, rate = 1/distri@escala) + }) [1] "cuantil" > setMethod("genera", signature("Exponencial"), function(distri, + n = 1) { + rexp(n, rate = 1/distri@escala) + }) [1] "genera" > setMethod("generaStd", signature("Exponencial"), function(distri, + n = 1) { + (rexp(n, rate = 1/distri@escala) - distri@escala)/distri@escala + }) [1] "generaStd" En cambio vemos que dicho método funciona perfectamente para instancias de esta clase, gracias a que lo ha “heredado” de DistribucionAbsContinua: > source("classesDistr3.r") > expo <- new("Exponencial", escala = 2) > expo An object of class "Exponencial" Slot "escala": [1] 2 > dens(expo, 2) [1] 0.1839397 > distri(expo, 2) [1] 0.6321206 > distri(expo, -1) [1] 0 Lo anterior ilustra un aspecto fundamental de la OOP: el método distri tiene signatura DistribucionAbsContinua. Es decir, se ha definido para esta clase. Pero la llamada a la función genérica distri con un argumento de clase Exponencial ha conducido a una llamada al método distri de DistribucionAbsContinua. 36 distri(expo, etc se ha “resuelto” (o “despachado”) mediante la búsqueda de un método adecuado en la jerarquı́a de clases, desde clases más especializadas a clases más generales. Como la clase Exponencial carecı́a de dicho método, se ha activado el método del mismo nombre, el más especializado posible, que se ha encontrado en la jerarquı́a de clases. En este caso el primero encontrado ha sido el definido para DistribucionAbsContinua. Lógicamente, un método distri propio de la clase Exponencial serı́a mucho más eficiente, al no tener que recurrir a la integración numérica. Nótese el empleo de la clase VIRTUAL en la definición de la clase Exponencial. setClass("DistribucionAbsContinua", representation("VIRTUAL")) Si se especifica que una clase desciende directamente de VIRTUAL se indica explı́citamente que es virtual, es decir, que no es instanciable, no se pueden crear objetos a partir de esta clase. En el ejemplo anterior no serı́a necesario emplear VIRTUAL ya que una clase que no desciende explı́citamente de ninguna otra y que tiene representation nula, es por defecto virtual. Pero resulta más claro indicarlo de este modo. 3.3 Métodos de acceso a los slots El siguiente código, aparentemente inocuo: > gamm <- new("Gamma") > gamm@escala <- 10 > gamm An object of class "Gamma" Slot "forma": [1] 1 Slot "escala": [1] 10 Slot "media": [1] 1 Slot "sigma": [1] 1 ha “destrozado” la estructura interna de gamm: a pesar de tener ahora parámetros forma = 4 y escala = 10, su media y sigma siguen siendo 8 y 4. El comportamiento de este objeto será errático, como puede comprobarse, por ejemplo, al hacerle generar valores según generaStd -y es probable que no nos demos cuenta de nada. La raı́z de este problema está en la imposibilidad de proteger u ocultar slots, de controlar de alguna manera el acceso a los slots. (setClass posee 37 un argumento access, seguramente pensado para futuras extensiones en este sentido del modelo de objetos S4, pero que de momento no se utiliza.) Por el momento, sólo queda la alternativa de pedir, implorar, exigir... que no se acceda directamente a los slots (mediante el operador @) y que al crear clases se definan métodos de acceso a los slots, para que no tenga que hacerse directamente. Dichos métodos se supone que velarán por mantener la coherencia interna de los objetos. En algunos lenguajes es práctica habitual designar estos métodos mediante los prefijos “get” y “set”. Por ejemplo, en la clase Gamma, el método para conocer el valor del slot forma se denominarı́a getForma y el método para modificarlo se denominarı́a setForma. En S/R la tradición es designar estos métodos mediante nombres como forma y forma<- respectivamente, es decir consultar el valor del slot mediante el mismo nombre del slot (pero ahora empleado como una función) y modificarlo mediante un método denominado mediante el propio nombre del slot seguido de <-. Esta sintaxis permite que funcionen automáticamente las tı́picas construcciones R del estilo forma(gamm) <- 2.0. En el fichero classesDistr.r se han creado accesores para los slots de las distintas clases. Primero se han definido las correspondientes funciones genéricas. Por ejemplo, las funciones genéricas relacionadas con la clase Gamma serı́an: > setGeneric("forma", function(distri) standardGeneric("forma")) [1] "forma" > setGeneric("forma<-", function(distri, value) standardGeneric("forma<-")) [1] "forma<-" > setGeneric("escala", function(distri) standardGeneric("escala")) [1] "escala" > setGeneric("escala<-", function(distri, value) standardGeneric("escala<-")) [1] "escala<-" A continuación se definen los correspondientes métodos. La creación de los métodos de consulta es trivial. La creación de los métodos de modificación suele hacerse indirectamente, mediante una llamada a la función setReplaceMethod: > setMethod("forma", signature("Gamma"), function(distri) return(distri@forma)) [1] "forma" > setReplaceMethod("forma", signature("Gamma", "numeric"), function(distri, + value) { + if (value > 0) { + distri@forma <- value + distri@media <- value * distri@escala 38 + + + + + }) distri@sigma <- sqrt(value) * distri@escala } else stop("El parámetro 'forma' de una Gamma debe ser positivo") return(distri) [1] "forma<-" > setMethod("escala", signature("Gamma"), function(distri) return(distri@escala)) [1] "escala" > setReplaceMethod("escala", signature("Gamma", "numeric"), function(distri, + value) { + if (value > 0) { + distri@escala <- value + distri@media <- distri@forma * value + distri@sigma <- sqrt(distri@forma) * value + } + else stop("El parámetro 'escala' de una Gamma debe ser positivo") + return(distri) + }) [1] "escala<-" Ahora se puede modificar el valor de un campo de las instancias de Gamma, de forma segura: > gamm <- new("Gamma") > gamm An object of class "Gamma" Slot "forma": [1] 1 Slot "escala": [1] 1 Slot "media": [1] 1 Slot "sigma": [1] 1 > escala(gamm) <- 10 > gamm 39 An object of class "Gamma" Slot "forma": [1] 1 Slot "escala": [1] 10 Slot "media": [1] 10 Slot "sigma": [1] 10 > escala(gamm) [1] 10 > forma(gamm) <- 4 > gamm An object of class "Gamma" Slot "forma": [1] 4 Slot "escala": [1] 10 Slot "media": [1] 40 Slot "sigma": [1] 20 3.4 Uso y abuso de la herencia ¿Debe Exponencial descender de Gamma? O por el contrario ¿Debe Gamma descender de Exponencial? ¿O ya está bien tal como lo hemos definido en los ejemplos anteriores? Hay razones a favor y en contra de cada una de estas opciones, pero lo que sigue apunta a que relacionar ambas clases mediante la relación de herencia más bien es un ejemplo de mal uso de dicha relación. A priori hay razones para considerar la herencia para relacionar ambas clases: Una distribución exponencial de parámetro escala ES una Gamma de parámetros forma = 1 y escala. Por lo tanto la distribución exponencial se podrı́a considerar una especialización de la Gamma. Pero lo anterior choca con la manera en que habitualmente los lenguajes de programación (S/R incluido) implementan la herencia. Exponencial heredará TODOS los slots de Gamma y 40 ahora el slot forma más bien sobra (por ejemplo, podrı́a dar problemas modificar accidentalmente su valor a algo distinto de 1). En vista de lo anterior, por el hecho de que una exponencial quedarı́a perfectamente definida por un solo slot, escala, y que para definir una Gamma necesitarı́amos un slot adicional, forma, podrı́amos pensar en hacer descender la clase Gamma de Exponencial (que a su vez descenderı́a directamente de DistribucionAbsContinua). Esta ambigüedad procede de que la manera habitual de concebir la herencia es una mezcla de dos cosas en realidad no coincidentes. Por un lado tenemos el concepto abstracto de que “un A es un B” y por otro lado el hecho de que “toda la información que definı́a B también va a definir A”. A veces más bien quisiéramos eliminar información que poseı́a B. Examinemos con más detalle las dos opciones planteadas para relacionar las clases Gamma y Exponencial: Definición de Exponencial como descendiente de Gamma: El código completo de esta opción está definido en classesDistr2muchHerencia1.r. Parece a priori una forma cómoda de especializar Gamma. Una razón para tomar esta decisión de diseño podrı́a ser aprovechar la mayorı́a de métodos de Gamma, que tendrı́an que seguir funcionando adecuadamente para “gammas de parámetro ‘forma’ igual a 1”. Parece que bastarı́a modificar la comprobación de validez de las instancias y añadir posibles métodos más especı́ficos, como noMemoria. Aparentemente todo funciona bien: > source("classesDistr2muchHerencia1.r") > expo <- new("Exponencial", escala = 2) > expo An object of class "Exponencial" Slot "forma": [1] 1 Slot "escala": [1] 2 Slot "media": [1] 2 Slot "sigma": [1] 2 > dens(expo, 2) [1] 0.1839397 > distri(expo, 2) [1] 0.6321206 41 > distri(expo, -1) [1] 0 > noMemoria(expo) [1] "Cumple que Pr{X > t+x | X > t} = Pr{X > x} para t > 0, x > 0" > expoTonta <- new("Exponencial", forma = 1, escala = 2) > dens(expoTonta, 2) [1] 0.1839397 > distri(expoTonta, 2) [1] 0.6321206 > distri(expoTonta, -1) [1] 0 > noMemoria(expoTonta) [1] "Cumple que Pr{X > t+x | X > t} = Pr{X > x} para t > 0, x > 0" > expoMala <- new("Exponencial", forma = 2) Error in validObject(.Object) : invalid class "Exponencial" object: Los parámetros de una Gamma deben ser positivos > expoMuyMala <- new("Exponencial", forma = 2, escala = -2) Error in validObject(.Object) : invalid class "Exponencial" object: Los parámetros de una Gamma deben ser positivos Pero hay riesgo de realizar cálculos erróneos y difı́ciles de detectar: > forma(expo) <- 100 > distri(expo, 2) [1] 3.981281e-159 Estos problemas se corregirı́an en gran parte (re)definiendo los correspondientes métodos (distri, etc) en la clase Exponencial. Basta quitar el código comentado en classesDistr2muchHerencia1.r. Pero procediendo de esta manera no resulta evidente ninguna ventaja en hacer descender Exponencial de Gamma, a no ser que complicar las cosas sea una “ventaja”. Muy poco código de Gamma se puede reutilizar en Exponencial. Otra razón por la que el anterior diseño de clases es una mala opción es que no permite que un método de Exponencial que se base o utilice una propiedad caracterı́stica de dicha distribución, se pueda utilizar también para una Gamma adecuada, es decir con parámetro forma igual a 1: 42 > expo <- new("Exponencial", escala = 2) > noMemoria(expo) [1] "Cumple que Pr{X > t+x | X > t} = Pr{X > x} para t > 0, x > 0" > gamm <- new("Gamma", forma = 1, escala = 2) >noMemoria(gamm) Error in function (classes, fdef, mtable) : unable to find an inherited method for function "noMemoria", for signature "Gamma" Definición de Gamma como descendiente de Exponencial El fichero classesDistr2muchHerencia2.r contiene el código completo de esta opción. Ya de entrada parece claro que no se consigue mucha reutilización de código. > source("classesDistr2muchHerencia2.r") Otenemos un código algo más seguro: > expoTonta <- new("Exponencial", forma = 1, escala = 2) Error in .local(.Object, ...) : unused argument(s) (forma = 1) Que admite un uso lógico del método ’noMemoria’ > expo <- new("Exponencial", escala = 2) > noMemoria(expo) [1] "Cumple que Pr{X > t+x | X > t} = Pr{X > x} para t > 0, x > 0" > gamm <- new("Gamma", forma = 1, escala = 2) > noMemoria(gamm) [1] "Cumple que Pr{X > t+x | X > t} = Pr{X > x} para t > 0, x > 0" Si bien también hace cosas inadecuadas: > gamm <- new("Gamma", forma = 4, escala = 2) > noMemoria(gamm) [1] "Cumple que Pr{X > t+x | X > t} = Pr{X > x} para t > 0, x > 0" El método ’noMemoria’ tendrı́a que ser exclusivamente de Exponencial aunque serı́a deseable que, bajo control, también tuviese sentido para ciertos objetos de clase Gamma. Una solución a lo anterior es el empleo de métodos ’as’ que realizan la “coerción” de un objeto “obligándolo” explı́citamente a adaptarse a otra clase. Los = setAs( from = métodos “as” se suelen definir mediante la función setAs: ”Gamma”, to = ”Exponencial”, def = function(from) if (from@forma != 1) stop( ”Sólo es posible convertir una Gamma de parámetro forma = 1 en Exponencial”) new(”Exponencial”, escala = from@escala) ) El resultado de la anterior ejecución de setAs es la creación de un método que permite forzar que una instancia de Gamma se convierta en una instancia de Exponencial: 43 > source("classesDistr.r") > extends("Gamma", "Exponencial") [1] FALSE > extends("Exponencial", "Gamma") [1] FALSE > gamm <- new("Gamma", forma = 1, escala = 2) > inherits(gamm, "Gamma") [1] TRUE > inherits(gamm, "Exponencial") [1] FALSE > noMemoria(gamm) Error in function (classes, fdef, mtable) : unable to find an inherited method for function "noMemoria", for signature "Gamma" > noMemoria(as(gamm, "Exponencial")) [1] "Cumple que Pr{X > t+x | X > t} = Pr{X > x} para t > 0, x > 0" > gamm <- new("Gamma", forma = 4, escala = 2) > as(gamm, "Exponencial") Error in asMethod(object) : Sólo es posible convertir una Gamma de parámetro forma = 1 en Exponencial De esta manera tenemos más bajo control la posible relación entre Gamma y Exponencial. 44