Programación orientada a objetos. Polimorfismo.

José Javier Blanco Rivero

Como la palabra misma lo sugiere, polimofirsmo nos indica la capacidad de adoptar varias formas. En el mundo de las ciencias de la computación, el concepto de polimorfismo es uno de los pilares del paradigma orientado a objetos y se refiere a una forma de reusar código, que consiste en añadirle a un objeto la capacidad exhibir distintas conductas según el contexto. Una palabra clave en este dominio es la de <despachar>, ya que alude justamente a la acción de entregar algún pedido, y concretamente en programación, remite a la capacidad de retornar el valor adecuado según sea el caso.

Existen tres tipos de polimorfismo, a saber, de subtipos, paramétrico y ad hoc (algunos agregan un cuarto: el casteo, es decir, la conversión dinámica de un tipo a otro).

El polimorfismo de subtipos tiene lugar en tiempo de ejecución y existen dos vertientes del mismo, a saber, el single dispatch y el multiple dispatch. El primero es su forma más común, y básicamente, lo que despacho simple quiere decir es que, cuando el programa se está ejecutando, se decide qué versión del método se va a invocar basándose en el tipo de la referencia del objeto. Por ejemplo, si creamos un objeto x y usando la notación de punto llamamos a un método, del que hay varios con el mismo nombre (pongamos: x.métodoCualquiera() ), el compilador ubicará la versión correcta a invocarse basándose en el tipo de dato que x tiene en tiempo de ejecución (tipo que puede no ser el mismo con el que x se declaró y que recogió el compilador).

Despacho múltiple, en contraposición, consiste en despachar el método correcto teniendo en cuenta el tipo de todos los parámetros o argumentos. Por ejemplo, si tenemos los siguientes métodos homónimos: métodoCualquiera(String a, Integer b, Boolean c), métodoCualquiera(), métodoCualquiera(String x, String y, String z), el compilador o intérprete decidirá cuál es el correcto basándose en el número, orden y tipo de datos; digamos que mi llamado es métodoCualquiera("Hola", "Hello", "Ciao"), entonces el método o función que se invocará es el último.

Dado que la técnica de despacho múltiple no se fundamenta en el tipo de dato de la referencia del objeto, sino en la firma del método, nótese que los lenguajes que usan esta técnica pueden, por lo general, prescindir de la notación de punto (e.g. Julia).

Tanto el polimorfismo paramétrico como el ad hoc tienen lugar en tiempo de compilación, esto es, cuando el compilador chequea el código y lo traduce en código de máquina o en bytecode (como es el caso de la JVM). A diferencia del polimorfismo de subtipos, que es dinámico, estas clases de polimorfismo pueden servirse de las técnicas de mejoramiento del desempeño que aplica el compilador. El precio a pagar es la pérdida de la flexibilidad y el dinamismo que se ganan al modificar la conducta del código en tiempo de ejecución.

El polimorfismo paramétrico le confiere al programador, en un lenguaje con tipado estático, la capacidad de escribir código en forma general y abstracta. Ya hemos visto el polimorfismo parámetrico con los genéricos en Java, de hecho, el concepto de genérico más que una particularidad de Java no es otra cosa que otro nombre para el polimorfismo paramétrico.

Por último, el polimorfismo ad hoc consiste en la capacidad que tiene una función o método de arrojar un resultado para diferentes tipos de datos que recibe como argumentos. Dicho de otra forma, una función o método polimórfico es capaz de despachar un resultado correcto para cada tipo que reciba. Por ejemplo, si tengo una función de sumar que me devuelve un resultado independientemente de si le paso como argumentos valores de tipo Float, Long, Integer o Double, tengo una función polimórfica ad hoc. Otra palabra que se usa para describir esta clase de polimorfismo es la sobrecarga (overload).

En Java podemos observar las tres formas de polimorfismo (con la salvedad que en tiempo de ejecución se soporta solamente el single dispatch). El polimorfismo de subtipos suele estar intrínsecamente relacionado con la herencia y con el concepto de sobreescritura (override). Mientras que el polimorfismo ad hoc lo está con el concepto de sobrecarga (overload).

Sin más preámbulos vayamos al código.

import java.time.LocalDate; //importamos clase para trabajar con fechas
public class MiembroDeLaComunidadUniversitaria{
  String nombre;
  int dni;
  
  public MiembroDeLaComunidadUniversitaria(String nombre, int dni){
    this.nombre = nombre;
    this.dni = dni;
  }
  
