Pregunta ¿Cómo solucionan los multimétodos el problema del espacio de nombres?


Estoy investigando el diseño de lenguaje de programación, y estoy interesado en la cuestión de cómo reemplazar el popular paradigma OO de envío de mensajes de un solo envío con el paradigma de función genérica multimétodos. En su mayor parte, parece muy sencillo, pero recientemente me he quedado estancado y agradecería algo de ayuda.

OO que pasa mensajes, en mi opinión, es una solución que resuelve dos diferente problemas. Explico lo que quiero decir en detalle en el siguiente pseudocódigo.

(1) Soluciona el problema de envío:

=== en el archivo animal.code ===

   - Animals can "bark"
   - Dogs "bark" by printing "woof" to the screen.
   - Cats "bark" by printing "meow" to the screen.

=== en el archivo myprogram.code ===

import animal.code
for each animal a in list-of-animals :
   a.bark()

En este problema, "ladrar" es un método con múltiples "ramas" que operan de manera diferente dependiendo de los tipos de argumento. Implementamos "ladrar" una vez para cada tipo de argumento en el que estamos interesados ​​(perros y gatos). En tiempo de ejecución, podemos iterar a través de una lista de animales y seleccionar dinámicamente la rama adecuada para tomar.

(2) Soluciona el problema del espacio de nombres:

=== en el archivo animal.code ===

   - Animals can "bark"

=== en el archivo tree.code ===

   - Trees have "bark"

=== en el archivo myprogram.code ===

import animal.code
import tree.code

a = new-dog()
a.bark() //Make the dog bark

…

t = new-tree()
b = t.bark() //Retrieve the bark from the tree

En este problema, "ladrar" es en realidad dos funciones conceptualmente diferentes que solo ocurrir tener el mismo nombre El tipo del argumento (ya sea perro o árbol) determina qué función queremos decir en realidad.


Los multimétodos resuelven elegantemente el problema número 1. Pero no entiendo cómo resuelven el problema número 2. Por ejemplo, el primero de los dos ejemplos anteriores se puede traducir de manera directa a multimétodos:

(1) Perros y gatos usando multimétodos

=== en el archivo animal.code ===

   - define generic function bark(Animal a)
   - define method bark(Dog d) : print("woof")
   - define method bark(Cat c) : print("meow")

=== en el archivo myprogram.code ===

import animal.code
for each animal a in list-of-animals :
   bark(a)

El punto clave es que el método ladrar (Perro) está conceptualmente relacionado con la corteza (Cat). El segundo ejemplo no tiene este atributo, por lo que no entiendo cómo multimétodos resuelven el problema del espacio de nombres.

(2) Por qué los multimétodos no funcionan para Animales y árboles

=== en el archivo animal.code ===

   - define generic function bark(Animal a)

=== en el archivo tree.code ===

   - define generic function bark(Tree t)

=== en el archivo myprogram.code ===

import animal.code
import tree.code

a = new-dog()
bark(a)   /// Which bark function are we calling?

t = new-tree
bark(t)  /// Which bark function are we calling?

En este caso, ¿dónde debe definirse la función genérica? ¿Debería definirse en el nivel superior, sobre el animal y el árbol? No tiene sentido pensar en la corteza de animales y árboles como dos métodos de la misma función genérica porque las dos funciones son conceptualmente diferentes.

Hasta donde sé, no he encontrado ningún trabajo anterior que haya resuelto este problema todavía. He analizado Clojure multimethods y CLOS multimétodos y tienen el mismo problema. Estoy cruzando los dedos y esperando una solución elegante al problema, o un argumento de persuasión sobre por qué no es realmente un problema en la programación real.

Por favor, avíseme si la pregunta necesita aclaración. Este es un punto bastante sutil (pero importante), creo.


Gracias por las respuestas cordura, Rainer, Marcin y Matthias. Entiendo tus respuestas y estoy completamente de acuerdo en que el despacho dinámico y la resolución del espacio de nombres son dos cosas diferentes. CLOS no combina las dos ideas, mientras que OO tradicional pasa mensajes. Esto también permite una extensión directa de multimétodos a la herencia múltiple.

Mi pregunta es específicamente en la situación cuando la fusión es deseable.

El siguiente es un ejemplo de lo que quiero decir.

=== file: XYZ.code ===

define class XYZ :
   define get-x ()
   define get-y ()
   define get-z ()

=== archivo: POINT.code ===

define class POINT :
   define get-x ()
   define get-y ()

=== file: GENE.code ===

define class GENE :
   define get-x ()
   define get-xx ()
   define get-y ()
   define get-xy ()

==== file: my_program.code ===

