¿Es posible tener un lenguaje escrito dinámicamente sin pato? [cerrado]

9

Esta pregunta se hizo aquí , pero recibió respuestas pobres y no aclaró el problema. Creo que justifica pedirlo de nuevo.

Entiendo que puede escribir pato con lenguajes dinámicamente o estáticamente (pero los ejemplos de estos son raros, como las plantillas de C ++).

Sin embargo, no estoy seguro de si existe un lenguaje escrito dinámicamente sin tipear pato.

La escritura de pato significa que el tipo de un objeto se basa en las operaciones y los atributos que tiene en un momento dado. ¿Hay alguna forma de escribir dinámicamente sin admitir inevitablemente la escritura de pato?

Veamos este código de Python para el examen:

def func(some_object)
    some_object.doSomething()

something = Toaster()
func(something)

En lenguajes de tipo dinámico, el tipo de un objeto solo se conoce en tiempo de ejecución. Entonces, cuando intentas hacer una operación en él (p some_object.doSomething(). Ej. ), El tiempo de ejecución solo tiene una opción , que es verificar si el tipo de some_objectsoporte es doSomething(), o no , exactamente qué tipo de pato es.

Entonces, ¿es posible tener un lenguaje escrito dinámicamente sin escribir pato? Por favor explique.

Aviv Cohn
fuente
1
He comprobado algunos idiomas (Javascript, Ruby y PHP) en Wikipedia y se supone que este último es dinámico y débil, no está escrito en forma de pato. Eso puede responder a tu pregunta.
Trylks
En realidad, el tiempo de ejecución tiene la opción de verificar la firma de tipo del objeto antes de intentar acceder a cualquiera de sus 'campos o llamar a sus métodos'. Eso sería "fuerte tipografía dinámica". Tenga en cuenta que Python, por ejemplo, funciona parcialmente de esta manera; por ejemplo, emite explícitamente un error de tipo cuando lo intenta 1 + "1". En el caso de Python, la disciplina de verificación está prácticamente ausente y depende de la implementación del código de usuario verificar los tipos si el usuario (en oposición al tiempo de ejecución de Python) lo encuentra útil. También tenga en cuenta que la escritura de pato frente a la escritura de no pato es similar a la escritura nominal frente a la estructural (ver Wikipedia).
Michael Pankov
¿quizás deberías usar un ganso en lugar de un pato para escribir por ti?
Jwenting

Respuestas:

19

Primero, para asegurarme de que estamos hablando de las mismas cosas, comenzaría con algunas definiciones.

La escritura estática significa que los errores de tipo se informan en tiempo de compilación, mientras que la escritura dinámica significa que los errores de tipo se informan en tiempo de ejecución.

La escritura de pato significa que un fragmento de código requiere que un objeto admita las operaciones que se utilizan y nada más.

La tipificación estructural requiere que un objeto admita un conjunto dado de operaciones (incluso si algunas de ellas no se pueden usar).

La tipificación nominal requiere que el objeto sea exactamente del tipo dado, o sea un subtipo de ese tipo.

Entonces, como puede ver, la tipificación estructural es más estricta que la tipificación de pato y la tipificación nominal es más estricta que la estructural.

Ahora, voy a hablar sobre el lenguaje TypeScript , porque ilustra muy bien la mayoría de estas opciones.

Considere el siguiente programa TypeScript:

interface Person {
    Name : string;
    Age : number;
}

function greet(person : Person) {
    alert("Hello, " + person.Name);
}

greet({ Name: "svick" });

Dado que el objeto al que se pasa greetno tiene la Agepropiedad, esto provoca un error de tiempo de compilación, lo que demuestra que TypeScript usa escritura estructural estática .

A pesar del error, el código anterior se compila en el siguiente JavaScript, que funciona bien:

function greet(person) {
    alert("Hello, " + person.Name);
}

greet({ Name: "svick" });

Esto muestra que TypeScript también utiliza la escritura dinámica de pato .

Si el código en su lugar se compila a algo como:

function greet(person) {
    if (!(typeof(person.Name) == 'string' && typeof(person.Age) == 'number'))
        throw 'TypeError';

    alert("Hello, " + person.Name);
}

Entonces ese sería un ejemplo de tipificación estructural dinámica , porque verifica que el objeto tenga las propiedades requeridas de los tipos requeridos, incluso si la función en sí no los requiere.

Si se compiló para:

function greet(person) {
    if (!(person instanceof Person))
        throw 'TypeError'

    alert("Hello, " + person.Name);
}

Ese sería un ejemplo de tipificación nominal dinámica , porque verifica el nombre del tipo del objeto, no su estructura.