  public String inscribirse(String actividad){
    return "La inscripción de "+nombre+" para "+actividad+" se ha realizado satisfactoriamente.";
  }
}
public class Materia { 
    String titulo;
    String carrera;
    int cupo;
    
    public Materia(String titulo, String carrera){
      this.titulo=titulo;
      this.carrera=carrera;
      this.cupo=40;
    }
    
    public int getCupo(){
      return cupo;
    }
    
    public int tomarCupo(){
      if(cupo==0) return cupo;
      return this.cupo=cupo-1;
    }
    
    public String getTitulo(){
      return titulo;
    }
  }
public class Estudiante extends MiembroDeLaComunidadUniversitaria{
  String carrera;
  int carnetNro;
  String semestre_Ano;
  
  public Estudiante(String nombre, int dni, String carrera, int carnetNro, String semestre_Ano){
    super(nombre, dni);
    this.nombre=nombre;
    this.dni=dni;
    this.carrera=carrera;
    this.carnetNro=carnetNro;
    this.semestre_Ano=semestre_Ano;
  }
  
  public boolean verificar_cupo(Materia materia){
    if (materia.getCupo()>= 1) return true;
    return false;
  }
  
  //método heredado que exhibe conducta propia de la clase hija
   //sobreescritura de método padre
    @Override
    public String inscribirse(String actividad){
      return "El alumno "+nombre+" DNI "+dni+" perteneciente a la carrera "+carrera+" ha completado su inscripción";
    }
  
  //sobrecarga de método padre
  public String inscribirse(String actividad, LocalDate fecha){
    return "Incripción realizada para la actividad "+actividad+" a tener lugar el "+fecha;
  }
  
  public String inscribirse(Materia materia){
    if(verificar_cupo(materia)){
        materia.tomarCupo();
        return "Se completó la inscripción en "+materia.titulo;
      }else{
        return "No hay cupo disponible";
      }
  }
}
1.8s
Materia informatica = new Materia("Informática", "Sociología");
Estudiante gabriel = new Estudiante("Gabriel Mena", 1323220, "Sociología", 90239102, "2022-I");
0.5s

Una vez que hemos creados los dos objetos que necesitamos, vamos a llamar a cada una de las variantes del método inscribirse( ) :

gabriel.inscribirse(informatica);
0.5s
 gabriel.inscribirse("Jornadas Estudiantiles", LocalDate.of(2022, java.time.Month.NOVEMBER, 23))
0.5s

En estos dos primeros casos, tenemos el llamado a los métodos sobrecargados (overload). La sobrecarga involucra el cambio de la firma del método, bien sea en el tipo de retorno o en los parámetros.

gabriel.inscribirse("Rubgy");
0.6s

En este caso llamamos al método padre (no se cambia la firma del método) y añadimos una nueva implementación; lo sobre-escribimos (override). Más arriba utilizamos la anotación @Override, justo sobre la firma del método, para que el compilador nos ayude en caso que nos equivoquemos y sobrecarguemos en lugar de sobreescribir. (Sigan este link para una ilustración sobre la diferencia entre sobrecargar y sobreescribir).

En el caso del polimorfismo paramétrico, podemos demostrarlo al servirnos de los genéricos para abstraer una conducta del tipo de dato. Veamos un ejemplo:

Una universidad ofrece todo tipo de cursos, y éstos pueden ser ofrecidos por las escuelas, las facultades (e.g. cursos donde cooperan varias escuelas) o institutos de investigación. A continuación, sin usar herencia, crearemos la clase curso y la parametrizaremos de modo que podamos crear cursos ofrecidos por cada entidad universitaria:

public class Curso<T>{
  private String titulo;
  private T entidad;
  private String semestre;
  
  public Curso(String titulo, T entidad, String semestre){
    this.titulo=titulo;
    this.entidad=entidad;
    this.semestre=semestre;
  }
  
  public String inscribirse(String nombre){
    return "El alumno "+nombre+" se ha inscrito exitosamente en el curso "+titulo+" ofrecido por la/el "+entidad.toString()+" durante el periodo lectivo "+semestre;
  }
}
public class Facultad{
  String nombre;
  int nroEscuelas;
  
  public Facultad(String nombre, int nroEscuelas){
    this.nombre=nombre;
    this.nroEscuelas=nroEscuelas;
  }
  
  @Override
  public String toString(){
    return nombre;
  }
}
public class Escuela{
  Facultad facultad;
  String nombre;
  
  public Escuela(Facultad facultad, String nombre){
    this.facultad=facultad;
    this.nombre=nombre;
  }
  