import XYZ.code
import POINT.code
import GENE.code

obj = new-xyz()
obj.get-x()

pt = new-point()
pt.get-x()

gene = new-point()
gene.get-x()

Debido a la combinación de la resolución del espacio de nombres con el envío, el programador puede llamar ingenuamente a get-x () en los tres objetos. Esto también es perfectamente inequívoco. Cada objeto "posee" su propio conjunto de métodos, por lo que no hay confusión en cuanto a lo que el programador quiso decir.

Contraste esto a la versión multimétodo:


=== file: XYZ.code ===

define generic function get-x (XYZ)
define generic function get-y (XYZ)
define generic function get-z (XYZ)

=== archivo: POINT.code ===

define generic function get-x (POINT)
define generic function get-y (POINT)

=== file: GENE.code ===

define generic function get-x (GENE)
define generic function get-xx (GENE)
define generic function get-y (GENE)
define generic function get-xy (GENE)

==== file: my_program.code ===

import XYZ.code
import POINT.code
import GENE.code

obj = new-xyz()
XYZ:get-x(obj)

pt = new-point()
POINT:get-x(pt)

gene = new-point()
GENE:get-x(gene)

Como get-x () de XYZ no tiene relación conceptual con get-x () de GENE, se implementan como funciones genéricas separadas. Por lo tanto, el programador final (en my_program.code) debe calificar explícitamente a get-x () y decirle al sistema cual get-x () realmente quiere llamar.

Es cierto que este enfoque explícito es más claro y fácil de generalizar a despacho múltiple y herencia múltiple. Pero el uso (abuso) de despacho para resolver problemas de espacio de nombres es una característica extremadamente conveniente de OO para pasar mensajes.

Personalmente, creo que el 98% de mi propio código se expresa de forma adecuada mediante el envío único y la herencia única. Utilizo esta conveniencia de usar el despacho para la resolución del espacio de nombres mucho más que el uso de despacho múltiple, por lo que soy reacio a renunciar a él.

¿Hay alguna manera de sacarme lo mejor de ambos mundos? ¿Cómo evito la necesidad de calificar de manera explícita mis llamadas a funciones en una configuración de múltiples métodos?


Parece que el consenso es que

  • Los métodos multimétodos resuelven el problema del envío pero no atacan el problema del espacio de nombres.
  • funciones que son conceptualmente diferentes debería tener diferentes nombres y usuarios debería se espera que los califique manualmente.

Entonces creo que, en los casos en que el envío único de herencia única es suficiente, el OO de paso de mensajes es más conveniente que las funciones genéricas.

Parece que es investigación abierta entonces. Si un lenguaje proporcionara un mecanismo para métodos múltiples que también se puede usar para la resolución del espacio de nombres, ¿sería esa una característica deseada?

Me gusta el concepto de funciones genéricas, pero actualmente siento que están optimizadas para hacer que "las cosas muy difíciles no sean tan difíciles" a expensas de hacer que las "cosas triviales sean ligeramente molestas". Como la mayoría del código es trivial, sigo creyendo que es un problema que vale la pena resolver.


32
2018-03-04 07:27


origen


Respuestas:


El despacho dinámico y la resolución del espacio de nombres son dos cosas diferentes. En muchos sistemas de objetos, las clases también se usan para espacios de nombres. También tenga en cuenta que a menudo tanto la clase como el espacio de nombres están vinculados a un archivo. Entonces estos sistemas de objetos combinan al menos tres cosas:

  • definiciones de clase con sus ranuras y métodos
  • el espacio de nombres para los identificadores
  • la unidad de almacenamiento del código fuente

Common Lisp y su sistema de objetos (CLOS) funciona de manera diferente:

  • las clases no forman un espacio de nombres
  • las funciones y los métodos genéricos no pertenecen a las clases y, por lo tanto, no están definidos dentro de las clases
  • las funciones genéricas se definen como funciones de nivel superior y, por lo tanto, no están anidadas o son locales
  • identificadores para funciones genéricas son símbolos
  • los símbolos tienen su propio mecanismo de espacio de nombre llamado paquetes
  • las funciones genéricas son 'abiertas'. Uno puede agregar o eliminar métodos en cualquier momento
  • las funciones genéricas son objetos de primera clase
  • Los mathods son objetos de primera clase
  • las clases y las funciones genéricas tampoco se combinan con los archivos. Puede definir múltiples clases y múltiples funciones genéricas en un archivo o en tantos archivos como desee. También puede definir clases y métodos desde el código en ejecución (por lo tanto, no vinculados a archivos) o algo así como un REPL (leer ciclo de impresión de evaluación).