Lo que todo esto muestra es que es posible la tipificación dinámica sin pato (tanto estructural como nominal). Pero este enfoque no se usa con mucha frecuencia, porque combina principalmente las desventajas de la escritura sin pato (debe especificar los tipos explícitamente; menos flexible) y la escritura dinámica (los errores de tipo solo se muestran en tiempo de ejecución y solo en el código que realmente se ejecuta )

Si va a agregar anotaciones de tipo para hacer posible la escritura sin pato, también puede verificar los tipos en el momento de la compilación.

svick
fuente
1
No diría que el mecanografiado utiliza la escritura dinámica de pato. El hecho de que, a pesar de los errores de compilación, tsc todavía produce javascript, me parece una rareza, pero de todos modos ... el error de tipeo se informó en tiempo de compilación, y la parte de tiempo de ejecución se deja en JavaScript, no en mecanografiado. Sin embargo, el mecanografiado también utiliza la escritura estática de pato; cuando escribe "var t = 1;", se supone que la variable es un número.
Joel
@ Joel Sí, TypeScript hereda la escritura dinámica de pato de JavaScript, pero eso no hace que sea menos una propiedad de TypeScript. Y su ejemplo demuestra inferencia de tipos, no duck typing.
svick
¿Cómo llamaría a un lenguaje que requiere que los métodos se definan como partes de las interfaces, pero que permite que las ubicaciones de almacenamiento se tipeen utilizando combinaciones arbitrarias de interfaces? Si pasa [3,4]a algún método de una colección de la celebración de [1,2]los rendimientos [1,2,3,4], y que pasa [3,4]a algún método de alguna celebración colección [1,2]serían [4,6], si los métodos tienen el mismo nombre? Llamando al primero Sequence$Appendable$Addy al segundo, NumericVector$Addpero luego pudiendo decir que una variable debe interpretarse como un Sequenceo un NumericVector...
supercat
... parece más limpio que simplemente esperar que los nombres homográficos no causen confusión. En los idiomas donde cada variable tiene que ser solo un tipo, un tipo "puramente dinámico" podría ser útil, pero si los tipos compuestos estuvieran disponibles, pensaría que eliminaría el 99% de los casos de uso para el pato "inesperado" mecanografía.
supercat
Me inclino a estar de acuerdo con @Joel, la diferencia entre una advertencia y una excepción tiene poco que ver con la escritura del idioma. TypeScript es una biblioteca. tsces una interfaz en ella. Si usa la biblioteca, desencadenará un evento. Por defecto, si nada está escuchando, obtienes un script. Si escucha y lanza una excepción, puede detener la generación del script. ¿Eso cambia el sistema de tipos de TypeScript? Por supuesto no.
Evan Carroll
3

Aparentemente (por lo que leí), la escritura de pato solo tiene significado en un contexto orientado a objetos, cuando las funciones se adjuntan como métodos a los objetos. Luego, cuando escriba duck.quacks(3), esto funcionará si el valor actual de ducktiene el método quacks.

La escritura dinámica no está necesariamente asociada a una vista OO con métodos.

Puede definir el tipo realcon el operador o la función asociados +: real*real->real, y el tipo rationalcon el operador asociado +: rational*rational->rational. Luego, si escribe a+b, en un sistema de tiempo verificado dinámicamente, ambas variables ay bpueden tener un +operador, pero obtiene un error de tipo de tiempo de ejecución.

La tipificación dinámica verifica la consistencia categorial de los valores, posiblemente varios de ellos.

Duck Typing comprueba la consistencia del comportamiento del código con un objeto en mano (uno solo, por lo que yo entiendo).

En cierto sentido, la tipificación de pato es una forma de polimorfismo de tiempo de ejecución, excepto por el hecho de que solo se aplica a los métodos de acceso de un solo objeto.

Sin embargo, uno podría definir una forma más general de polimorfismo en tiempo de ejecución, donde el operador +a ejecutar se determinaría sobre la base de todos los argumentos. Para que un pato y un pollo puedan bailar juntos si comparten una función de baile común. Podrían tener varios para que los patos puedan bailar también con gansos con una función diferente. Pero eso parece un poco complicado. Hasta donde recuerdo, algo parecido (con probablemente más estructura) pudo haber sido posible con las funciones genéricas del lenguaje EL1 , un precursor muy antiguo de lenguajes orientados a objetos.