  @Override
  public String toString(){
    return nombre;
  }
}
public class Instituto{
 Facultad facultad;
 String nombre;
 int nroInvestigadores; 
 
  public Instituto(Facultad facultad, String nombre, int nroInvestigadores){
    this.facultad=facultad;
    this.nombre=nombre;
    this.nroInvestigadores=nroInvestigadores;
  }
  
  @Override
  public String toString(){
    return nombre;
  }
}
1.2s
Facultad facultad_de_ciencias = new Facultad("Facultad de Ciencias", 4);
Instituto instituto_avanzado = new Instituto(facultad_de_ciencias, "Instituto de Estudios Avanzados", 40);
Curso<Escuela> python_per_tutti = new Curso<>("Python per tutti", new Escuela(facultad_de_ciencias, "Escuela de Computación"), "2022-II");
Curso<Facultad> curso_inductorio = new Curso<>("Curso inductorio", facultad_de_ciencias, "2022-I");
Curso<Instituto> algoritmos_geneticos = new Curso<>("Algoritmos genéticos", instituto_avanzado, "2022-I");
0.4s

La clase curso toma como parámetro un tipo cualquiera, en este caso, una entidad universitaria y crea cursos para cada tipo específico. De esta manera, sin usar la herencia hemos logrado extender una conducta determinada a través de distintas clases.

Arriba corroboramos que, en efecto, podemos crear cursos asociados a una Escuela, Facultad o Instituto, ahora veamos el comportamiento del único método de la clase Curso.

python_per_tutti.inscribirse("Juan Linares");
0.2s
algoritmos_geneticos.inscribirse("Pino Piatelli");
0.2s
curso_inductorio.inscribirse("Mario Balotelli");
0.2s

Por supuesto que la implementación del método puede ser más compleja, pero lo importante justamente es ver cómo el output del método cambia de la forma esperada según el tipo de la clase que Curso recibió como parámetro.

Ahora bien, si lo notan, podríamos hacer algo parecido con Instituto. ¿Se atreven a parametrizar a la clase Instituto?

También se podría crear una interfaz paramétrica (e.g. public interface Ejemplo<T>{}) y añadirle algún método default o métodos abstractos que otras clases tengan que implementar. O inclusive pueden crear métodos paramétricos, aunque es un caso más raro.

Puede ser posible que deseemos limitar de alguna manera nuestros tipos paramétricos, digamos que deseamos que T sea una clase que herede de tal otra o que sea una superclase de tal otra. Pues bien, para esto existen los bounds o límites. En el primer caso hablamos de un límite superior (e.g. List<T extends Exception>), mientras que en el segundo hablamos de un límite inferior (e.g List<T super Exception>).

Pongamos por ejemplo que deseamos crear una clase numérica general, por ende, cualquier tipo que herede de la clase Number de Java será un parámetro aceptable.

import java.util.concurrent.atomic.AtomicInteger;
public class NumberE <N extends Number>{
  private N num;
  public NumberE(N num){
    this.num=num;
  }
}
NumberE<AtomicInteger> n = new NumberE<>(new AtomicInteger(232));
NumberE<Long> l = new NumberE<>(232313L);
n;
0.4s
l;
0.2s

No se preocupen por la naturaleza del output; todavía no hemos implementado en la clase NumberE cómo se debe comportar este número en relación con otros, ni cómo debe aparecer por el stdout.

Lo importante a la hora usar genéricos es tener en cuenta algunos detalles:

  • No podemos llamar el constructor de un génerico (e.g. T( ) ) ya que en tiempo de ejecución esa llamada se resolvería en Object( ).

  • No podemos crear un array de un tipo estático, ya que estarías creando un array de tipo Object.

  • No podemos llamar instanceof debido a algo que se llama type erasure (básicamente, para garantizar la compatibilidad con código java viejo, cuando el compilador lee un genérico lo sustituye por Object, por ende, el chequeo instanceof no va a resultar en lo esperado; siempre va a ser true ya que toda clase de Java hereda de Object).

  • No podemos utilizar un tipo primitivo (esto es, int, double, long, boolean, etc.) como genérico, sino que debemos utilizar más bien la clase envoltorio, es decir, Integer, Double, Long, etc.

  • No podemos crear una variable estática como un genérico ya que el tipo está vinculado a instancias de la clase.

  • Y por último, hay que tener en cuenta algunas convenciones a la hora de nombrar genéricos:

    • E -> Elemento

    • K -> Llave de un mapa

    • V -> Valor de un mapa

    • N -> Número

    • T -> Tipo de dato genérico

    • S, U, V -> para enumerar distintos tipos de datos genéricos

    ----------------------------------------------------------------------------------------