Estilo en CLOS:

  • si una funcionalidad necesita despacho dinámico y la funcionalidad está estrechamente relacionada, entonces use una función genérica con diferentes métodos
  • si hay muchas funcionalidades diferentes, pero con un nombre común, no las pongas en la misma función genérica. Crea diferentes funciones genéricas.
  • funciones genéricas con el mismo nombre, pero donde el nombre está en paquetes diferentes son funciones genéricas diferentes.

Ejemplo:

(defpackage "ANIMAL" (:use "CL")) 
(in-package "ANIMAL")

(defclass animal () ())
(deflcass dog (animal) ())
(deflcass cat (animal) ()))

(defmethod bark ((an-animal dog)) (print 'woof))
(defmethod bark ((an-animal cat)) (print 'meow)) 

(bark (make-instance 'dog))
(bark (make-instance 'dog))

Tenga en cuenta que la clase ANIMAL y el paquete ANIMAL tener el mismo nombre Pero eso no es necesario. Los nombres no están conectados de ninguna manera. DEFMETHOD crea implícitamente una función genérica correspondiente.

Si agrega otro paquete (por ejemplo GAME-ANIMALS), entonces el BARK la función genérica será diferente. A menos que estos paquetes estén relacionados (por ejemplo, un paquete usa el otro).

Desde un paquete diferente (espacio de nombres de símbolos en Common Lisp), uno puede llamar a estos:

(animal:bark some-animal)

(game-animal:bark some-game-animal)

Un símbolo tiene la sintaxis

PACKAGE-NAME::SYMBOL-NAME

Si el paquete es el mismo que el paquete actual, puede omitirse.

  • ANIMAL::BARK se refiere al símbolo llamado BARK en el paquete ANIMAL. Tenga en cuenta que hay dos dos puntos.
  • AINMAL:BARK se refiere a exportado símbolo BARK en el paquete ANIMAL. Tenga en cuenta que solo hay dos puntos. Exportador, importador y utilizando son mecanismos definidos para paquetes y sus símbolos. Por lo tanto, son independientes de las clases y funciones genéricas, pero se pueden usar para estructurar el espacio de nombres para los símbolos que los nombran.

El caso más interesante es cuando los multimétodos se usan realmente en funciones genéricas:

(defmethod bite ((some-animal cat) (some-human human))
  ...)

(defmethod bite ((some-animal dog) (some-food bone))
  ...)

Arriba usa las clases CAT, HUMAN, DOG y BONE. ¿A qué clase debe pertenecer la función genérica? ¿Cómo se vería el espacio de nombres especial?

Como las funciones genéricas se distribuyen en todos los argumentos, no tiene sentido directo combinar la función genérica con un espacio de nombres especial y convertirlo en una definición en una sola clase.

Motivación:

Las funciones genéricas fueron agregadas en los años 80 a Lisp por los desarrolladores en Xerox PARC (para LOOPS comunes) y en Symbolics para Nuevos sabores. Uno quería deshacerse de un mecanismo de llamada adicional (paso de mensajes) y llevar el despacho a funciones ordinarias (nivel superior). New Flavors tenía un único envío, pero funciones genéricas con múltiples argumentos. La investigación en LOOPS comunes luego trajo despacho múltiple. Nuevos sabores y LOOPS comunes fueron reemplazados por los CLOS estandarizados. Estas ideas luego fueron llevadas a otros idiomas como Dylan.

Como el código de ejemplo en la pregunta no usa nada que las funciones genéricas puedan ofrecer, parece que hay que renunciar a algo.

Cuando el envío único, el envío de mensajes y la herencia individual es suficiente, las funciones genéricas pueden parecer un paso atrás. La razón para esto es, como se mencionó, que uno no quiere poner todo tipo de funcionalidades nombradas similares en una función genérica.

Cuando

(defmethod bark ((some-animal dog)) ...)
(defmethod bark ((some-tree oak)) ...)

se ven similares, son dos acciones conceptualmente diferentes.

Pero más:

(defmethod bark ((some-animal dog) tone loudness duration)
   ...)

(defmethod bark ((some-tree oak)) ...)

Ahora, de repente, las listas de parámetros para la misma función genérica mencionada se ven diferentes. ¿Debería permitirse que sea una función genérica? Si no, ¿cómo llamamos BARK en varios objetos en una lista de cosas con los parámetros correctos?

En el código Lisp real, las funciones genéricas suelen parecer mucho más complicadas con varios argumentos obligatorios y opcionales.

En Common Lisp, las funciones genéricas también tienen un solo tipo de método. Existen diferentes tipos de métodos y varias formas de combinarlos. Tiene sentido combinarlos, cuando realmente pertenecen a una cierta función genérica.

Dado que las funciones genéricas también son objetos de primera clase, pueden pasarse, regresar de funciones y almacenarse en estructuras de datos. En este punto, el objeto de función genérico en sí mismo es importante, ya no es su nombre.

Para el caso simple en el que tengo un objeto, que tiene coordenadas xey y puede actuar como un punto, heredaría para la clase de los objetos de una POINT clase (tal vez como alguna mixin). Entonces yo importaría el GET-X y GET-Y símbolos en algún espacio de nombres, cuando sea necesario.

Hay otros idiomas que son más diferentes de Lisp / CLOS y que intentan (ed) soportar multimétodos:

Parece haber muchos intentos de agregarlo a Java.


19
2018-03-04 08:35



Su ejemplo de "Por qué los métodos multimétodos no funcionarán" presupone que puede definir dos funciones genéricas de nombre idéntico en el mismo espacio de nombres de idioma. Esto generalmente no es el caso; por ejemplo, los multimétodos de Clojure pertenecen explícitamente a un espacio de nombres, por lo que si tiene dos funciones genéricas con el mismo nombre, deberá aclarar cuál está utilizando.

En resumen, las funciones que son "conceptualmente diferentes" siempre tendrán diferentes nombres o vivirán en espacios de nombres diferentes.


9
2018-03-04 08:29



Las funciones genéricas deben realizar el mismo "verbo" para todas las clases para las que se implementa su método.

En el caso de "ladrido" de animales / árbol, el verbo animal es "realizar una acción de sonido" y en el caso de árbol, bueno, supongo que es escudo de ambiente.

Que el inglés les llame a ambos "ladrar" es solo una coincidencia lingüística.

Si tiene un caso donde múltiples GF diferentes (funciones genéricas) realmente deberían tener el mismo nombre, usar espacios de nombres para separarlos es (probablemente) lo correcto.


3
2018-03-04 11:45



El OO de paso de mensajes, en general, no resuelve el problema del espacio de nombres del que usted habla. Los lenguajes OO con sistemas de tipo estructural no diferencian entre un método barken una Animal o una Tree siempre que tengan el mismo tipo. Solo porque los lenguajes de OO populares usan sistemas de tipo nominal (por ejemplo, Java) es lo que parece.


2
2018-03-04 17:21



Porque get-x () de XYZ no tiene relación conceptual con get-x () de GENE,   se implementan como funciones genéricas separadas

Por supuesto. Pero como su arglist es el mismo (simplemente pasa el objeto al método), entonces 'podrías' implementarlos como métodos diferentes en la misma función genérica.

La única restricción cuando se agrega un método a una función genérica, es que el arglista del método coincide con el arglista de la función genérica.

En general, los métodos deben tener el mismo número de requeridos y   parámetros opcionales y debe ser capaz de aceptar cualquier argumento   correspondiente a cualquier parámetro & rest o & key especificado por el genérico   función.

No hay restricción de que las funciones deben estar relacionadas conceptualmente. La mayoría de las veces lo son (anulando una superclase, etc.), pero ciertamente no tienen que serlo.

Aunque incluso esta restricción (necesita el mismo arglist) parece limitar a veces. Si miras a Erlang, las funciones tienen arity, y puedes definir múltiples funciones con el mismo nombre que tienen arity diferente (funciones con el mismo nombre y diferentes arglists). Y luego, una especie de despacho se encarga de llamar a la función correcta. Me gusta esto. Y en lisp, creo que esto se correlacionaría con tener una función genérica que acepte métodos que tengan variados arglists. Tal vez esto es algo que se puede configurar en el MOP?

Aunque leyendo un poco más aquí, parece que los argumentos de palabra clave pueden permitir que el programador logre tener una función genérica encapsular métodos con una aridad completamente diferente, utilizando diferentes claves en diferentes métodos para variar su número de argumentos:

Un método puede "aceptar" & key & & rest arguments definidos en su genérico   función teniendo un parámetro & rest, teniendo la misma y clave   parámetros, o especificando & allow-other-keys junto con & key. UN   método también puede especificar y parámetros clave que no se encuentran en el genérico   lista de parámetros de la función: cuando se llama a la función genérica, cualquier   y el parámetro clave especificado por la función genérica o cualquier parámetro aplicable   método será aceptado.

También tenga en cuenta que este tipo de desenfoque, donde los diferentes métodos almacenados en la función genérica hacen cosas conceptualmente diferentes, ocurre detrás de las escenas en su 'árbol tiene corteza', 'ladrido de perros' ejemplo. Al definir la clase de árbol, debe establecer un método automático getter y setter para la ranura de corte. Al definir la clase de perro, definirías un método de corte en el tipo de perro que realmente hace el ladrido. Y ambos métodos se almacenan en una función genérica # 'ladrido.

Como ambos están encerrados en la misma función genérica, los llamaría exactamente de la misma manera:

(bark tree-obj) -> Returns a noun (the bark of the tree)
(bark dog-obj) -> Produces a verb (the dog barks)

Como código:

CL-USER> 
(defclass tree ()
  ((bark :accessor bark :initarg :bark :initform 'cracked)))
#<STANDARD-CLASS TREE>
CL-USER> 
(symbol-function 'bark)
#<STANDARD-GENERIC-FUNCTION BARK (1)>
CL-USER> 
(defclass dog ()
  ())
#<STANDARD-CLASS DOG>
CL-USER> 
(defmethod bark ((obj dog))
  'rough)
#<STANDARD-METHOD BARK (DOG) {1005494691}>
CL-USER> 
(symbol-function 'bark)
#<STANDARD-GENERIC-FUNCTION BARK (2)>
CL-USER> 
(bark (make-instance 'tree))
CRACKED
CL-USER> 
(bark (make-instance 'dog))
ROUGH
CL-USER> 

Tiendo a favorecer este tipo de "dualidad de sintaxis", o borrosidad de las características, etc. Y no creo que todos los métodos en una función genérica tengan que ser conceptualmente similares. Esa es solo una guía IMO. Si ocurre una interacción lingüística en el idioma inglés (ladrar como sustantivo y verbo), es bueno tener un lenguaje de programación que maneje el caso con gracia.


2
2018-03-04 17:41



Está trabajando con varios conceptos y mezclándolos, como espacios de nombres, funciones genéricas globales, funciones genéricas locales (métodos), invocación de métodos, paso de mensajes, etc.

En algunas circunstancias, esos conceptos pueden superponerse sintacticamente y ser difíciles de implementar. Me parece que también estás mezclando muchos conceptos en tu mente.

Los lenguajes funcionales, no son mi fortaleza, he trabajado un poco con LISP.

Sin embargo, algunos de estos conceptos se usan en otros paradigmas, como Procedural, & Object (Class) Orientation. Es posible que desee comprobar cómo se implementan estos conceptos y, más adelante, volver a su propio lenguaje de programación.

Por ejemplo, algo que considero muy importante es el uso del espacio de nombres ("módulos"), como un concepto separado de la Programación Procesal, y evitar los choques de identificadores, como los que mencionas. Un lenguaje de programación con un espacio de nombres como el suyo sería así:

=== en el archivo animal.code ===

define module animals

define class animal
  // methods doesn't use "bark(animal AANIMAL)"
  define method bark()
  ...
  end define method
end define class

define class dog
  // methods doesn't use "bark(dog ADOG)"
  define method bark()
  ...
  end define method
end define class

end define module

=== en el archivo myprogram.code ===

define module myprogram

import animals.code
import trees.code

define function main
  a = new-dog()
  a.bark() //Make the dog bark

  …

  t = new-tree()
  b = t.bark() //Retrieve the bark from the tree
end define function main

end define module

Aclamaciones.


0
2018-04-30 23:16



Esta es la pregunta general de dónde colocar la tabla de despacho que muchos lenguajes de programación intentan abordar de manera conveniente.

En el caso de OOP lo ponemos en la definición de la clase (tenemos la concreción de tipo + función de esta manera, condimentada con herencia, da todos los placeres de los problemas de arquitectura).

En el caso de FP lo colocamos dentro de la función de despacho (tenemos una tabla centralizada compartida, esto no suele ser tan malo, pero tampoco perfecto).

Me gusta el enfoque basado en interfaz, cuando puedo crear la tabla virtual por separado de cualquier tipo de datos Y de cualquier definición de función compartida (protocolo en Clojure).

En Java (lo siento) se verá así:

Asumamos ResponseBody es una interfaz

public static ResponseBody create(MediaType contentType,
     long contentLength, InputStream content) {

    return new ResponseBody() {
      public MediaType contentType() {
        return contentType;
      }

      public long contentLength() {
        return contentLength;
      }

      public BufferedSource source() {
        return streamBuffered(content);
      }
    };
}

La tabla virtual se crea para este específico create función. Esto resuelve completamente el problema del espacio de nombres, también puede tener un despacho basado en el tipo no centralizado (OOP) si quieres.

También es trivial tener una implementación separada sin declarar nuevos tipos de datos para fines de prueba.


0
2018-01-13 23:37