¿Hay patrones de diseño que solo sean posibles en lenguajes de tipo dinámico como Python?

30

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"?

user7610
fuente
3
He descubierto que el despacho doble y el patrón de visitante son muy difíciles de lograr en lenguajes de tipo estático, pero fácilmente realizables en lenguajes dinámicos. Vea esta respuesta (y la pregunta) por ejemplo: programmers.stackexchange.com/a/288153/122079
user3002473
77
Por supuesto. Cualquier patrón que implique crear nuevas clases en tiempo de ejecución, por ejemplo. (eso también es posible en Java, pero no en C ++; hay una escala móvil de dinamismo).
user253751
1
Dependería mucho de lo sofisticado que sea su sistema de tipos :-) Los lenguajes funcionales generalmente funcionan bastante bien en esto.
Bergi
1
Todo el mundo parece estar hablando de sistemas de tipos como Java y C # en lugar de Haskell u OCaml. Un lenguaje con un poderoso sistema de tipos puede ser tan conciso como un lenguaje dinámico pero mantener la seguridad de los tipos.
Andrew dice Reinstate Monica
@immibis Eso es incorrecto. Los sistemas de tipo estático pueden crear absolutamente nuevas clases "dinámicas" en tiempo de ejecución. Consulte el Capítulo 33 de Fundamentos prácticos para lenguajes de programación.
cabeza de jardín

Respuestas:

4

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.

;;
;; Fetching systems, installing them, etc. 
;; ASDF and QL provide provide resp. a Make-like facility 
;; and system management inside the runtime: those are
;; not distinct programs.
;; Reflexivity allows to develop dedicated tools: for example,
;; being able to find the transitive reduction of dependencies
;; to parallelize builds. 
;; https://gitlab.common-lisp.net/xcvb/asdf-dependency-grovel
;;
(ql:quickload 'trivial-timeout)

;;
;; Readtables are part of the runtime.
;; See also NAMED-READTABLES.
;;
(defparameter *safe-readtable* (copy-readtable *readtable*))
(set-macro-character #\# nil t *safe-readtable*)
(set-macro-character #\: (lambda (&rest args)
                           (declare (ignore args))
                           (error "Colon character disabled."))
                     nil
                     *safe-readtable*)

;; eval-when is necessary when compiling the whole file.
;; This makes the result of the form available in the compile-time
;; environment. 
(eval-when (:compile-toplevel :load-toplevel :execute)
  (defvar +WHITELISTED-LISP-SYMBOLS+ 
    '(+ - * / lambda labels mod rem expt round 
      truncate floor ceiling values multiple-value-bind)))

