OOP in TOL
La principal diferencia del nuevo TOL 2.0.1 con respecto a sus predecesores será la apuesta por la programación orientada al objeto (OOP).
Se trata de que el usuario pueda definir estructuras de NameBlock, es decir clases virtuales puras en la nomenclatura OOP, con una API mínima para poder usarla como descriptor de argumento en funciones de forma análoga al Struct en Set. Así como a los conjuntos con estructura se les denomina usualmente registros, a los NameBlock con clase les llamaremos instancias.
Miembros y métodos
A los elementos de una estructura se les suele llamar campos y se accede a ellos mediante la sintaxis <nombre_conjunto>-><nombre_campo>
, pero en una clase hay que distinguir los métodos de los miembros, aunque todos ellos se acceden mediante la misma sintaxis <nombre_instancia>::<nombre_campo>
. Un método es una función de una clase con un prototipo o definición conocida y fija, es decir, que el tipo devuelto y los argumentos, incluidos sus nombres, están prefijados de antemano.Todos los demás elementos de una clase se denominarán miembros. Nótese que un elemento de tipo Code
no es un método.
Tanto unos como otros pueden estar definidos desde un principio o pueden estar solamente declarados a la espera de que se implementen posteriormente.
- Declaración de miembro:
Text _.name;
- Declaración de método:
Text getName(Real unused);
- Definición de miembro:
Text _.name = "nobody";
- Definición de método:
Text getName(Real unused) { _.name};
Los métodos deben ser definidos en la propia clase o en alguna de sus clases heredadas y serán almacenados en la propia clase, y por lo tanto sólo existe una copia única para todas las intancias, la cual no se debe alterar bajo ningún concepto, mientras que los miembros se ubican físicamente en las instancias, pueden por tanto ser definidos por estas, y cada instancia tiene su propia copia de forma que los cambios sufridos en una no afectan a las demás.
El miembro privado reservado _this
Todos los NameBlock, sean o no instancia de una clase, tienen un miembro privado llamado _this al que se puede acceder desde todos sus métodos.
//Example of using implicit private member _this NameBlock a = [[ Text _.name = "a"; Text getName(Real unused) { _this::_.name } ]]; //User cannot overwrite _this due to it's a reserved word NameBlock b = [[ Text _.name = "b"; NameBlock _this = a; Text getName(Real unused) { _this::_.name } ]];
Uso de miembros de tipo Code
En OOP no se recomienda el uso de miembros que apunten a funciones asignables como pueden ser los punteros de funciones en C++ o los objetos de tipo Code en TOL. Una función asignable pretende recoger comportamientos polimórficos impredecibles y eso en OOP se trata mediante herencia de métodos virtuales explicados más adelante.
TOL dará un un mensaje de aviso cuando esto ocurra, aunque se permitirá su uso por si fuera necesario en casos extremos.
En tales casos las funciones así utilizadas no tendrán acceso al ámbito de la clase pues no se trata de verdaderos métodos.
Aún más peligroso resulta usar estos miembros de tipo Code dentro de un NameBlock genérico, no instancia de clase, puesto que en ellos es imposible distinguir si fue creado como método o como objeto, por lo que tendrá acceso al ámbito del NameBlock y sólo debería ser asignado a verdaderos métodos del mismo, nunca a objetos externos.
Especialmente dañino resultaría que una misma función externa fuera asignada a miembros de tipo Code de diferentes NameBlock's no globales, o bien que esa función se usara además fuera de ellos, pues ello podría incluso ocasionar una caída de TOL.
Miembros y métodos estáticos
Si la declaración de un elemento de la clase va precedida por la palabra reservada Static, entonces dicho elemento será compartido por tadas las intancias de dicha clase y para referirse a ellos fuera del ámbito de la misma será necesario utilizar la sintaxis <nombre_de_clase>::<nombre_de_elemento>
Clases
Como ya se ha dicho una clase es un tipo de estructura de información especial que engloba datos y funcionaidades asociadas. En TOl los nombres de las clases son palabras reservadas del parser que no pueden ser utilizadas para otro tipo de identificadores y para evitar un uso fortuito y errores difíciles de detectar se obliga a que sus nombres empiecen por el caracter especial @. A partir de la versión 2.0.1 se recomienda también que los Struct
comiencen por @ y desde la 2.0.2 será obligatorio.
Clases primarias
Son aquellas clases que no heredan de ninguna otra previa sino que se definen por sí mismas. En cierta manera, una clase primaria es a un NameBlock lo que un Struct es para un Set: le obliga a seguir una pauta organizativa, aunque los requisitos que conlleva son algo distintos.
Clases estructurales
Llamaremos clase estructural a aquella que no tiene ningún método.
Class @Doc { Text _.name; Text _.description };
Clases no estructurales
Si tiene algún método le diremos no estructural.
Class @Doc { Text _.name; Text _.description; Text getName(Real unused) { _.name } };
Clases virtuales
Una clase no estructural será virtual si tiene algún método no definido, el cual deberá ser definido en alguna clase heredada de esta, o bien en la propia instanciación de objetos.
Class @Doc { Text _.name; Text _.description; Text getName(Real unused) { _.name }; Text getSize(Real unused) };
Clases virtuales puras
Una clase primaria virtual pura es aquella que en la que ningún método tiene una definición por defecto
Class @Vector { Matrix get.column (Anything unused); Real has.timeInfo(Anything unused); TimeSet dating (Anything unused); Date firstDate (Anything unused); Date lastDate (Anything unused) };
Clases heredadas
Es posible crear clases heredadas de otras que implementen métodos o asignen valores por defecto a miembros. También está permitida la herencia múltiple siempre y cuando no exista conflicto entre los métodos y miembros heredados. Si no desea cambiar ninguno de los miembros o métodos por defecto ni añadir nada nuevo, simplemente se listan las clases de las que se quiere heredar. En otro caso se abrirían llaves y se harían las modificaciones o añadidos pertinentes:
//Si no se quiere cambiar nada con herencia múltiple se enumeran las clases //antecesoras Class @VectorDoc: @Vector, @Doc; //Con herencia simple no tiene mucho sentido no cambiar nada //pues sería una simple clonación de tipos Class @VectorDoc.Ser: @VectorDoc { Serie _ser; Matrix get.data (Anything unused) { Tra(SerMat(_ser)) }; Real has.timeInfo(Anything unused) { True }; TimeSet dating (Anything unused) { Dating(_ser) }; Date firstDate (Anything unused) { First(_ser) }; Date lastDate (Anything unused) { Last(_ser) } }; Class @VectorDoc.Mat: @VectorDoc { Matrix _mat; Matrix get.data (Anything unused) { _mat } Real has.timeInfo(Anything unused) { False }; TimeSet dating (Anything unused) { W }; Date firstDate (Anything unused) { UnknownDate }; Date lastDate (Anything unused) { UnknownDate } };
Otros ejemplos de clases
Esta otra sería una clase primaria parcialmente virtual pues tiene algunos miembros y métodos definidos por defecto y otros no
Class @Input { VectorDoc _.data; Real _.enabled = True; Polyn _.omega = 1; Polyn _.delta = 1; Ratio transferFunction (Anything unused) { _.omega / _.delta }; Matrix eval(Polyn omega, Polyn delta) { Polyn _.omega := omega; Polyn _.delta := delta; MatDifEq(transferFunction, _.vector::get.column(0)) } };
Clases anidadas
Una clase puede tener un miembro que es una instancia de otra clase
Class @Output { VectorDoc _.data; Set _.arima };
Predeclaración de clases
Es posible predeclarar una clase sin definir sus métodos ni las clases de las que hereda, para poder ser usada dentro de otra clase que a su vez la precise, tal y como se ilstra en el siguiente ejemplo:
//Forward declaration Class @ClHeight; //This class uses @ClHeight as a method argument Class @ClAge { Text _.name; Real _.age; Real equalName(ClHeight arg) { _.name==arg::_.name } }; //This class uses @ClAge as a method argument Class @ClHeight { Text _.name; Real _.height; Real equalName(@ClAge arg) { _.name==arg::_.name } };
Método destructor
En principio no parece que sea muy necesario el que las clases en TOL dispongan de un mecanismo de destruccion, ya que la memoria no la maneja el usuario sino que el propio lenguaje se encarga de liberar la memoria al salir del ámbito local en la que se alojó.
Pero esa no es la única tarea de un destructor de instancias. Hay cosas como cerrar ficheros o conexiones a bases de datos o liberar recursos manejados por terceras partes, como es el caso de ANN en #823.
Con este motivo se ha reservado un nombre de método oculto especial para ser ejecutado cuando una instancia de clase va a ser destruida. Ha de ser un método con la siguiente API
Real __destroy (Real void)
Hay que hacer notar que los métodos de destrucción, al igual que en C++, no siguen la regla de virtualización sino que se llaman todos los métodos de destrucción que existan a lo largo de la jerarquía de clases antecesoras en el orden inverso de herencia. Gracias a esto un método de destrucción sólo debe ocuparse de los miembros que se definen en su propia clase. En caso de herencia múltiple los destructores de las clases progenitoras se llamarán en el orden en que fueron declaradas.
Instancias
Una instancia de una clase es un NameBlock que cumple la API definida por dicha clase y define al menos todos los miembros sin valor por defecto. Obsérvese que el orden de declaración de los miembros es irrelevante e independiente del orden de herencia de las clases.
@VectorDoc.Ser vd.ser = [[ Text _.name = "Viernes"; Text _.description = "Es 1 los días viernes y 0 el resto"; Serie _ser = CalInd(WD(5),C) ]]; @VectorDoc.Mat vd.mat = [[ Text _.name = "Constante"; Matrix _mat = SetCol(NCopy(1,100)); Text _.description = "Siempre vale 1" ]];
Para ser más exacto sólo esposibe crear instancias de una clase no virtual, es decir, con todos los métodos implementados.
Constructores de instancias
La forma más recomendada de crear instancias de forma regulada es mediante construtores que son métodos estáticos que devuelven una instancia de esa clase a partir de una lista de los argumentos necesarios para la definición del objeto. Los mimebros auxiliares o derivados serán rellenados por el propio constructor.
Evidentemente una misma clase puede tener varios constructores y unos pueden ser especializaciones de otros más generales a los que llamen después de calcular los argumentos necesarios
Class @Circle { //Definition members Real _.center.x_; Real _.center.y_; Real _.radius; //Auxiliar members Real _.perimeter; Real _.area; //Basic Constructor Static @Circle New( Real x, //First coordinate of center Real y, //Second coordinate of center Real r) //Radius { @Circle new = [[ Real _.center.x_ = x; Real _.center.y_ = y; Real _.radius = r; //Auxiliuar members definition Real _.perimeter = 2*Pi*r; Real _.area = Pi*r^2 ]] }; //Derivate constructor Static @Circle Random( Real min.x, Real max.x, Real min.y, Real max.y, Real min.r, Real max.r) { @Circle::New(Rand(min.x,max.x), Rand(min.y,max.y), Rand(min.r,max.r)) }; //Returns true if the point (x,y) is inside the circle Real includes(Real x, Real y) { (x-_.center.x_)^2+(y-_.center.y_)^2<=_.radius^2 }; Real outsides(Real x, Real y){ Not(includes(x,y)) } }; @Circle c1 = @Circle::New(0,0,1); @Circle c2 = @Circle::Random(0,1,-1,0,1/2,1);
Las clases vistas como tipos de usuario
Los nombres de clase funcionarán como tipos definidos por el usuario a todos los efectos, salvo alguna excepción debida a las limitaciones del parser de TOL y que quedarán debidamente documentadas. Es posible entonces declarar argumentos de función de una clase determinada lo cual admitirá cualquier NameBlock que sea instancia de esa clase o cualquier clase heredada de ella.
Matrix sum(@Vector a, @Vector b) { a::get.data(0) + b::get.data(0) }; Matrix c = sum(vd.ser, vd.mat);
Acceso a variables del ámbito global
El ámbito global puede verse en cierta manera como una clase cuyos elementos son todos estáticos y puede accederse a ellos con el operador ::
sin operando izquierdo
Real G = 1; Set aux = { Real G = 2; Real g = ::G [[g, G]] }; Real ok.1 = aux[1]==1; Real ok.2 = aux[2]==2;
De esta manera puede asegurarse que se usa la variable global en lugar de otra local que pueda haber pasado desapercibida, dando lugar a errores difíciles de detctar. Hay sólo una excepción. Por ejemplo, este otro código da un error porque no es viable aplicar el operador de búsqueda global al tipo Anything, debido a que es posible crear variables de distinto tipo con el mismo nombre, y todas ellas serían aceptables dando lugar a una ambigüedad dificil de resolver sin pérdida de eficiencia.
Real G = 1; Set fail = { Real G = 2; [[Real ::G, G]] };