Duck typing

De Wikipedia, la enciclopedia libre

En los lenguajes de programación orientados a objetos, se conoce como duck typing o tipado pato el estilo de tipificación dinámica de datos en que el conjunto actual de métodos y propiedades determina la validez semántica, en vez de que lo hagan la herencia de una clase en particular o la implementación de una interfaz específica. El nombre del concepto se refiere a la prueba del pato, una humorada de razonamiento inductivo atribuida a James Whitcomb Riley (ver Historia más abajo), que pudo ser como sigue:

"Cuando veo un ave que camina como un pato, nada como un pato y suena como un pato, a esa ave yo la llamo un pato."[1][2]

En duck typing, el programador solo se ocupa de los aspectos del objeto que van a usarse, y no del tipo de objeto que se trata. Por ejemplo en un lenguaje sin duck-typing uno puede crear una función que toma un objeto de tipo Pato y llama los métodos "caminar" y "parpar" de ese objeto. En un lenguaje con duck-typing, la función equivalente tomaría un objeto de cualquier tipo e invocaría los métodos caminar y parpar. Si el objeto tratado no tiene los métodos pedidos, la función enviará una señal de error en tiempo de ejecución. Este hecho de que la función acepte cualquier tipo de objeto que implemente correctamente los métodos solicitados es lo que evoca la cita precedente y da nombre a la forma de tipificación.

El Duck typing usualmente es acompañado por el hábito de no probar el tipo de los argumentos en los métodos y funciones, y en vez de eso confiar en la buena documentación, el código claro y la prueba para asegurar el uso correcto. Los usuarios de lenguajes con tipificado estático al iniciarse con lenguajes de tipificado dinámico a menudo se ven tentados a agregar chequeos de tipo estáticos (previos a ejecución), desaprovechando la flexibilidad y beneficios del duck typing y restringiendo el dinamismo del lenguaje.

Ejemplos del concepto[editar]

Considera el siguiente pseudocódigo para un lenguaje con duck-typing:

función calcular(a, b, c) => devuelve (a+b)*c

ejemplo1 = calcular (1, 2, 3)
ejemplo2 = calcular ([1, 2, 3], [4, 5, 6], 2)
ejemplo3 = calcular ('manzanas ', 'y naranjas, ', 3)

mostrar a_cadena ejemplo1
mostrar a_cadena ejemplo2
mostrar a_cadena ejemplo3

En el ejemplo, cada vez que se llama la función calcular se pueden emplear objetos sin relación de herencia (números, listas y cadenas). En tanto y en cuanto los objetos soporten los métodos "+" y "*" la operación tendrá éxito. Si se traduce este algoritmo a Ruby o Python por ejemplo, el resultado de la ejecución será:

9
[1, 2, 3, 4, 5, 6, 1, 2, 3, 4, 5, 6]
manzanas y naranjas, manzanas y naranjas, manzanas y naranjas,  

Así, el duck typing permite polimorfismo sin herencia. El único requerimiento de la función calcular para las variables es que soporten los métodos "+" y "*".

La prueba del pato puede verse en el siguiente ejemplo (en Python). Hasta donde a la función en_el_bosque le importa, el objeto es un pato:

class Pato:
    def parpar(self): 
        print "Cuac!"
    def plumas(self): 
        print "El pato tiene plumas blancas y grises."
 
class Persona:
    def parpar(self):
        print "La persona imita el sonido de un pato."
    def plumas(self): 
        print "La persona toma una pluma del suelo y la muestra."
 
def en_el_bosque(pato):
    pato.parpar()
    pato.plumas()
 
def juego():
    donald = Pato()
    juan = Persona()
    en_el_bosque(donald)
    en_el_bosque(juan)

juego()

Duck typing en lenguajes de tipificación estática[editar]

Ciertos lenguajes que usualmente son de tipificación estática como Boo y la versión 4 de C# tienen anotaciones extra de tipos[3][4]​ que instruyen al compilador para que disponga el chequeo de tipo de las clases en tiempo de ejecución y no en tiempo de compilación, y que incluya código de chequeo de tipos en tiempo de ejecución en la salida compilada. Estos agregados permiten al lenguaje gozar de la mayor parte de los beneficios del duck-typing con la única desventaja de tener que identificar y especificar qué clases serán dinámicas en tiempo de compilación.