;;
;; Read-time evaluation #.+WHITELISTED-LISP-SYMBOLS+
;; The same language is used to control the reader.
;;
(defpackage :sandbox
  (:import-from
   :common-lisp . #.+WHITELISTED-LISP-SYMBOLS+)
  (:export . #.+WHITELISTED-LISP-SYMBOLS+))

(declaim (inline read-sandbox))

(defun read-sandbox (stream &key (timeout 3))
  (declare (type (integer 0 10) timeout))
  (trivial-timeout:with-timeout (timeout)
    (let ((*read-eval* nil)
          (*readtable* *safe-readtable*)
          ;;
          ;; Packages are first-class: no possible name collision.
          ;;
          (package (make-package (gensym "SANDBOX") :use '(:sandbox))))
      (unwind-protect
           (let ((*package* package))
             (loop
                with stop = (gensym)
                for read = (read stream nil stop)
                until (eq read stop)
                ;;
                ;; Eval at runtime
                ;;
                for value = (eval read)
                ;;
                ;; Type checking
                ;;
                unless (functionp value)
                do (error "Not a function")
                ;; 
                ;; Compile at run-time
                ;;
                collect (compile nil value)))
        (delete-package package)))))

;;
;; Static type checking.
;; warning: Constant 50 conflicts with its asserted type (MOD 11)
;;
(defun read-sandbox-file (file)
  (with-open-file (in file)
    (read-sandbox in :timeout 50)))

;; get it right, this time
(defun read-sandbox-file (file)
  (with-open-file (in file)
    (read-sandbox in)))

#| /tmp/plugin.lisp
(lambda (x) (+ (* 3 x) 100))
(lambda (a b c) (* a b))
|#

(read-sandbox-file #P"/tmp/plugin.lisp")

;; 
;; caught COMMON-LISP:STYLE-WARNING:
;;   The variable C is defined but never used.
;;

(#<FUNCTION (LAMBDA (#:X)) {10068B008B}>
 #<FUNCTION (LAMBDA (#:A #:B #:C)) {10068D484B}>)

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 )

volcado de memoria
fuente
39

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 Objecttipo 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:

def sendData(self, value):
   self.connection.send(serialize(value.someProperty))

¿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 llamada sendData()? ¿Qué está pasando? ¿Cómo se ve esa variable? ¿De dónde vino? Si no es local, debe rastrear todo el historial de valuepara rastrear lo que está sucediendo. ¿Quizás estás pasando algo más que también tiene una somePropertypropiedad, 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:

def SendData(value as MyDataType):
   self.Connection.Send(Serialize(value.SomeProperty))

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!)

Mason Wheeler
fuente
24
"Este tipo está siendo un troll". - No estoy seguro de que un ataque ad hominem vaya a ayudar a su caso bien presentado. Y si bien soy consciente de que el argumento de la autoridad es una falacia igualmente mala como ad hominem, todavía me gustaría señalar que Gilad Bracha probablemente ha diseñado más idiomas y (lo más relevante para esta discusión) más sistemas de tipos estáticos que la mayoría. Solo un pequeño extracto: es el único diseñador de Newspeak, co-diseñador de Dart, coautor de Java Language Specification y Java Virtual Machine Specification, trabajó en el diseño de Java y JVM, diseñado ...
Jörg W Mittag
10
Strongtalk (un sistema de tipo estático para Smalltalk), el sistema de tipo Dart, el sistema de tipo Newspeak, su tesis doctoral sobre modularidad es la base de casi todos los sistemas de módulos modernos (por ejemplo, Java 9, ECMAScript 2015, Scala, Dart, Newspeak, Ioke , Seph), su (s) trabajo (s) sobre mixins revolucionó la forma en que pensamos sobre ellos. Ahora, eso no significa que tenga razón, pero creo que haber diseñado múltiples sistemas de tipo estático lo hace un poco más que un "troll".
Jörg W Mittag
17
"Si bien es cierto que los sistemas de tipos" lo restringen a un subconjunto, "lo que está fuera de ese subconjunto es, por definición, lo que no funciona". - Esto está mal. Sabemos por la Indecidibilidad del problema de detención, el Teorema de Rice y la miríada de otros resultados de Indecidibilidad e Incomputabilidad que un verificador de tipo estático no puede decidir para todos los programas si son de tipo seguro o inseguro. No puede aceptar esos programas (algunos de los cuales no son seguros para los tipos), por lo que la única opción sensata es rechazarlos (sin embargo, algunos de ellos son seguros para los tipos). Alternativamente, el lenguaje debe diseñarse en ...
Jörg W Mittag el
99
... de tal manera que sea imposible para el programador escribir esos programas indecidibles, pero de nuevo, algunos de ellos son realmente de tipo seguro. Por lo tanto, no importa cómo lo divida: el programador no puede escribir programas de escritura segura. Y puesto que en realidad hay infinitamente muchos de ellos (por lo general), podemos estar casi seguro de que al menos algunos de ellos no son solamente las cosas que hace el trabajo, pero también es útil.
Jörg W Mittag
8
@MasonWheeler: el problema de detención aparece todo el tiempo, precisamente en el contexto de la verificación de tipos estáticos. A menos que los lenguajes estén cuidadosamente diseñados para evitar que el programador escriba ciertos tipos de programas, la verificación de tipos estáticos se convierte rápidamente en equivalente a resolver el problema de detención. O bien se termina con los programas que no pueden escribir, ya que podría confundir el tipo de corrector, o terminar con los programas que se pueden escribir pero ellos tienen una cantidad infinita de tiempo para verificación de tipos.
Jörg W Mittag
27

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:

doc.header.status.text()

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.

JimmyJames
fuente
44
No puede hacer esto en Java porque Java está mal diseñado. No sería tan difícil en C # usando 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!)
Mason Wheeler
66
@MasonWheeler "IDynamicMetaObjectProvider"? ¿Está relacionado con la dynamicpalabra 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.
jpmc26
99
@MasonWheeler Te estás metiendo en la semántica. Sin entrar en un debate sobre minucias (no estamos desarrollando un formalismo matemático sobre SE aquí), la tipificación dinámica es la práctica de tomar decisiones de tiempo de compilación en torno a los tipos, especialmente la verificación de que cada tipo tiene los miembros particulares a los que accede el programa. Ese es el objetivo que se dynamiclogra 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.
jpmc26
44
A pesar de no ser un gran admirador de Java, también me atrevo a decir que llamar a Java "mal diseñado" específicamente porque no agregó tipeo dinámico es ... demasiado entusiasta.
jpmc26
55
Además de la sintaxis un poco más conveniente, ¿en qué se diferencia de un diccionario?
Theodoros Chatzigiannakis
10

La cita es correcta, pero también muy falsa. Vamos a desglosarlo para ver por qué:

Lo maravilloso de la escritura dinámica es que te permite expresar cualquier cosa que sea computable.

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.

Y los sistemas de tipos no: los sistemas de tipos suelen ser decidibles y lo restringen a un subconjunto.

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í).

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.

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 .

jk.
fuente
2
No estoy completamente seguro de si acabas de responder sí o no. Suena más como un no para mí.
user7610
1
@TheodorosChatzigiannakis Sí, ¿de qué otra forma se implementarían los lenguajes dinámicos? Primero, pasarás por un arquitecto astronauta si alguna vez quieres implementar un sistema de clase dinámico o cualquier otra cosa un poco involucrada. En segundo lugar, es probable que no tenga el recurso para hacerlo depurable, totalmente introspectable, eficiente ("solo use un diccionario" es cómo se implementan los idiomas lentos). En tercer lugar, se utilizan algunas de las características dinámicas mejor cuando está integrado en todo el lenguaje, no sólo como una biblioteca: pensar en la recolección de basura, por ejemplo (no son los GC como bibliotecas, pero no son de uso general).
coredump
1
@Theodoros Según el artículo que ya vinculé aquí una vez, todas menos el 2.5% de las estructuras (en los módulos de Python que estudiaron las investigaciones) se pueden expresar fácilmente en un lenguaje mecanografiado. Quizás el 2.5% hace que valga la pena pagar los costos de la escritura dinámica. De eso se trataba esencialmente mi pregunta. neverworkintheory.org/2016/06/13/polymorphism-in-python.html
user7610
3
@JiriDanek Por lo que puedo decir, no hay nada que impida que un lenguaje de tipo estático tenga puntos de llamada polimórficos y mantenga una escritura estática en el proceso. Consulte Verificación de tipo estático de métodos múltiples . Tal vez estoy malinterpretando tu enlace.
Theodoros Chatzigiannakis
1
"Un lenguaje con mecanografía dinámica le permite expresar cualquier cosa siempre que Turing esté completo, lo que la mayoría lo es". Si bien esta es, por supuesto, una afirmación verdadera, realmente no se mantiene en "el mundo real" porque la cantidad de texto que uno tiene escribir podría ser extremadamente grande.
Daniel Jour
4

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 un Catobjeto, 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].contentmucho más ordenado que decir obj.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 llamada User.find_by_email('[email protected]')sin tener que declarar el find_by_emailmétodo. Por supuesto, no es nada que no se pueda lograr como UserRepository.FindBy("email", "[email protected]")en un lenguaje estáticamente tipado, pero no se puede negar su pulcritud.

kamilk
fuente
44
Seguramente hay cosas que solo puede hacer en idiomas estáticamente escritos. Pero no serían necesariamente un buen diseño.
coredump
2
El punto sobre el azúcar sintáctico tiene muy poco que ver con la escritura dinámica y todo con, bueno, la sintaxis.
Leftaroundabout
@leftaroundabout Los patrones tienen mucho que ver con la sintaxis. Los sistemas de tipos también tienen mucho que ver con eso.
user253751
4

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.

class Proxy(object):
    def __init__(self, obj):
        self.__target = obj

    def __getattr__(self, attr):
        return getattr(self.__target, attr)

Usando esto, Proxy(someObject)crea un nuevo objeto que se comporta igual que someObject. 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.

Jules
fuente
Creo que podría emular esto en Go. Hay un conjunto de métodos que deben tener todos los objetos proxy (de lo contrario, el pato podría no graznar y todo se desmorona). Puedo crear una interfaz Go con estos métodos. Tendré que pensarlo más, pero creo que lo que tengo en mente funcionará.
user7610
Puede hacer algo similar en cualquier lenguaje .NET con RealProxy y genéricos.
LittleEwok
@LittleEwok: RealProxy utiliza la generación de código de tiempo de ejecución; como digo, muchos lenguajes estáticos modernos tienen una solución como esta, pero aún es más fácil en un lenguaje dinámico.
Julio
Los métodos de extensión de C # son como los parches de mono hechos seguros. No puede cambiar los métodos existentes, pero puede agregar otros nuevos.
Andrew dice Reinstate Monica el
3

, 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.

JacquesB
fuente
+1 su penúltimo párrafo, creo que es el argumento crucial. Sin embargo, todavía argumentaría que hay una diferencia, ya que con los tipos estáticos tienes el control total de dónde y qué puedes mono parchear
jk.
2

Me pregunto, ¿existen patrones o estrategias de diseño útiles que, utilizando la formulación de la cita, "no funcionen como tipos"?

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:

Map<Class<?>, Function<?, String>> someMap;
someMap.get(object.getClass()).apply(object);

Sé que someMap.get(T.class)devolverá un Function<T, String>, debido a cómo construí someMap. Pero Java solo está seguro de que tengo una función.

Otro ejemplo:

data = parseJSON(someJson)
validate(data, someJsonSchema);
print(data.properties.rowCount);

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:

x, y, z = struct.unpack("II6s", data)

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:

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.

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:

Me pregunto, ¿existen patrones o estrategias de diseño útiles que, utilizando la formulación de la cita, "no funcionen como tipos"?

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.

Winston Ewert
fuente
1
¿Por qué es Java el punto de corte en el que no debe ir a un 'sistema de tipos más avanzado / complicado'?
jk.
2
@jk, ¿qué te lleva a pensar que eso es lo que estoy diciendo? Evité explícitamente tomar partido sobre si valía la pena un sistema de tipo más avanzado / complicado.
Winston Ewert
2
Algunos de estos son ejemplos terribles, y los otros parecen ser más decisiones de lenguaje en lugar de mecanografiados frente a no mecanografiados. Estoy particularmente confundido sobre por qué la gente piensa que la deserialización es tan compleja en los idiomas escritos. El resultado escrito sería data = parseJSON<SomeSchema>(someJson); print(data.properties.rowCount); y si uno no tiene una clase para deserializar, podemos recurrir a él data = parseJSON(someJson); print(data["properties.rowCount"]);, que todavía está escrito y expresa la misma intención.
NPSF3000
2
@ NPSF3000, ¿cómo funciona la función parseJSON? Parecería usar reflexión o macros. ¿Cómo podrían escribirse los datos ["properties.rowCount"] en un lenguaje estático? ¿Cómo podría saber que el valor resultante es un entero?
Winston Ewert
2
@ NPSF3000, ¿cómo planea usarlo si no sabe que es un número entero? ¿Cómo planeas recorrer los elementos en una lista en el JSON sin saber que era una matriz? El punto de mi ejemplo fue que sabía que data.propertiesera un objeto y sabía que data.properties.rowCountera un número entero y simplemente podía escribir código que los usara. Su propuesta data["properties.rowCount"]no proporciona lo mismo.
Winston Ewert
1

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étodo stripPrefixIfPresent:, de modo que pueda decir [@"foo/bar/baz" stripPrefixIfPresent:@"foo/"](tenga en cuenta el uso de los NSSringliterales @"").

  • 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.

cmaster
fuente
Puedo hacer lo primero en C ++ lanzando a void *, pero eso está eludiendo el sistema de tipos, por lo que no cuenta. Puedo hacer el segundo en C # con métodos de extensión, perfectamente dentro de un sistema de tipos. Para el tercero, creo que el "selector para el método" puede ser una lambda, por lo que cualquier lenguaje estáticamente escrito con lambdas puede hacer lo mismo, si lo entiendo correctamente. No estoy familiarizado con ObjC.
user7610
1
@JiriDanek "Puedo hacer lo primero en C ++ al anular *", no exactamente, el código que lee elementos no tiene forma de recuperar el tipo real por sí solo. Necesitas etiquetas de tipo. Además, no creo que decir "puedo hacer esto en <language>" sea la forma apropiada / productiva de ver esto, porque siempre puedes emularlos. Lo que importa es la ganancia en expresividad frente a la complejidad de la implementación. Además, parece pensar que si un lenguaje tiene capacidades estáticas y dinámicas (Java, C #), pertenece exclusivamente a la familia de lenguajes "estáticos".
coredump
1
@JiriDanek por 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?
coredump
1
Sugiere que es útil tener la opción de subvertir el sistema de tipos cuando sea necesario. Tener una escotilla de escape cuando la necesite. O alguien lo consideró útil. De lo contrario, no lo pondrían en el idioma.
user7610
2
@JiriDanek, creo que prácticamente lo lograste con tu último comentario. Esas escotillas de escape pueden ser extremadamente útiles si se usan con cuidado. Sin embargo, un gran poder conlleva una gran responsabilidad, y muchas personas abusan de él ... Por lo tanto, se siente mucho mejor usar un puntero a una clase base genérica de la que todas las otras clases se derivan por definición (como es el caso tanto en Objective-C como en Java), y confiar en RTTI para distinguir los casos, en lugar de lanzar un 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.
cmaster