He leído una pregunta relacionada. ¿Hay patrones de diseño innecesarios en lenguajes dinámicos como Python? y recordé esta cita en Wikiquote.org
Lo maravilloso de la escritura dinámica es que te permite expresar cualquier cosa que sea computable. Y los sistemas de tipos no: los sistemas de tipos suelen ser decidibles y lo restringen a un subconjunto. Las personas que prefieren los sistemas de tipo estático dicen "está bien, es lo suficientemente bueno; todos los programas interesantes que quieras escribir funcionarán como tipos ". Pero eso es ridículo: una vez que tiene un sistema de tipos, ni siquiera sabe qué programas interesantes hay.
--- Software Engineering Radio Episode 140: Newspeak y tipos conectables con Gilad Bracha
Me pregunto, ¿existen patrones o estrategias de diseño útiles que, utilizando la formulación de la cita, "no funcionen como tipos"?
Respuestas:
Tipos de primera clase
La escritura dinámica significa que tiene tipos de primera clase: puede inspeccionar, crear y almacenar tipos en tiempo de ejecución, incluidos los tipos propios del idioma. También significa que los valores están escritos, no variables .
El lenguaje de tipo estático puede producir código que también se basa en tipos dinámicos, como el envío de métodos, clases de tipos, etc., pero de una manera que generalmente es invisible para el tiempo de ejecución. En el mejor de los casos, le brindan alguna forma de realizar una introspección. Alternativamente, puede simular tipos como valores, pero luego tiene un sistema de tipo dinámico ad-hoc.
Sin embargo, los sistemas de tipos dinámicos rara vez tienen solo tipos de primera clase. Puede tener símbolos de primera clase, paquetes de primera clase, primera clase ... todo. Esto contrasta con la estricta separación entre el lenguaje del compilador y el lenguaje de tiempo de ejecución en lenguajes estáticamente tipados. Lo que puede hacer el compilador o el intérprete también puede hacerlo el tiempo de ejecución.
Ahora, aceptemos que la inferencia de tipos es algo bueno y que me gusta que comprueben mi código antes de ejecutarlo. Sin embargo, también me gusta poder producir y compilar código en tiempo de ejecución. Y también me encanta calcular cosas en tiempo de compilación. En un lenguaje de tipo dinámico, esto se hace con el mismo idioma. En OCaml, tiene el sistema de tipo módulo / functor, que es diferente del sistema de tipo principal, que es diferente del lenguaje del preprocesador. En C ++, tiene el lenguaje de plantilla que no tiene nada que ver con el lenguaje principal, que generalmente ignora los tipos durante la ejecución. Y eso está bien en esos idiomas, porque no quieren proporcionar más.
En última instancia, eso no cambia realmente qué tipo de software puede desarrollar, pero la expresividad cambia la forma en que los desarrolla y si es difícil o no.
Patrones
Los patrones que se basan en tipos dinámicos son patrones que involucran entornos dinámicos: clases abiertas, despacho, bases de datos en memoria de objetos, serialización, etc. Cosas simples como contenedores genéricos funcionan porque un vector no olvida en tiempo de ejecución el tipo de objetos que contiene (sin necesidad de tipos paramétricos).
Traté de presentar las muchas formas en que se evalúa el código en Common Lisp, así como ejemplos de posibles análisis estáticos (esto es SBCL). El ejemplo de sandbox compila un pequeño subconjunto de código Lisp obtenido de un archivo separado. Para estar razonablemente seguro, cambio la tabla de lectura, permito solo un subconjunto de símbolos estándar y envuelvo las cosas con un tiempo de espera.
Nada de lo anterior es "imposible" de hacer con otros idiomas. El enfoque de plug-in en Blender, en software de música o IDEs para lenguajes compilados estáticamente que hacen una compilación sobre la marcha, etc. En lugar de herramientas externas, los lenguajes dinámicos favorecen herramientas que hacen uso de información que ya está allí. ¿Todas las llamadas conocidas de FOO? todas las subclases de BAR? todos los métodos que están especializados por clase ZOT? Estos son datos internalizados. Los tipos son solo otro aspecto de esto.
(ver también: CFFI )
fuente
Respuesta corta: no, porque la equivalencia de Turing.
Respuesta larga: Este tipo está siendo un troll. Si bien es cierto que los sistemas de tipos "lo restringen a un subconjunto", las cosas fuera de ese subconjunto son, por definición, cosas que no funcionan.
Cualquier cosa que pueda hacer en cualquier lenguaje de programación completo de Turing (que es un lenguaje diseñado para programación de propósito general, además de muchas cosas que no lo son; es una barra bastante baja para borrar y hay varios ejemplos de un sistema que se convierte en Turing- completa sin querer) que puede hacer en cualquier otro lenguaje de programación completo de Turing. Esto se llama "equivalencia de Turing" y solo significa exactamente lo que dice. Es importante destacar que no significa que pueda hacer la otra cosa con la misma facilidad en el otro idioma; algunos argumentarán que ese es el objetivo de crear un nuevo lenguaje de programación en primer lugar: brindarle una mejor manera de hacer ciertos cosas que los idiomas existentes apestan.
Un sistema de tipo dinámico, por ejemplo, se puede emular encima de un sistema de tipo OO estático simplemente declarando todas las variables, parámetros y valores de retorno como el
Object
tipo base y luego usando la reflexión para acceder a los datos específicos, así que cuando se dé cuenta de esto ve que literalmente no hay nada que pueda hacer en un lenguaje dinámico que no pueda hacer en un lenguaje estático. Pero hacerlo de esa manera sería un gran desastre, por supuesto.El tipo de la cita es correcto: los tipos estáticos restringen lo que puedes hacer, pero esa es una característica importante, no un problema. Las líneas en el camino restringen lo que puede hacer en su automóvil, pero ¿las encuentra restrictivas o útiles? (¡Sé que no me gustaría conducir en una carretera concurrida y compleja donde no hay nada que indique a los autos que van en la dirección opuesta para mantenerse a su lado y no venir a donde estoy conduciendo!) Al establecer reglas que delineen claramente lo que Si se considera un comportamiento no válido y se asegura de que no suceda, disminuye en gran medida las posibilidades de que ocurra un bloqueo desagradable.
Además, está describiendo mal el otro lado. No es que "todos los programas interesantes que desea escribir funcionen como tipos", sino que "todos los programas interesantes que desea escribir requerirán tipos". Una vez que superas un cierto nivel de complejidad, se vuelve muy difícil mantener la base de código sin un sistema de tipos para mantenerte en línea, por dos razones.
Primero, porque el código sin anotaciones de tipo es difícil de leer. Considere el siguiente Python:
¿Cómo espera que se vean los datos que recibe el sistema en el otro extremo de la conexión? Y si está recibiendo algo que se ve completamente mal, ¿cómo se da cuenta de lo que está pasando?
Todo depende de la estructura de
value.someProperty
. ¿Pero a qué se parece? ¡Buena pregunta! ¿Cuál es la llamadasendData()
? ¿Qué está pasando? ¿Cómo se ve esa variable? ¿De dónde vino? Si no es local, debe rastrear todo el historial devalue
para rastrear lo que está sucediendo. ¿Quizás estás pasando algo más que también tiene unasomeProperty
propiedad, pero no hace lo que crees que hace?Ahora veámoslo con anotaciones de tipo, como puede ver en el lenguaje Boo, que usa una sintaxis muy similar pero está estáticamente escrita:
Si algo sale mal, de repente su trabajo de depuración se vuelve más fácil: ¡busque la definición de
MyDataType
! Además, la posibilidad de tener un mal comportamiento porque pasó algún tipo incompatible que también tiene una propiedad con el mismo nombre de repente se reduce a cero, porque el sistema de tipos no le permitirá cometer ese error.La segunda razón se basa en la primera: en un proyecto grande y complejo, lo más probable es que tenga múltiples contribuyentes. (Y si no, lo está construyendo usted mismo durante mucho tiempo, que es esencialmente lo mismo. ¡Intente leer el código que escribió hace 3 años si no me cree!) Esto significa que no sabe lo que era pasar por la cabeza de la persona que escribió casi cualquier parte del código en el momento en que lo escribió, porque usted no estaba allí, o no recuerda si fue su propio código hace mucho tiempo. ¡Tener declaraciones de tipo realmente te ayuda a comprender cuál era la intención del código!
Las personas como el tipo en la cita con frecuencia describen erróneamente los beneficios de la escritura estática como "ayudar al compilador" o "todo sobre la eficiencia" en un mundo donde los recursos de hardware casi ilimitados hacen que esto sea cada vez menos relevante cada año que pasa. Pero como he demostrado, aunque esos beneficios ciertamente existen, el beneficio principal está en los factores humanos, particularmente la legibilidad y la facilidad de mantenimiento del código. (¡Sin embargo, la eficiencia adicional es ciertamente una buena ventaja!)
fuente
Voy a eludir la parte del "patrón" porque creo que se convierte en la definición de lo que es o no es un patrón y hace mucho que pierdo interés en ese debate. Lo que diré es que hay cosas que puedes hacer en algunos idiomas que no puedes hacer en otros. Quiero que quede claro, yo estoy no diciendo que hay problemas que puede resolver en un idioma que no se puede resolver en otro. Mason ya ha señalado la integridad de Turing.
Por ejemplo, he escrito una clase en python que toma un elemento DOM XML y lo convierte en un objeto de primera clase. Es decir, puedes escribir el código:
y tiene el contenido de esa ruta desde un objeto XML analizado. algo ordenado y ordenado, en mi opinión. Y si no hay un nodo principal, solo devuelve objetos ficticios que no contienen nada más que objetos ficticios (tortugas completamente hacia abajo). No hay una forma real de hacerlo en, digamos, Java. Tendría que haber compilado una clase con anticipación que se basara en algún conocimiento de la estructura del XML. Dejando a un lado si es una buena idea, este tipo de cosas realmente cambia la forma en que resuelves los problemas en un lenguaje dinámico. Sin embargo, no digo que cambie de una manera que siempre es necesariamente mejor. Hay algunos costos definidos para los enfoques dinámicos y la respuesta de Mason ofrece una visión general decente. Si son una buena opción depende de muchos factores.
En una nota al margen, puede hacer esto en Java porque puede construir un intérprete de Python en Java . El hecho de que resolver un problema específico en un idioma determinado puede significar construir un intérprete o algo similar a menudo se pasa por alto cuando las personas hablan sobre la integridad de Turing.
fuente
IDynamicMetaObjectProvider
, y es muy simple en Boo. ( Aquí hay una implementación en menos de 100 líneas, incluida como parte del árbol fuente estándar en GitHub, ¡porque es así de fácil!)"IDynamicMetaObjectProvider"
? ¿Está relacionado con ladynamic
palabra clave de C # ? ... que efectivamente solo agrega tipeo dinámico a C #? No estoy seguro de que su argumento sea válido si tengo razón.dynamic
logra en C #. Las "búsquedas de reflexión y diccionario" suceden en tiempo de ejecución, no en tiempo de compilación. Realmente no estoy seguro de cómo puede argumentar que no agrega escritura dinámica al idioma. Mi punto es que el último párrafo de Jimmy cubre eso.La cita es correcta, pero también muy falsa. Vamos a desglosarlo para ver por qué:
Bueno, no del todo. Un lenguaje con mecanografía dinámica le permite expresar cualquier cosa siempre que Turing esté completo , lo cual es la mayoría. El sistema de tipos en sí no te permite expresarlo todo. Vamos a darle el beneficio de la duda aquí sin embargo.
Esto es cierto, pero observe que ahora estamos hablando firmemente de lo que permite el sistema de tipos , no de lo que permite el lenguaje que usa un sistema de tipos. Si bien es posible usar un sistema de tipos para calcular cosas en tiempo de compilación, esto generalmente no está completo en Turing (ya que el sistema de tipos es generalmente decidible), pero casi cualquier lenguaje estáticamente tipado también está completo en su tiempo de ejecución (los idiomas con tipeo dependiente son no, pero no creo que estemos hablando de ellos aquí).
El problema es que los idiomas de tipos dinámicos tienen un tipo estático. A veces todo es una cadena, y más comúnmente hay una unión etiquetada donde cada cosa es una bolsa de propiedades o un valor como un int o un doble. El problema es que los lenguajes estáticos también pueden hacer esto, históricamente fue un poco complicado hacer esto, pero los lenguajes modernos de tipo estático hacen que esto sea tan fácil de hacer como usar un lenguaje de tipos dinámicos, entonces, ¿cómo puede haber una diferencia en ¿Qué puede ver el programador como un programa interesante? Los lenguajes estáticos tienen exactamente las mismas uniones etiquetadas, así como otros tipos.
Para responder la pregunta en el título: No, no hay patrones de diseño que no se puedan implementar en un lenguaje de tipo estático, porque siempre se puede implementar un sistema dinámico suficiente para obtenerlos. Puede haber patrones que se obtienen de forma gratuita en un lenguaje dinámico; Esto puede o no valer la pena soportar las desventajas de esos idiomas para YMMV .
fuente
Seguramente hay cosas que solo puede hacer en idiomas escritos dinámicamente. Pero no serían necesariamente un buen diseño.
Puede asignar primero un número entero 5 y luego una cadena
'five'
, o unCat
objeto, a la misma variable. Pero solo hace que sea más difícil para un lector de su código descubrir qué está sucediendo, cuál es el propósito de cada variable.Puede agregar un nuevo método a una clase de biblioteca Ruby y acceder a sus campos privados. Puede haber casos en los que tal pirateo pueda ser útil, pero esto sería una violación de la encapsulación. (No me importa agregar métodos que solo se basan en la interfaz pública, pero eso no es nada que los métodos de extensión C # tipados estáticamente no puedan hacer).
Puede agregar un nuevo campo a un objeto de la clase de otra persona para pasar algunos datos adicionales con él. Pero es mejor diseñar simplemente crear una nueva estructura o extender el tipo original.
En general, cuanto más organizado desee que permanezca su código, menos ventajas tendrá de poder cambiar dinámicamente las definiciones de tipos o asignar valores de diferentes tipos a la misma variable. Pero entonces su código no es diferente de lo que podría lograr en un lenguaje de tipo estático.
En qué lenguajes dinámicos son buenos es el azúcar sintáctico. Por ejemplo, al leer un objeto JSON deserializado, puede referirse a un valor anidado simplemente como
obj.data.article[0].content
mucho más ordenado que decirobj.getJSONObject("data").getJSONArray("article").getJSONObject(0).getString("content")
.Los desarrolladores de Ruby especialmente podrían hablar extensamente sobre la magia que se puede lograr mediante la implementación
method_missing
, que es un método que le permite manejar llamadas intentadas a métodos no declarados. Por ejemplo, ActiveRecord ORM lo usa para que pueda hacer una llamadaUser.find_by_email('[email protected]')
sin tener que declarar elfind_by_email
método. Por supuesto, no es nada que no se pueda lograr comoUserRepository.FindBy("email", "[email protected]")
en un lenguaje estáticamente tipado, pero no se puede negar su pulcritud.fuente
El patrón de proxy dinámico es un acceso directo para implementar objetos proxy sin necesidad de una clase por tipo que necesita proxy.
Usando esto,
Proxy(someObject)
crea un nuevo objeto que se comporta igual quesomeObject
. Obviamente, también querrás agregar funcionalidad adicional de alguna manera, pero esta es una base útil para comenzar. En un lenguaje estático completo, necesitaría escribir una clase de Proxy por tipo que desea usar como proxy o usar la generación de código dinámico (que, ciertamente, se incluye en la biblioteca estándar de muchos lenguajes estáticos, en gran parte porque sus diseñadores están al tanto de los problemas de no poder hacer esta causa).Otro caso de uso de lenguajes dinámicos es el llamado "parche de mono". En muchos sentidos, este es un antipatrón en lugar de un patrón, pero se puede usar de formas útiles si se hace con cuidado. Y aunque no hay una razón teórica por la cual los parches de mono no se puedan implementar en un lenguaje estático, nunca he visto uno que realmente lo tenga.
fuente
Sí , hay muchos patrones y técnicas que solo son posibles en un lenguaje de tipo dinámico.
La aplicación de parches de mono es una técnica en la que se agregan propiedades o métodos a objetos o clases en tiempo de ejecución. Esta técnica no es posible en un lenguaje de tipo estático, ya que esto significa que los tipos y las operaciones no se pueden verificar en tiempo de compilación. O para decirlo de otra manera, si un lenguaje admite parches de mono, es, por definición, un lenguaje dinámico.
Se puede demostrar que si un lenguaje admite parches de mono (o técnicas similares para modificar tipos en tiempo de ejecución), no se puede verificar estáticamente los tipos. Por lo tanto, no es solo una limitación en los idiomas existentes actualmente, es una limitación fundamental de la escritura estática.
Por lo tanto, la cita es correcta: más cosas son posibles en un lenguaje dinámico que en un lenguaje estáticamente tipado. Por otro lado, ciertos tipos de análisis solo son posibles en un lenguaje de tipo estático. Por ejemplo, siempre sabe qué operaciones están permitidas en un tipo dado, lo que le permite detectar operaciones ilegales en el tipo de compilación. No es posible dicha verificación en un lenguaje dinámico cuando las operaciones se pueden agregar o eliminar en tiempo de ejecución.
Es por eso que no hay un "mejor" obvio en el conflicto entre lenguajes estáticos y dinámicos. Los lenguajes estáticos ceden cierto poder en tiempo de ejecución a cambio de un tipo diferente de poder en tiempo de compilación, que creen que reduce la cantidad de errores y facilita el desarrollo. Algunos creen que la compensación vale la pena, otros no.
Otras respuestas han argumentado que la equivalencia de Turing significa que todo lo posible en un idioma es posible en todos los idiomas. Pero esto no sigue. Para admitir algo como parches de mono en un lenguaje estático, básicamente tiene que implementar un sub-lenguaje dinámico dentro del lenguaje estático. Por supuesto, esto es posible, pero diría que está programando en un lenguaje dinámico incrustado, ya que también pierde la comprobación de tipo estático que existe en el idioma del host.
C # desde la versión 4 ha admitido objetos con tipo dinámico. Claramente, los diseñadores de lenguaje ven el beneficio de tener ambos tipos de tipeo disponibles. Pero también muestra que no puedes tener tu pastel y comerte yo también: cuando usas objetos dinámicos en C # obtienes la habilidad de hacer algo como parches de mono, pero también pierdes la verificación de tipo estático para la interacción con estos objetos.
fuente
Si y no.
Hay situaciones en las que el programador conoce el tipo de una variable con más precisión que un compilador. El compilador puede saber que algo es un Objeto, pero el programador sabrá (debido a los invariantes del programa) que en realidad es una Cadena.
Déjame mostrarte algunos ejemplos de esto:
Sé que
someMap.get(T.class)
devolverá unFunction<T, String>
, debido a cómo construí someMap. Pero Java solo está seguro de que tengo una función.Otro ejemplo:
Sé que data.properties.rowCount será una referencia válida y un número entero, porque he validado los datos contra un esquema. Si ese campo faltara, se habría lanzado una excepción. Pero un compilador solo sabría que está lanzando una excepción o devuelve algún tipo de JSONValue genérico.
Otro ejemplo:
El "II6s" define la forma en que los datos codifican tres variables. Como he especificado el formato, sé qué tipos se devolverán. Un compilador solo sabría que devuelve una tupla.
El tema unificador de todos estos ejemplos es que el programador conoce el tipo, pero un sistema de tipos de nivel Java no podrá reflejar eso. El compilador no sabrá los tipos y, por lo tanto, un lenguaje de tipo estático no me permitirá llamarlos, mientras que un lenguaje de tipo dinámico sí.
A eso se refiere la cita original:
Cuando uso la escritura dinámica, puedo usar el tipo más derivado que conozco, no simplemente el tipo más derivado que conoce el sistema de tipos de mi idioma. En todos los casos anteriores, tengo un código que es semánticamente correcto, pero será rechazado por un sistema de escritura estático.
Sin embargo, para volver a su pregunta:
Cualquiera de los ejemplos anteriores y, de hecho, cualquier ejemplo de tipeo dinámico puede hacerse válido en tipeo estático agregando los moldes apropiados. Si conoce un tipo que su compilador no conoce, simplemente dígale al compilador mediante el valor. Entonces, en algún nivel, no obtendrá ningún patrón adicional mediante el uso de la escritura dinámica. Es posible que necesite emitir más para comenzar a trabajar con código escrito estáticamente.
La ventaja del tipeo dinámico es que simplemente puede usar estos patrones sin preocuparse por el hecho de que es difícil convencer a su sistema de tipos de su validez. No cambia los patrones disponibles, simplemente los hace más fáciles de implementar porque no tiene que descubrir cómo hacer que su sistema de tipos reconozca el patrón o agregue conversiones para subvertir el sistema de tipos.
fuente
data = parseJSON<SomeSchema>(someJson); print(data.properties.rowCount);
y si uno no tiene una clase para deserializar, podemos recurrir a éldata = parseJSON(someJson); print(data["properties.rowCount"]);
, que todavía está escrito y expresa la misma intención.data.properties
era un objeto y sabía quedata.properties.rowCount
era un número entero y simplemente podía escribir código que los usara. Su propuestadata["properties.rowCount"]
no proporciona lo mismo.Aquí hay algunos ejemplos de Objective-C (de tipo dinámico) que no son posibles en C ++ (de tipo estático):
Poner objetos de varias clases distintas en el mismo contenedor.
Por supuesto, esto requiere una inspección de tipo de tiempo de ejecución para interpretar posteriormente el contenido del contenedor, y la mayoría de los amigos de la escritura estática objetarán que no debería hacerlo en primer lugar. Pero he descubierto que, más allá de los debates religiosos, esto puede ser útil.
Expandir una clase sin subclases.
En Objective-C, puede definir nuevas funciones miembro para las clases existentes, incluidas las definidas por lenguaje como
NSString
. Por ejemplo, puede agregar un métodostripPrefixIfPresent:
, de modo que pueda decir[@"foo/bar/baz" stripPrefixIfPresent:@"foo/"]
(tenga en cuenta el uso de losNSSring
literales@""
).Uso de devoluciones de llamada orientadas a objetos.
En lenguajes tipados estáticamente como Java y C ++, debe hacer un esfuerzo considerable para permitir que una biblioteca llame a un miembro arbitrario de un objeto suministrado por el usuario. En Java, la solución es el par de interfaz / adaptador más una clase anónima, en C ++ la solución suele estar basada en plantillas, lo que implica que el código de la biblioteca debe exponerse al código del usuario. En Objective-C, simplemente pasa la referencia del objeto más el selector del método a la biblioteca, y la biblioteca puede invocar la devolución de llamada de manera simple y directa.
fuente
void*
sí solo no es una escritura dinámica, es una falta de escritura. Pero sí, dynamic_cast, tablas virtuales, etc. hacen que C ++ no esté meramente estáticamente tipado. ¿Es tan malo?void*
tipo de objeto específico. El primero produce un error de tiempo de ejecución si se equivoca, el último da como resultado un comportamiento indefinido.