Comparación con otros tipos de tipificación[editar]

Sistemas de tipificación estructural[editar]

El duck typing es similar pero diferente a la tipificación estructural. La tipificación estructural es un sistema de tipificación estática que determina la compatibilidad y la equivalencia de tipos según su estructura, mientras que el duck typing es dinámico y determina dicha compatibilidad basándose en la parte de la estructura que se accede en tiempo de ejecución.
El lenguaje de programación Objective Caml usa un sistema de tipificación estructural.

Interfaces[editar]

Las interfaces pueden proveer algunos de los beneficios del duck typing pero este es diferente en cuanto no se define explícitamente una interfaz. Por ejemplo, si una librería Java de terceros implementa una clase que no es posible modificar, no se podrá usar una instancia de esa clase en lugar de una interfaz definida por uno mismo, mientras que el duck typing sí permite hacerlo.

Templates o tipos genéricos[editar]

Los Template, funciones o métodos genéricos aplican la prueba del pato en un contexto de tipificación estática; esto reúne todas las ventajas y desventajas de la tipificación estática contra la tipificación dinámica en general. El duck typing puede también ser más flexible en que solo los métodos llamados en tiempo de ejecución necesitan ser implementados, mientras que los templates requieren que se implemente todos los métodos que no deban resultar inaccesibles en tiempo de compilación.

Puede mencionarse como ejemplos los lenguajes C++ y D con templates, que derivaron de los genéricos de Ada.

Críticas[editar]

Una crítica citada a menudo dice:

Un problema con el duck typing es que fuerza al programador a tener un entendimiento mucho más amplio del código con que está trabajando en todo momento. En un lenguaje con tipificación estática y fuerte que use controles de tipo en jerarquías y parámetros, es mucho más difícil pasarle un tipo inesperado de objeto a una clase. Por ejemplo en Python podrías fácilmente crear una clase llamada Vino, que espera una clase que implemente el atributo "prensa" como un ingrediente. Sin embargo una clase llamada Imprenta podría también implementar un método llamado prensa(). Con duck typing, para poder prevenir errores extraños y difíciles de detectar el desarrollador debe ser consciente de cada uso potencial del método "prensa", aunque esté conceptualmente desvinculado del tema con que esté trabajando.
Esencialmente, el problema es que "si camina como pato y se oye como pato", podría ser un dragón haciendo el pato. No siempre querrás dejar los dragones en el estanque, aunque sepan hacerse pasar por patos.

Los defensores del duck typing como Guido van Rossum argumentan que el asunto se maneja con pruebas, y con el conocimiento del código necesario para mantenerlo.[5][6]

Las críticas acerca del duck typing tienden a ser casos especiales de aspectos más amplios relacionados con la disputa entre la tipificación estática y la dinámica.

Historia[editar]

Alex Martelli hizo uso precoz (2000) del término en un mensaje del grupo de noticias comp.lang.python. También resaltó la malinterpretación de la prueba literal del pato, lo que podría indicar que el término ya era usado.

En otras palabras, no chequeen si ES un pato, chequeen si PARPA como pato, CAMINA como pato, etc, etc, dependiendo de exactamente qué parte de la conducta de un pato necesitan para jugar su juego de lenguaje..

Implementaciones[editar]

En C#[editar]

En C# 4.0 el compilador y la ejecución colaboran para implementar dynamic member lookup.

namespace DuckTyping
{
  using System;

  public class Pato
  {
    public void Parpar()
    {
      Console.WriteLine("Quaaaaaack!");
    }

    public void Plumas()
    {
      Console.WriteLine("El pato tiene plumas blancas y grises.");
    }
  }

  public class Persona
  {
    public void Parpar()
    {
      Console.WriteLine("La persona imita a un pato.");
    }