En Scala también tenemos los tres tipos de polimorfismo, sólo que el sistema de tipos de Scala es más sofisticado que el de Java y permite hilar más fino en algunos casos, tales como determinar la forma en que se comporta el subtipado con tipos complejos (covarianza y contravarianza) y los tipos de alto orden (o tipos de tipos).

Más allá de esto, las diferencias con Java son meramente sintácticas.

class MiembroDeLaComunidadUniversitaria(val nombre: String, val dni: Int){
  def inscribirse(actividad: String):String = "La inscripción de "+nombre+" para "+actividad+" se ha realizado satisfactoriamente.";
}
class Estudiante(val curso: String, val semestreAno: String, nombre: String, dni: Int) extends MiembroDeLaComunidadUniversitaria(nombre, dni) {
  override def inscribirse(actividad: String):String = "El alumno "+nombre+" documento nro "+
dni+" se ha inscrito en el curso "+actividad
  def inscribirse(deporte: String, mensualidad: Int): String = "La inscripción en "+deporte+" se ha completado, debiendo abonar un total de "+mensualidad+"$ mensuales"
  }
0.7s
val est = new Estudiante("Matemáticas", "2022-I", "Juana Marcano", 1745894)
est.inscribirse("Taller de teoría de números")
est.inscribirse("Fútbol 11", 200)
0.7s

Pues bien, igual que en Java, en primer lugar tenemos el método sobreescrito (fíjense que la anotación override es diferente acá) y después el mismo método sobrecargado.

En el caso del polimorfismo paramétrico es donde vemos más diferencias. El caso más simple es exactamente igual, a saber, una clase paramétrica. Digamos que la clase Instituto sólo puede crearse si tiene un área de estudio asociada:

class Instituto[T](val nombre: String, val especialidad: T) 
class CienciasSocialesComputacionales(val lineas_de_investigacion: Int)
var instituto_de_ciencias_sociales_computacionales = new Instituto[CienciasSocialesComputacionales]("Instituto de Ciencias Sociales Computacionales", new CienciasSocialesComputacionales(10)) 
2.4s

En Scala los límites o bounds tienen una sintaxis algo diferente, mucho más sintética. Si queremos expresar que B hereda de A, decimos B <: A. Si queremos expresar que X es padre de Y, decimos X :> Y.

Digamos que especialidad es un Trait y que Instituto sólo puede crear instancias de clases que implementen el Trait especialidad.

trait Especialidad{
  val nombre: String
  def registrarseCONEAU(): String
}
class ComputationalSocialSciences(val lineas_de_investigacion: Int) extends Especialidad{
  val nombre = "Ciencias Sociales Computacionales"
  def registrarseCONEAU():String = "Se ha registrado la especialidad "+nombre+" exitosamente"
}
val css = new ComputationalSocialSciences(10)
css.nombre
css.registrarseCONEAU()
class Institute[T<:Especialidad](val nombre: String, val especialidad: T)
val institutecss = new Institute[ComputationalSocialSciences]("Instituto de Ciencias Sociales Computacionales", css)
0.9s

Ciertamente tendría más sentido que no se pudiesen crear instancias de las especialidades concretas (quizá un enumerado sea lo más adecuado), sin embargo, el ejemplo ilustra bien lo que se quiere demostrar en este caso.

En Scala también contamos con tipos existenciales para ahorrarnos molestias en ciertas situaciones. Pongamos que tenemos una fábrica que produce varios bienes y tenemos una máquina que cuenta el número de productos antes de mandarlos a empacar. De forma más concreta, nuestra máquina recibe un List[T] de un tipo genérico y retorna su tamaño size().

En principio, podríamos usar en método paramétrico:

def contar_productos[T](productos: List[T]): Int = productos.size
// Simplifiquemos un poco el ejemplo y utilicemos tipos que ya existen
var chocolates = List("A", "B", "C")
var jugos = List(1,2,3,4,5)
var caramelos = List(0.23, 2.3, 90.2, 23.43, 4.2, 12.4)
  
contar_productos(chocolates)
contar_productos(jugos)
contar_productos(caramelos)
0.9s

Y esto funciona bien. Pero sucede que no nos interesa ninguna propiedad de las clases que estamos vertiendo a la lista, sino las propiedades de la lista misma. Entonces podemos hacer lo siguiente:

def count_products(products: List[T] forSome {type T}): Int = products.size
var chocolates = List("A", "B", "C")
var jugos = List(1,2,3,4,5)
var caramelos = List(0.23, 2.3, 90.2, 23.43, 4.2, 12.4)
  
count_products(chocolates)
count_products(jugos)
count_products(caramelos)
0.9s

Ahora bien, digamos que dada una jerarquía de clases determinada nos gustaría que esta misma jerarquía se respetase en los tipos complejos que construimos a partir de estas clases ¡No entren en pánico! Un tipo complejo no es otra cosa que una clase paramétrica, como ya hemos visto.

Puesto de una manera más esquemática, si tenemos una jerarquía donde B hereda de A, esto es, B <: A, deseamos que un tipo complejo de A sea padre de un tipo complejo de B, a saber, T[A] :> T[B]. A esto se le llama covarianza y para indicarle esto al compilador tenemos que añadir un símbolo + a nuestro tipo (e.g. T[+A] ).

Si por el contrario deseáramos que nuestros tipos complejos invirtiesen la jerarquía de clases, es decir, si B <: A entonces T[A] <: T[B] a esto se le llama contravarianza y para indicarle al compilador esta relación debemos usar el símbolo - en la parametrización (e.g. T[-A]).

Y está el caso más complejo y raro en que necesitemos un tipo de tipos, al que se le llama higher kind types. Es decir, sería algo como ClaseCualquiera[M[_]]. El parámetro interno lo sustituimos con un placeholder como _ porque en realidad a este nivel de abstracción no interesa qué tipo ocupe esa posición.

Los bounds, la covarianza, contravarianza y los tipos de alto orden son probablemente herramientas que usaremos poco, sin embargo, es importante familiarizarse con la sintaxis y lo que estas ideas quieren expresar, porque este es el lenguaje en que nos habla la documentación.

---------------------------------------------------------------------------------------

Clojure no es un lenguaje orientado a objetos, sin embargo, su diseñador reconoció la versatilidad que le otorga a nuestro código el disponer de la capacidad de polimorfismo en tiempo de ejecución. Tomando inspiración de Common Lisp, Clojure adoptó el concepto de multimétodos (multimethod, que no es otra cosa que una forma de despacho múltiple), sin embargo, se le dio un giro funcional porque en vez de despachar por tipos se despacha a través de una función. Veamos.

Dado que Clojure no acepta el modelo de herencia, no podemos utilizar herencia de clases. No obstante, existe una función que crea jerarquías ad hoc basadas en llaves calificadas (esto es, llaves nombradas con su namespace, e.g. ::facultad lo que se traduciría, digamos, en algo como usuario.universidad/facultad). Así que hagámonos de esta herramienta y creemos una jerarquía que represente la estructura organizativa universitaria.

(def universidad (-> (make-hierarchy)
                     (derive ::facultad ::universidad)
                     (derive ::escuela ::facultad)
                     (derive ::consejo-de-facultad ::facultad)
                     (derive ::consejo-de-escuela ::escuela)
                     (derive ::laboratorios ::universidad)
                     (derive ::secretarias ::universidad)
                     (derive ::rectorado ::universidad)
                     (derive ::institutos ::facultad)))
0.2s

make-hierarchy crea una jerarquía entre llaves, mientras que con la función derive indicamos relaciones hijo->padre.

