He estado usando Python durante unos días y creo que entiendo la diferencia entre la escritura dinámica y la estática. Lo que no entiendo es bajo qué circunstancias sería preferible. Es flexible y legible, pero a expensas de más controles de tiempo de ejecución y pruebas unitarias adicionales requeridas.
Además de criterios no funcionales como la flexibilidad y la legibilidad, ¿qué razones hay para elegir la escritura dinámica? ¿Qué puedo hacer con la escritura dinámica que no es posible de otra manera? ¿Qué ejemplo de código específico se te ocurre que ilustra una ventaja concreta del tipeo dinámico?
dynamic-typing
static-typing
Justin984
fuente
fuente
Respuestas:
Como solicitó un ejemplo específico, le daré uno.
El ORM masivo de Rob Conery tiene 400 líneas de código. Es así de pequeño porque Rob puede asignar tablas SQL y proporcionar resultados de objetos sin requerir muchos tipos estáticos para reflejar las tablas SQL. Esto se logra utilizando el
dynamic
tipo de datos en C #. La página web de Rob describe este proceso en detalle, pero parece claro que, en este caso de uso particular, la tipificación dinámica es en gran parte responsable de la brevedad del código.Compárese con Dapper de Sam Saffron , que usa tipos estáticos;
SQLMapper
solo la clase tiene 3000 líneas de código.Tenga en cuenta que se aplican las exenciones de responsabilidad habituales, y su kilometraje puede variar; Dapper tiene objetivos diferentes a los de Massive. Solo señalo esto como un ejemplo de algo que puedes hacer en 400 líneas de código que probablemente no sería posible sin una escritura dinámica.
La escritura dinámica le permite diferir sus decisiones de tipo en tiempo de ejecución. Eso es todo.
Ya sea que use un lenguaje de tipo dinámico o uno de tipo estático, sus elecciones de tipo aún deben ser sensatas. No va a agregar dos cadenas juntas y esperar una respuesta numérica a menos que las cadenas contengan datos numéricos, y si no lo hacen, obtendrá resultados inesperados. Un idioma escrito estáticamente no le permitirá hacer esto en primer lugar.
Los defensores de los lenguajes de tipo estático señalan que el compilador puede hacer una cantidad sustancial de "comprobación de sanidad" de su código en tiempo de compilación, antes de que se ejecute una sola línea. Esta es una buena cosa ™.
C # tiene la
dynamic
palabra clave, que le permite diferir la decisión de tipo en tiempo de ejecución sin perder los beneficios de la seguridad de tipo estático en el resto de su código. La inferencia de tipos (var
) elimina gran parte del dolor de escribir en un lenguaje de tipo estático al eliminar la necesidad de declarar siempre tipos explícitamente.Los lenguajes dinámicos parecen favorecer un enfoque más interactivo e inmediato de la programación. Nadie espera que tenga que escribir una clase y pasar por un ciclo de compilación para escribir un poco de código Lisp y verlo ejecutar. Sin embargo, eso es exactamente lo que se espera que haga en C #.
fuente
Frases como "mecanografía estática" y "mecanografía dinámica" se lanzan mucho, y las personas tienden a usar definiciones sutilmente diferentes, así que comencemos aclarando lo que queremos decir.
Considere un lenguaje que tiene tipos estáticos que se verifican en tiempo de compilación. Pero supongamos que un error de tipo genera solo una advertencia no fatal y, en tiempo de ejecución, todo está tipeado. Estos tipos estáticos son solo para conveniencia del programador y no afectan el codegen. Esto ilustra que la escritura estática no impone por sí misma ninguna limitación y no se excluye mutuamente con la escritura dinámica. (Objective-C se parece mucho a esto).
Pero la mayoría de los sistemas de tipo estático no se comportan de esta manera. Hay dos propiedades comunes de los sistemas de tipo estático que pueden imponer limitaciones:
El compilador puede rechazar un programa que contiene un error de tipo estático.
Esto es una limitación porque muchos programas seguros de tipo contienen necesariamente un error de tipo estático.
Por ejemplo, tengo un script de Python que necesita ejecutarse como Python 2 y Python 3. Algunas funciones cambiaron sus tipos de parámetros entre Python 2 y 3, por lo que tengo un código como este:
Un verificador de tipo estático Python 2 rechazaría el código Python 3 (y viceversa), aunque nunca se ejecutaría. Mi programa de tipo seguro contiene un error de tipo estático.
Como otro ejemplo, considere un programa Mac que quiera ejecutarse en OS X 10.6, pero aproveche las nuevas características en 10.7. Los métodos 10.7 pueden o no existir en tiempo de ejecución, y depende de mí, el programador, detectarlos. Un verificador de tipo estático se ve obligado a rechazar mi programa para garantizar la seguridad de tipo o aceptar el programa, junto con la posibilidad de producir un error de tipo (falta la función) en tiempo de ejecución.
La verificación de tipo estático supone que el entorno de tiempo de ejecución se describe adecuadamente mediante la información de tiempo de compilación. ¡Pero predecir el futuro es peligroso!
Aquí hay una limitación más:
El compilador puede generar código que asume que el tipo de tiempo de ejecución es el tipo estático.
Asumir que los tipos estáticos son "correctos" ofrece muchas oportunidades para la optimización, pero estas optimizaciones pueden ser limitantes. Un buen ejemplo son los objetos proxy, por ejemplo, la comunicación remota. Supongamos que desea tener un objeto proxy local que reenvíe invocaciones de métodos a un objeto real en otro proceso. Sería bueno si el proxy fuera genérico (para que pueda enmascararse como cualquier objeto) y transparente (para que el código existente no necesite saber que está hablando con un proxy). Pero para hacer esto, el compilador no puede generar código que suponga que los tipos estáticos son correctos, por ejemplo, mediante la inclusión de llamadas a métodos estáticos, porque eso fallará si el objeto es realmente un proxy.
Los ejemplos de dicha comunicación remota en acción incluyen NSXPCConnection de ObjC o TransparentProxy de C # (cuya implementación requirió algunas pesimizaciones en el tiempo de ejecución; consulte aquí para una discusión).
Cuando el codegen no depende de los tipos estáticos, y tiene instalaciones como el reenvío de mensajes, puede hacer muchas cosas interesantes con objetos proxy, depuración, etc.
Así que eso es una muestra de algunas de las cosas que puede hacer si no está obligado a satisfacer un verificador de tipo. Las limitaciones no están impuestas por los tipos estáticos, sino por la verificación forzada de tipos estáticos.
fuente
A Python 2 static type checker would reject the Python 3 code (and vice versa), even though it would never be executed. My type safe program contains a static type error.
En cualquier lenguaje estático razonable, puede hacerlo con unaIFDEF
declaración de preprocesador de tipo, mientras mantiene la seguridad de tipo en ambos casos.Las variables de tipo pato son lo primero en lo que todos piensan, pero en la mayoría de los casos puede obtener los mismos beneficios a través de la inferencia de tipo estático.
Pero escribir pato en colecciones creadas dinámicamente es difícil de lograr de otra manera:
Entonces, ¿qué tipo
JSON.parse
devuelve? ¿Un diccionario de matrices de enteros o diccionarios de cadenas? No, incluso eso no es lo suficientemente general.JSON.parse
tiene que devolver algún tipo de "valor de variante" que puede ser nulo, bool, flotante, cadena, matriz de cualquiera de estos tipos recursivamente, o diccionario de cadena a cualquiera de estos tipos recursivamente. Las principales fortalezas de la tipificación dinámica provienen de tener tales tipos de variantes.Hasta ahora, este es un beneficio de los tipos dinámicos , no de los lenguajes de tipo dinámico. Un lenguaje estático decente puede simular cualquier tipo de ese tipo perfectamente. (E incluso los lenguajes "malos" a menudo pueden simularlos al romper la seguridad del tipo bajo el capó y / o requerir una sintaxis de acceso torpe).
La ventaja de los lenguajes de tipo dinámico es que dichos tipos no pueden inferirse mediante sistemas de inferencia de tipos estáticos. Tienes que escribir el tipo explícitamente. Pero en muchos de estos casos, incluida esta vez, el código para describir el tipo es exactamente tan complicado como el código para analizar / construir los objetos sin describir el tipo, por lo que todavía no es necesariamente una ventaja.
fuente
Como cada sistema de tipo estático remotamente práctico está muy limitado en comparación con el lenguaje de programación que le concierne, no puede expresar todos los invariantes que el código podría verificar en tiempo de ejecución. Para no eludir las garantías que un sistema de tipos intenta dar, por lo tanto, opta por ser conservador y no permitir los casos de uso que pasarían estas verificaciones, pero no se puede probar (en el sistema de tipos).
Haré un ejemplo. Suponga que implementa un modelo de datos simple para describir objetos de datos, colecciones de ellos, etc., que está tipado estáticamente en el sentido de que, si el modelo dice que el atributo
x
del objeto de tipo Foo contiene un número entero, siempre debe contener un número entero. Como se trata de una construcción en tiempo de ejecución, no puede escribirla estáticamente. Supongamos que almacena los datos descritos en archivos YAML. Usted crea un mapa hash (para ser entregado a una biblioteca YAML más tarde), obtiene elx
atributo, lo almacena en el mapa, obtiene ese otro atributo que resulta ser una cadena, ... ¿espera un segundo? ¿Cuál es el tipo dethe_map[some_key]
ahora? Bueno, dispara, sabemos quesome_key
es así'x'
y el resultado debe ser un número entero, pero el sistema de tipos ni siquiera puede comenzar a razonar sobre esto.Algunos sistemas de tipos investigados activamente pueden funcionar para este ejemplo específico, pero estos son extremadamente complicados (tanto para que los escritores de compiladores los implementen como para que el programador razone), especialmente para algo tan "simple" (quiero decir, lo acabo de explicar en uno párrafo).
Por supuesto, la solución de hoy es boxear todo y luego emitir (o tener un montón de métodos anulados, la mayoría de los cuales generan excepciones "no implementadas"). Pero esto no está estáticamente escrito, es un truco alrededor del sistema de tipos para hacer las verificaciones de tipos en tiempo de ejecución.
fuente
No hay nada que pueda hacer con la escritura dinámica que no pueda hacer con la escritura estática, porque puede implementar la escritura dinámica sobre un lenguaje escrito estáticamente.
Un breve ejemplo en Haskell:
Con suficientes casos puede implementar cualquier sistema de tipo dinámico dado.
Por el contrario, también puede traducir cualquier programa estáticamente escrito en uno dinámico equivalente. Por supuesto, perdería todas las garantías de corrección en tiempo de compilación que proporciona el lenguaje estáticamente tipado.
Editar: quería mantener esto simple, pero aquí hay más detalles sobre un modelo de objetos
Una función toma una lista de datos como argumentos y realiza cálculos con efectos secundarios en ImplMonad, y devuelve un dato.
DMember
es un valor de miembro o una función.Extienda
Data
para incluir objetos y funciones. Los objetos son listas de miembros nombrados.Estos tipos estáticos son suficientes para implementar todos los sistemas de objetos de tipo dinámico con los que estoy familiarizado.
fuente
Data
.+
operador que combina dosData
valores en otroData
valor.Data
representa los valores estándar en el sistema de tipo dinámico.Membranas :
Cada tipo está envuelto por un tipo que tiene la misma interfaz, pero que intercepta mensajes y envuelve y desenvuelve valores a medida que cruzan la membrana. ¿Cuál es el tipo de la función de ajuste en su idioma estático favorito? Quizás Haskell tiene un tipo para esas funciones, pero la mayoría de los lenguajes tipados estáticamente no lo hacen o terminan usando Object → Object, renunciando efectivamente a su responsabilidad como verificadores de tipo.
fuente
class Foo a where ...
data Wrapper = forall a. Foo a => Wrapper a
String
ya que es un tipo concreto en Java. Smalltalk no tiene este problema porque no intenta escribir#doesNotUnderstand
.Como alguien mencionó, en teoría no hay mucho que pueda hacer con la escritura dinámica que no podría hacer con la escritura estática si implementara ciertos mecanismos por su cuenta. La mayoría de los lenguajes proporcionan mecanismos de relajación de tipos para admitir la flexibilidad de tipos, como punteros vacíos y el tipo de objeto raíz o interfaz vacía.
La mejor pregunta es por qué la escritura dinámica es más adecuada y más apropiada en ciertas situaciones y problemas.
Primero, definamos
Entidad : necesitaría una noción general de alguna entidad en el código. Puede ser cualquier cosa, desde un número primitivo hasta datos complejos.
Comportamiento : digamos que nuestra entidad tiene algún estado y un conjunto de métodos que permiten al mundo exterior instruir a la entidad ante ciertas reacciones. Llamemos al estado + interfaz de esta entidad su comportamiento. Una entidad puede tener más de un comportamiento combinado de cierta manera por las herramientas que proporciona el lenguaje.
Definiciones de entidades y sus comportamientos : cada lenguaje proporciona algunos medios de abstracción que lo ayudan a definir comportamientos (conjunto de métodos + estado interno) de ciertas entidades en el programa. Puede asignar un nombre a estos comportamientos y decir que todas las instancias que tienen este comportamiento son de cierto tipo .
Esto es probablemente algo que no es tan desconocido. Y como dijiste, entendiste la diferencia, pero aún así. Probablemente no sea una explicación completa y más precisa, pero espero que sea lo suficientemente divertida como para aportar algo de valor :)
Escritura estática : el comportamiento de todas las entidades en su programa se examina en tiempo de compilación, antes de que el código comience a ejecutarse. Esto significa que si desea, por ejemplo, que su entidad de tipo Persona tenga un comportamiento (para comportarse como) Magician, entonces debería definir la entidad MagicianPerson y darle comportamientos de un mago como throwMagic (). Si está en su código, dígale por error al compilador Person.throwMagic () normal que le dirá
"Error >>> hell, this Person has no this behavior, dunno throwing magics, no run!".
Escritura dinámica : en los entornos de escritura dinámica, los comportamientos disponibles de las entidades no se verifican hasta que realmente intente hacer algo con cierta entidad. Ejecutar el código Ruby que le pide a Person.throwMagic () no se detectará hasta que su código realmente llegue allí. Esto suena frustrante, ¿no es así? Pero también suena revelador. Según esta propiedad, puede hacer cosas interesantes. Por ejemplo, digamos que diseñas un juego en el que cualquier cosa puede recurrir a Magician y realmente no sabes quién será, hasta que llegues a cierto punto del código. Y luego viene Frog y tú dices
HeyYouConcreteInstanceOfFrog.include Magic
y a partir de entonces esta rana se convierte en una rana particular que tiene poderes mágicos. Otras ranas, todavía no. Verá, en lenguajes de escritura estáticos, tendría que definir esta relación por algún medio estándar de combinación de comportamientos (como la implementación de la interfaz). En el lenguaje de escritura dinámico, puede hacerlo en tiempo de ejecución y a nadie le importará.La mayoría de los lenguajes de escritura dinámica tienen mecanismos para proporcionar un comportamiento genérico que capturará cualquier mensaje que se pase a su interfaz. Por ejemplo Ruby
method_missing
y PHP__call
si recuerdo bien. Eso significa que puede hacer cualquier tipo de cosas interesantes en tiempo de ejecución del programa y tomar una decisión de tipo basada en el estado actual del programa. Esto trae herramientas para modelar un problema que son mucho más flexibles que, por ejemplo, en un lenguaje de programación estático conservador como Java.fuente