    public void Plumas()
    {
      Console.WriteLine("La persona toma una pluma del piso y la muestra.");
    }
  }

  internal class Program
  {
    private static void EnElBosque(dynamic pato)
    {
      pato.Parpar();
      pato.Plumas();
    }

    private static void Juego()
    {
      dynamic donald = new Pato();
      dynamic juan = new Persona();
      EnElBosque(donald);
      EnElBosque(juan);
    }

    private static void Main()
    {
      Juego();
    }
  }
}

En ColdFusion[editar]

El lenguaje de scripting para desarrollo de aplicaciones web ColdFusion permite que los argumentos de las funciones sean de tipo any (cualquiera). Para este tipo de argumento, puede pasarse un tipo arbitrario de objeto y las llamadas a métodos son entabladas en tiempo de ejecución. Si un objeto no implementa un método solicitado, se arroja una excepción en tiempo de ejecución que puede ser capturada y manejada adecuadamente. En ColdFusion 8, esta circunstancia se puede capturar con el evento onMissingMethod() en vez de con un manejador de excepciones. Un tipo alternativo de argumento WEB-INF.cftags.component restringe el tipo de objeto solo a componentes ColdFusion (CFC) lo cual permite un mejor manejo de mensajes de error cuando se pasan elementos que no sean objetos.

En Common Lisp[editar]

Common Lisp provee una extensión orientada a objetos (Common Lisp Object System, abreviado CLOS). La combinación de CLOS y la tipificación dinámica de Lisp hacen del duck typing un estilo de programación habitual en Common Lisp.

Con Common Lisp uno tampoco necesita averiguar los tipos, porque en la ejecución se obtendrá una señal de error en caso de que una función no sea aplicable. El error puede manejarse con el Sistema de Condiciones de Common Lisp. Los métodos se definen fuera de las clases y también pueden definirse para objetos específicos.

(defclass duck () ())

(defmethod quack ((a-duck duck))
  (print "Quaaaaaack!"))

(defmethod feathers ((a-duck duck))
  (print "The duck has white and gray feathers."))

(defclass person () ())

(defmethod quack ((a-person person))
  (print "The person imitates a duck."))

(defmethod feathers ((a-person person))
  (print "The person takes a feather from the ground and shows it."))

(defmethod in-the-forest (duck)
  (quack duck)
  (feathers duck))