Ahora creemos nuestro multimétodo. Para crear un multimétodo tenemos que seguir dos pasos. Primero con la función defmulti registramos el multimétodo; esta función recibe como argumentos el nombre del multimétodo, la función de despacho y opcionalmente una jerarquía que aplicar, la cual en nuestro caso la acabamos de crear (esta jerarquía debe pasarse como un var, es decir, el nombre precedido por #').

(defmulti presupuesto (fn [id gastos ingresos] (isa? universidad id ::universidad)) :hierarchy #'universidad)
0.1s

La función de despacho que usamos toma tres argumentos, el primero va a ser el valor de despacho como tal (id). La función isa? recibe tres argumentos, primero la jerarquía y luego los miembros que se quieren testear. Acá creamos dos nombres, gastos e ingresos, ya que queremos calcular el prespuesto de cada entidad universitaria.

A continuación debemos crear los métodos que van a implementar la conducta deseada según el resultado de la función de despacho. En este caso vamos a crear sólo uno.

(defmethod presupuesto true
  [id gastos ingresos]
  (- ingresos gastos))
0.3s

Como vemos, es una implementación simple que resta los gastos de los ingresos. Ahora probemos:

(presupuesto ::facultad 1000 800)
0.1s
(presupuesto ::escuela 1900 2000)
0.0s
(presupuesto ::consejo-de-escuela 21331 32122)
0.0s

Ahora hagamos otro ejemplo, pero esta vez utilicemos algunos records para crear nuestra jerarquía (como vimos antes, los records son clases Java bajo cuerdas). Primero, vamos a crear nuestros records.

(defrecord Estudiante [nombre apellido dni])
(defrecord Profesor [materia titulo-universitario escalafon])
(defrecord Director [escuela periodo dependencias-a-cargo])
(defrecord Bedel [escuela antiguedad tareas])
0.4s

Es hora de crear nuestra jerarquía:

(def miembros-universidad (-> (make-hierarchy)
                              (derive Estudiante ::miembroDeLaComunidadUniversitaria)
                              (derive Profesor ::miembroDeLaComunidadUniversitaria)
                              (derive Director ::miembroDeLaComunidadUniversitaria)
                              (derive Bedel ::miembroDeLaComunidadUniversitaria)))
0.0s

Como en Clojure no existe herencia, los records no pueden derivar unos de otros, así que hacemos que deriven de la llave miembroDeLaComunidadUniversitaria.

(defmulti inscribirse
  (fn [class & _] (some #{class} (descendants miembros-universidad ::miembroDeLaComunidadUniversitaria)))
  :hierarchy #'miembros-universidad)
0.0s

Aunque pueda lucir un poco complicado, arriba sencillamente estamos diciendo que nuestra función de despacho recibe una o más clases como argumento y si esa clase es descendiente de miembroDeLaComunidadUniversitaria entonces se llamará al método adecuado para el caso.

Ahora toca escribir los métodos:

(defmethod inscribirse Estudiante
  [_ estudiante carrera] (str "El estudiante " (.nombre estudiante) " " (.apellido estudiante) " se ha inscrito en la carrera " carrera))
(defmethod inscribirse Profesor
  [_ curso especialidad duracion] (str "El profesor se ha inscrito en el curso " curso " especialidad " especialidad " con una duración de " duracion))
(defmethod inscribirse Director
  [_ elecciones-periodo candidatura-a] (str "Se ha realizado la inscripción para la candidatura a " candidatura-a " en el periodo " elecciones-periodo))
(defmethod inscribirse Bedel
  [_]
  (str "Se organizan las inscripciones para el nuevo periodo lectivo"))
0.1s
(inscribirse Profesor "Algoritmos y ciencias sociales" "Informática aplicada a las ciencias sociales" "3 semestres")
0.0s
(inscribirse Estudiante (->Estudiante "Juan" "Bermudez" 21323) "Ciencias de la Computación")
0.0s
(inscribirse Director "2022-II" "Rector")
0.0s
(inscribirse Bedel)
0.0s

Vale destacar que si cometemos algún error al crear un multimethod vamos a tener que remover el nombre del namespace, ya que de lo contrario no vamos a poder usar el mismo nombre. Para eso utilizamos la función ns-unmap (e.g. (ns-unmap *ns* 'presupuesto) )

En Clojure también podemos usar el despacho simple, si así lo deseamos. Para eso usamos los protocolos.

(defprotocol dependencias-universitarias (rendir-informe-presupuestario [this gastos ingresos]))
0.1s

Un protocolo se parece mucho a una interfaz Java, ya que el método se declara pero no se implementa; esto quedará para los records que implementen este protocolo.

En Clojure los records reciben como argumento opcional el nombre de un protocolo y su implementación. Creemos algunos records de esta manera:

(defrecord Facultad [titulo nroEscuelas institutos]
  dependencias-universitarias (rendir-informe-presupuestario [this gastos ingresos] (- ingresos gastos)))
(defrecord Escuela [titulo especialidades nroCupos]
  dependencias-universitarias (rendir-informe-presupuestario [this gastos ingresos] (str "Discutir cómo nos quedamos sin presupuesto " (- ingresos gastos))))
(defrecord Instituto [titulo especialidad facultad]
 dependencias-universitarias (rendir-informe-presupuestario [this gastos ingresos] (str "Estamos en problemas..." (- ingresos gastos))))
0.6s

Probemos:

(def facultad_humanidades (->Facultad "Facultad de Humanidades" 2 (list "Instituto de Estadística" "Instituto de Estudios Sociales")))
(rendir-informe-presupuestario facultad_humanidades 1900 2312)
0.1s
(def escuela_estudios_politicos (->Escuela "Escuela de Estudios Políticos" (list "Relaciones Internacionales" "Administración Pública") 120))
(rendir-informe-presupuestario escuela_estudios_politicos 1900 200)
0.1s
(def instituto_ciencias_computacionales (->Instituto "Instituto de Ciencias Computacionales" "Computación" "Facultad de Ciencias"))
(rendir-informe-presupuestario instituto_ciencias_computacionales 9022 1200)
0.1s

----------------------------------------------------------------------------------------

En Python podemos observar implementaciones de polimorfismo de subtipos y ad hoc. En el primer caso, tenemos una de las formas más básicas de polimorfismo en contexto de herencia como lo es la sobreescritura.

class MiembroDeLaComunidadUniversitaria:
  def __init__(self, nombre, dni):
    self.nombre=nombre
    self.dni=dni
    
  def inscribirse(self, actividad):
    raise NotImplementedError
    
class Estudiante(MiembroDeLaComunidadUniversitaria):
  def __init__(self, nombre, dni, carrera):
    super().__init__(nombre, dni)
    self.carrera=carrera
    
  def inscribirse(self, curso):
    return "El alumno "+self.nombre+" documento de identidad nro "+str(self.dni)+" se ha inscrito exitosamente en el curso "+curso
0.0s
est = Estudiante("Juan Moreno", 54458, "Ciencias de la Computación")
est.inscribirse("Python para todos")
0.0s

En el código de arriba definimos en la clase padre un método llamado inscribirse, el cual no implementamos sino que hicimos devolver una excepción de método no implementado. En la clase hija sobreescribimos el método de la clase padre y cuando la instancia del objeto Estudiante es utilizada para llamar al método inscribirse es justamente la implementación de Estudiante la que es llamada.

En Python no es posible acudir a la sobrecarga, sin embargo, podemos aplicar el polimorfismo ad hoc al añadirle parámetros opcionales a nuestras funciones. Por ejemplo:

def inscribir(nombre, dni, curso = "Matemáticas", periodo ="2022-II"):
  return "El alumno "+nombre+" dni nro "+str(dni)+" se ha inscrito en el curso "+curso+" en el periodo lectivo "+periodo
0.0s
inscribir("Juan Martir", 2323343)
0.0s
inscribir("Juan Martir", 2323343, "Ciencias Sociales")
0.0s
inscribir("Juan Martir", 2323343, "Ciencias Sociales", "2022-I")
0.0s

En Python también tenemos lo que se denomina polimorfismo con métodos de clase. Existen dos vertientes de esta técnica. La primera de ellas se basa en el hecho de que no existe nada que nos impida que distintas clases posean métodos homónimos, por lo que se puede aprovechar esta coincidencia para lograr una conducta polimórfica en determinado contexto como puede ser otra función. Veamos.

class Profesor:
  def cobrar(self):
    return 100
class Medico:
  def cobrar(self):
    return 10000
class Ingeniero:
  def cobrar(self):
    return 100000
def pagar_nomina():
  x = []
  for empleado in (Profesor(), Medico(), Ingeniero()):
    x.append(empleado.cobrar())
  return x
0.0s

Nótese que para ahorrarnos código, dado que nuestras clases no se instancian con ningún dato, en vez de crear variables que almacenaran la instancia del objeto creamos a las instancias directamente en la lista que vamos a iterar. Acá empleado asumirá el rol de una variable que en cada paso que se da al recorrer la colección, va a representar a cada uno de los objetos y va a llamar a la versión del método correspondiente en cada caso. Veamos:

pagar_nomina()
0.0s

Ya que Python permite un solo constructor por clase, la segunda vertiente consiste en usar un classmethod para definir constructores alternativos para nuestra clase. Un classmethod, puesto de forma simple, es un método que se puede llamar tanto desde la clase como desde sus instancias. A diferencia de un método estático, el classmethod es capaz de tener acceso a los atributos de la clase y a la clase misma, de hecho, recibe como primer argumento a la clase. Esta podría considerarse una técnica avanzada, así que lo podemos dejar para otra ocasión.

----------------------------------------------------------------------------------------

Julia soporta las tres clases de polimorfismo que hemos tratado en otros lenguajes. En el caso del polimorfismo de subtipos hemos visto que Julia, a semejanza de Clojure, adopta la filosofía de composición antes que herencia. Y si bien Julia cuenta con una jerarquía de tipos, tanto el tope como el fondo de la jerarquía están ocupados por tipos abstractos que, como en Clojure, no son otra cosa que nombres.

Podemos crear nuestros propios nombres para crear una jerarquía propia y, de esta manera, crear funciones que, justamente, al estar definidas en este nivel de abstracción se comportarán de forma polimórfica. Veámoslo con un ejemplo:

abstract type MiembroDeLaComunidadUniversitaria end
function inscribirse(miembro::MiembroDeLaComunidadUniversitaria, recaudos::String)
  "Juntar recaudos...
  Inscripción completa."
end
struct Estudiante <: MiembroDeLaComunidadUniversitaria
  nombre::String
  dni::Int
  Estudiante(nombre, dni) = new(nombre, dni)
end
struct Profesor <: MiembroDeLaComunidadUniversitaria
  nombre::String
  especialidad::String
  materias::Vector{String}
  Profesor(nombre, especialidad, materias) = new(nombre, especialidad, materias)
end
0.2s

Pues bien, siguiendo la misma idea de los ejemplos que hemos venido trabajando, definimos una pequeña jerarquía de tipos compuestos. Noten que usamos un símbolo que ya conocemos de Scala y cuyo significado es exactamente el mismo. Al escribir Estudiante <: MiembroDeLaComunidadUniversitaria le estamos indicando al compilador de Julia que Estudiante es un subtipo de MiembroDeLaComunidadUniversitaria.

El otro punto a destacar es que hemos creado una función inscribirse uno de cuyos parámetros tiene que ser del tipo MiembroDeLaComunidadUniversitaria. Ahora bien, desde que no se puede instanciar un tipo asbtracto, podemos colocar allí cualquier tipo que derive de MiembroDeLaComunidadUniversitaria, como es el caso de Estudiante y Profesor.

Pues bien, llamemos a la función inscribirse usando instancias de ambos structs:

inscribirse(Estudiante("Juana Mena", 32223), "Muchos papeles innecesarios")
0.1s
inscribirse(Profesor("Martín Durán", "Relaciones Internacionales", ["Seguridad Internacional", "Historia de las Relaciones Internacionales", "Teoría de las Relaciones Internacionales", "Relaciones cívico-militares"]), "Papeles y más papeles")
0.2s

A diferencia de la sobreescritura, como hemos visto en otros lenguajes, en este caso no podemos cambiar la implementación pues sencillamente estaríamos redefiniendo con ello a la función. Sin embargo, tenemos una función que trabaja para distintos tipos.

Lo que sí podemos hacer es sobrecargar nuestra función, de hecho, en Julia más precisamente estaríamos empleando el multiple dispatch al agregale más parámetros a inscribirse:

function inscribirse(miembro::MiembroDeLaComunidadUniversitaria, recaudos::String, curso::String)
  "Inscripcion completada para el curso "*curso
end
0.2s
inscribirse(Profesor("Martín Durán", "Relaciones Internacionales", ["Seguridad Internacional", "Historia de las Relaciones Internacionales", "Teoría de las Relaciones Internacionales", "Relaciones cívico-militares"]), "Papeles y más papeles", "Ciencias de la Computación")
0.2s

De este modo, en función del tipo, número y orden de los argumentos, el compilador de Julia será capaz de reconocer a qué versión de inscribirse se está llamando en cada caso.

Julia también soporta el polimorfismo paramétrico o los genéricos. La sintaxis para crear un genérico en Julia es la siguiente:

struct Instituto{T}
  nombre::String
  fundacion::Int
end
0.2s

Y al igual que vimos con Scala, digamos que resulta conveniente limitar algún modo el tipo que iría asociado con instituto. Dicho de otra manera, queremos poder crear institutos que deriven de una especialidad determinada. Entonces:

abstract type Especialidad end
struct CienciasSocialesComputacionales <: Especialidad
end
struct Institute{T<:Especialidad}
  nombre::String
  fundacion::Int
  nroInvestigadores::Int
  area::T
end
0.1s
instituto_ciencias_soc_comp = Institute{CienciasSocialesComputacionales}("Instituto de Ciencias Sociales Computacionales", 1990, 90, CienciasSocialesComputacionales())
0.4s

Por supuesto, existen otras sutilezas que podemos dejar para otra ocasión.

----------------------------------------------------------------------------------------

Racket ofrece soporte para el polimorfismo paramétrico, pero en una versión del lenguaje llamado Typed Racket o Racket tipado. Al tratarse de una versión diferente del Racket con el que venimos trabajando, lo excluiremos. Para quien desee explorarlo haga click aquí.

Y con esto terminamos nuestra lección sobre polimorfismo.

Runtimes (5)
Runtime Languages (12)