babou
fuente
Python también soporta la sobrecarga del operador. def add (a, b): return a + b es factible. Se convierte a .__ add __ (b) automáticamente. ... Así que sí, escribir pato parece requerir algún tipo de tipos avanzados definidos por el usuario. Aunque los miembros de datos también funcionan, las estructuras de pato también serían posibles, similar a esto: def addstuff (a, b, t): t.stuff = a.stuff + b.stuff
StarWeaver
@StarWeaver Bueno, sí. Pero lo que me molesta con su ejemplo es la asimetría entre los operandos. Si hay un error de tipo, es porque a no tiene el método adecuado o porque hay una falta de coincidencia entre los métodos de a y el tipo de b. Entonces, dependiendo de la situación, obtienes una excepción diferente (si mi memoria de Python es correcta). Es posible que desee un nivel de abstracción en el que solo obtendría una excepción que diga que a y b no pueden + juntos (perdón por la oración extraña). Pero eso puede ser una cuestión de gustos. También me gustaría decir que + es conmutativo.
babou
-2

Una respuesta por analogía:

¿Puedes comprar un convertible y nunca poner la capota baja? Por supuesto. Probablemente no sea la mejor manera de gastar sus recursos, ya que se le paga extra por algunas características (por ejemplo, capota convertible, rigidez estructural adicional debido a la falta de techo como elemento estructural) y obtiene algunos peores resultados (por ejemplo, ruido adicional de la carretera, posiblemente menor seguridad contra choques, compartimentos de almacenamiento más pequeños) como resultado de invertir en esa característica que no usará. Pero es técnicamente factible.

Es lo mismo con los lenguajes dinámicos y la escritura de pato. Has renunciado a la mayor eficiencia y a las garantías de seguridad de tipo de tiempo de compilación de los lenguajes estáticos. ¿Para qué? Generalmente por la simplicidad de la escritura de patos. Las variables y las colecciones pueden contener cualquier cosa, y no es necesario que hagas mucho por adelantado especificando exactamente qué. Las colecciones mixtas como [ 12, "gumbo", 12.4, 4+4j ](un número entero, una cadena, un valor de coma flotante y un valor complejo) son triviales y no tiene la conversión de tipos constante que ve en el código Java (por ejemplo).

Es posible en un lenguaje dinámico como Python crear objetos que no están tipificados en pato:

class Hungarian(object):
    self __init__(self):
        self.value = []
    self addInt(self, intValue):
        self.value.append(intValue)
    self addFloat(self, floatValue):
        self.value.append(floatValue)
    self addComplex(self, complexValue):
        self.value.append(complexValue)
    # ...

Pero como puede observar, no hay una verificación real de los tipos, y cada uno de los métodos se implementa con una estructura incorporada de tipo pato ( list). Habiendo pagado el precio por el dinamismo, también podrías bajar la capota y obtener la simplicidad resultante:

class NotHungarian(object):
    def __init__(self):
        self.value = []
    def add(self, whatever):
        self.value.append(whatever)
Jonathan Eunice
fuente
¿De qué manera Hungarianno se tipea esa clase? Como señaló, no hay verificación de tipos.
svick
@svick Está diseñado para usarse de una manera específica de tipo, como Java, con métodos separados que no dependen de la acción realizada, sino también de los tipos utilizados. La escritura de pato usaría el mismo método independientemente del tipo que se agrega, como lo NotHungarianhace. La escritura de pato depende no solo de no verificar el tipo, sino también de usar el mismo nombre de llamada / método / mensaje ( add). ( NotHungariantambién usa un nombre de método add, que es común con otros objetos de Python, como set. "Grazna" como esos otros objetos / clases.)
Jonathan Eunice
1
Esta respuesta está un poco mal informada sobre la motivación para "renunciar a la mayor eficiencia y las garantías de seguridad de tipo de tiempo de compilación de lenguajes estáticos". La motivación era soltar los grilletes de un sistema tipo Fortran o C, porque estos sistemas a menudo se interponen en el camino (piense en las cadenas de longitud fija de Pascal), evitan la programación genérica y hacen que las características increíblemente poderosas sean evalinviables. La motivación nunca fue deshacerse de los tipos, y algunos lenguajes dinámicos tienen "mecanografía gradual". MJD tiene una buena presentación sobre tipeo estático versus dinámico.
amon
@amon No estoy de acuerdo. Los lenguajes tipados estáticamente generalmente tienen una ventaja bastante significativa sobre los lenguajes tipados dinámicamente porque usaron tipos de caja / valor, estructuras en lugar de dictos, etc. Los compiladores JIT han reducido la brecha, pero los lenguajes compilados estáticos AOT aún notablemente más rápido. Esa no es la única razón para elegir un idioma. Uso dinámicas siempre que puedo, y la penalización de rendimiento es limitada o sin importancia la mayor parte del tiempo. Y sí, hay otras motivaciones para elegir dinámicas. Pero renuncias a cosas reales para ir allí.
Jonathan Eunice