(defmethod game ()
  (let ((donald (make-instance 'duck))
        (john (make-instance 'person)))
    (in-the-forest donald)
    (in-the-forest john)))

(game)

El estilo usual de desarrollo en Common Lisp (usando un Lisp REPL como SLIME) permite también la reparación interactiva:

? (defclass cat () ())
#<STANDARD-CLASS CAT>
? (quack (make-instance 'cat))
> Error: There is no applicable method for the generic function:
>          #<STANDARD-GENERIC-FUNCTION QUACK #x300041C2371F>
>        when called with arguments:
>          (#<CAT #x300041C7EEFD>)
> If continued: Try calling it again
1 > (defmethod quack ((a-cat cat))
        (print "The cat imitates a duck."))

#<STANDARD-METHOD QUACK (CAT)>
1 > (continue)

"The cat imitates a duck."

De este modo se puede desarrollar el software extendiendo código duck-typed que funciona parcialmente.

En Objective-C[editar]

Objective-C, un híbrido entre C y Smalltalk, permite declarar objetos de tipo 'id' y enviarles cualquier mensaje, como en SmallTalk. Quien envía puede probar un objeto para ver si ha respondido al mensaje, el objeto puede decidir al recibir el mensaje si responderá al mensaje o no, y si el remitente envía un mensaje al que el receptor no puede responder, se arroja una excepción. Así, duck typing es completamente soportado en Objective-C.

En Python[editar]

El duck typing se usa intensivamente en Python. El Glosario de Python define el duck typing como sigue:

Estilo de programación Python que determina el tipo de un objeto a través de la inspección de sus métodos o su conjunto de atributos en lugar de emplear la relación con algún tipo de objeto ("Si luce como un pato y suena como un pato, debe ser un pato") Enfatizando las interfaces sobre los tipos específicos, el código bien diseñado mejora su flexibilidad al permitir sustitución polimórfica. El duck typing evita las pruebas con type() o isinstance(). En lugar de eso, prefiere el estilo de programación "Es más fácil pedir perdón que pedir permiso".

El ejemplo estándar de duck typing en Python son las clases que se asimilan a archivos. Estas clases pueden implementar algunos o todos los métodos de archivo y pueden usarse donde normalmente se usaría archivo. Por ejemplo, GzipFile implementa un objeto similar a un archivo para acceder a datos comprimidos con el algoritmo gzip. cStringIO permite tratar una cadena como un archivo. Los sockets y los archivos también comparten muchos atributos. Sin embargo, los sockets no disponen del método tell() y no pueden usarse dondequiera que puede usarse GzipFile. Esto demuestra la flexibilidad del duck typing: un objeto que se asemeja a un archivo implementa solo los métodos que puede, y consecuentemente solo puede usarse donde tiene sentido hacerlo.

El principio de "es más fácil pedir perdón que pedir permiso" describe el uso del manejo de excepciones. Por ejemplo en vez de ver si un objeto que supuestamente se asemeja al objeto Pato usado en el ejemplo precedente tiene o no el método parpar() (usando if hasattr(azulon, "parpar"): ... es preferible rodear el intento de parpar con un manejo de excepciones:

try:
    azulon.parpar()
except (AttributeError, TypeError):
    print "el azulón no puede parpar()"

Las ventajas de esta forma de trabajo son que favorece el manejo estructurado de otras clases de errores (así, por ejemplo, una subclase de Pato mudo podría arrojar una excepción de tipo CuacException que podría agregarse al manejador sin ahondar mucho en la lógica del código, y manejar situaciones en que diferentes clases de objetos pudieran tener colisiones de nombres por miembros incompatibles (si tuviésemos una persona de apellido Azulón con un atributo lógico "parpa=True"; un intento de ejecutar Azulón.parpar() arrojaría un TypeError)).

Volviendo al campo de los ejemplos más prácticos que implementan conductas similares a las de un archivo, es preferible generalmente emplear las herramientas de tratamiento de excepciones de Python para manejar la amplia variedad de errores de E/S que pueden ocurrir debido a numerosos problemas ambientales y de sistema operativo que escapan al control del programador. Aquí nuevamente las excepciones de "duck typing" pueden ser atrapadas junto a las de SO, E/S u otros posibles errores sin complicados chequeos ni lógicas de comprobación de errores.

En Julia[editar]

Julia usa despacho múltiple, funciones genéricas, anotaciones de tipos opcionales e inferencia de tipos automática por defecto, el tipo Any es el super-tipo de toda la jerarquía, ejemplo en el REPL (bucle de lectura-evaluación-impresión) interactivo de Julia:

julia> type Pato end

julia> type Persona
           nombre::ASCIIString
       end

julia> parpar(x::Pato) = println("Cuaaac!")
parpar (generic function with 1 method)

julia> parpar(x::Persona) = println("La persona imita el sonido de un pato.")
parpar (generic function with 2 methods)

julia> plumas(x::Pato) = println("El pato tiene plumas blancas y grises.")
plumas (generic function with 1 method)

julia> plumas(x::Persona) = println("La persona toma una pluma del suelo y la muestra.")
plumas (generic function with 2 methods)

julia> nombre(x::Persona) = println(x.nombre)
nombre (generic function with 1 method)

julia> function en_el_bosque(pato)    # lo mismo que pato::Any
           parpar(pato)
           plumas(pato)
       end
en_el_bosque (generic function with 1 method)

julia> function juego()
           donald = Pato()
           juan = Persona("Juan")

           en_el_bosque(donald)
           en_el_bosque(juan)
       end
juego (generic function with 1 method)

julia> juego()
Cuaaac!
El pato tiene plumas blancas y grises.
La persona imita el sonido de un pato.
La persona toma una pluma del suelo y la muestra.

julia>

Referencias[editar]

Enlaces externos[editar]