Как мультиметоды решают проблему пространства имен? - PullRequest
32 голосов
/ 04 марта 2012

Я исследую проектирование языка программирования, и меня интересует вопрос о том, как заменить популярную парадигму ОО с передачей однократных сообщений на парадигму универсальной функции мультиметода.По большей части, это кажется очень простым, но я недавно застрял и был бы признателен за некоторую помощь.

OO передачи сообщений, на мой взгляд, это одно решение, которое решает два различных проблемы.Я объясню, что я имею в виду подробно в следующем псевдокоде.

(1) Решает проблему отправки:

=== в файле animal.code ===

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

=== в файле myprogram.code ===

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

В этой задаче «кора» - это один метод с несколькими «ветвями», которые работают по-разному в зависимости от типов аргументов.Мы внедряем «лай» один раз для каждого интересующего нас типа аргумента (собаки и кошки).Во время выполнения мы можем перебирать список животных и динамически выбирать подходящую ветвь.

(2) Это решает проблему пространства имен:

=== в файле animal.code===

   - Animals can "bark"

=== в файле tree.code ===

   - Trees have "bark"

=== в файле 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

В этой задаче «кора» - это две концептуально разные функции, которые просто оказываются с одинаковыми именами.Тип аргумента (собака или дерево) определяет, какую функцию мы на самом деле имеем в виду.


Мультиметоды элегантно решают задачу № 1. Но я не понимаю, как они решают задачу № 2. Например, первый из двух приведенных выше примеров может быть прямо переведен в мультиметоды:

(1) Собаки и кошки с использованием мультиметодов

=== в файле animal.code ===

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

=== в файле myprogram.code ===

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

Ключевым моментом является то, что метод коры (Dog) концептуально связан с корой (Cat).Во втором примере этот атрибут отсутствует, поэтому я не понимаю, как мультиметоды решают проблему с пространством имен.

(2) Почему мультиметоды не работают для животных и деревьев

=== в файле animal.code ===

   - define generic function bark(Animal a)

=== в файле tree.code ===

   - define generic function bark(Tree t)

=== в файле 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?

В этом случае, где должна быть определена общая функция?Должно ли оно быть определено на верхнем уровне, выше как животного, так и дерева?Не имеет смысла думать о коре для животных и деревьев как о двух методах одной и той же родовой функции, потому что две функции концептуально различны.

Насколько я знаю, я не нашел ни одной прошлой работы, которая бы решала эту проблему.Я посмотрел на мультиметоды Clojure и мультиметоды CLOS, и у них та же проблема.Я скрещиваю пальцы и надеюсь либо на элегантное решение проблемы, либо на убедительный аргумент, почему на самом деле это не проблема в реальном программировании.

Пожалуйста, дайте мне знать, если вопрос требует уточнения.Я думаю, что это довольно тонкий (но важный) момент.


Спасибо за здравомыслие, Райнер, Марцин и Матиас.Я понимаю ваши ответы и полностью согласен с тем, что динамическая диспетчеризация и разрешение пространства имен - это две разные вещи.CLOS не объединяет две идеи, в отличие от традиционной ОО с передачей сообщений.Это также допускает прямое расширение мультиметодов на множественное наследование.

Мой вопрос, в частности, касается ситуации, когда соотношение желательно .

Ниже приведен примерчто я имею в виду=== файл: GENE.code ===

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

==== файл: 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()

Из-за связи разрешения пространства имен с диспетчеризацией программист может наивно вызывать get-x () для всех трех объектов. Это также совершенно однозначно. Каждый объект «владеет» своим набором методов, поэтому нет смысла понимать, что имел в виду программист.

Сравните это с мультиметодной версией:


=== файл: XYZ.code ===

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

=== файл: POINT.code ===

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

=== файл: 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)

Поскольку get-x () XYZ не имеет концептуального отношения к get-x () GENE, они реализованы как отдельные универсальные функции. Следовательно, конечный программист (в my_program.code) должен явно квалифицировать get-x () и сообщить системе , какую get-x () он на самом деле хочет вызвать.

Это правда, что этот явный подход более понятен и легко обобщается на множественную диспетчеризацию и множественное наследование. Но использование (злоупотребление) диспетчеризацией для решения проблем с пространством имен является чрезвычайно удобной особенностью ОО передачи сообщений.

Лично я чувствую, что 98% моего собственного кода адекватно выражены с помощью однократной отправки и однократного наследования. Я гораздо удобнее использую диспетчеризацию для разрешения пространства имен, чем многократную диспетчеризацию, поэтому не хочу отказываться от нее.

Есть ли способ получить мне лучшее из обоих миров? Как избежать необходимости явной квалификации моих вызовов функций в настройке нескольких методов?


Кажется, консенсус таков, что

  • мультиметоды решают проблему диспетчеризации, но не атакуют проблему пространства имен.
  • функции, которые концептуально отличаются должны иметь разные имена, и пользователи должны ожидать их ручной квалификации.

Затем я полагаю, что в случаях, когда однопроцессная одиночная передача достаточна, ОО с передачей сообщений удобнее, чем универсальные функции.

Звучит так, будто это открытое исследование. Если бы язык предоставил механизм для мультиметодов, который также может использоваться для разрешения пространства имен, это было бы желательным свойством?

Мне нравится концепция универсальных функций, но в настоящее время я чувствую, что они оптимизированы для создания "очень сложных вещей, не таких сложных" за счет создания "простых вещей, которые немного раздражают". Поскольку большая часть кода тривиальна, я по-прежнему считаю, что эту проблему стоит решить.

Ответы [ 7 ]

19 голосов
/ 04 марта 2012

Динамическая диспетчеризация и разрешение пространства имен - это две разные вещи. Во многих объектных системах классы также используются для пространств имен. Также обратите внимание, что часто и класс, и пространство имен связаны с файлом. Таким образом, эти объектные системы объединяют как минимум три вещи:

  • определения классов с их слотами и методами
  • пространство имен для идентификаторов
  • блок хранения исходного кода

Common Lisp и его объектная система (CLOS) работают по-разному:

  • классы не образуют пространство имен
  • универсальные функции и методы не принадлежат классам и поэтому не определены внутри классов
  • универсальные функции определены как функции верхнего уровня и, следовательно, не являются вложенными или локальными
  • идентификаторами для универсальных функций являются символы
  • символы имеют собственный механизм пространства имен, называемый пакетами
  • универсальные функции «открыты». В любое время можно добавлять или удалять методы
  • универсальные функции являются первоклассными объектами
  • Матоды являются первоклассными объектами
  • классы и универсальные функции также не связаны с файлами. Вы можете определить несколько классов и несколько общих функций в одном файле или в любом количестве файлов. Вы также можете определять классы и методы из исполняемого кода (таким образом, не привязанного к файлам) или чего-то вроде REPL (чтение цикла печати eval).

Стиль в CLOS:

  • если для функциональности требуется динамическая диспетчеризация и она тесно связана, используйте одну универсальную функцию с разными методами
  • если есть много разных функций, но с общим именем, не помещайте их в одну общую функцию. Создайте различные общие функции.
  • универсальные функции с одним и тем же именем, но где имя находится в разных пакетах, это разные универсальные функции.
* * Пример 1 042:
(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))

Обратите внимание, что класс ANIMAL и пакет ANIMAL имеют одинаковые имена. Но это не обязательно так. Имена никак не связаны. DEFMETHOD неявно создает соответствующую универсальную функцию.

Если вы добавите другой пакет (например, GAME-ANIMALS), то универсальная функция BARK будет другой. Если эти пакеты не связаны (например, один пакет использует другой).

Из другого пакета (пространства имен символов в Common Lisp) можно вызвать эти:

(animal:bark some-animal)

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

Символ имеет синтаксис

PACKAGE-NAME::SYMBOL-NAME

Если пакет совпадает с текущим, его можно опустить.

  • ANIMAL::BARK относится к символу с именем BARK в пакете ANIMAL. Обратите внимание, что есть два двоеточия.
  • AINMAL:BARK относится к экспортированному символу BARK в упаковке ANIMAL. Обратите внимание, что есть только один двоеточие. Экспорт , , импорт и с использованием - это механизмы, определенные для пакетов и их символов. Таким образом, они не зависят от классов и универсальных функций, но могут использоваться для структурирования пространства имен для символов, именующих их.

Более интересный случай, когда мультиметоды фактически используются в универсальных функциях:

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

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

Выше используются классы CAT, HUMAN, DOG и BONE. К какому классу должна принадлежать универсальная функция? Как будет выглядеть специальное пространство имен?

Поскольку универсальные функции распределяют по всем аргументам, не имеет прямого смысла отождествлять универсальную функцию со специальным пространством имен и делать ее определением в одном классе.

Мотивация:

Общие функции были добавлены в Lisp в 80-х годах разработчиками на Xerox PARC (для Common LOOPS ) и на Символика для Новые ароматы .Один хотел избавиться от дополнительного механизма вызова (передача сообщений) и довести диспетчеризацию до обычных (верхнего уровня) функций.Новые Flavors имели одну отправку, но универсальные функции с несколькими аргументами.Затем исследование Common LOOPS принесло многократную рассылку.Новые Flavors и Common LOOPS были заменены стандартизированным CLOS.Эти идеи затем были перенесены на другие языки, такие как Dylan .

Поскольку пример кода в вопросе не использует ничего общего, что могут предложить общие функции, похоже, что нужно датьвверх что-то.

Когда достаточно одной отправки, передачи сообщений и одного наследования, тогда универсальные функции могут выглядеть как шаг назад.Причина этого, как уже упоминалось, заключается в том, что не нужно объединять все виды схожих именованных функций в одну универсальную функцию.

Когда

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

выглядят одинаково, это два концептуальноразличные действия.

Но больше:

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

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

Теперь неожиданно списки параметров для одинаковой именованной обобщенной функции выглядят иначе.Должно ли это быть одной общей функцией?Если нет, то как мы вызываем BARK для различных объектов в списке с правильными параметрами?

В реальном коде на Лиспе универсальные функции обычно выглядят намного сложнее с несколькими обязательными и необязательными аргументами.

В Common Lisp универсальные функции также имеют не только один тип метода.Существуют разные типы методов и различные способы их комбинирования.Объединять их имеет смысл только тогда, когда они действительно принадлежат к определенной универсальной функции.

Поскольку универсальные функции также являются объектами первого класса, их можно передавать, возвращать из функций и сохранять в структурах данных.На этом этапе важен сам объект универсальной функции, а не его имя.

Для простого случая, когда у меня есть объект, который имеет координаты x и y и может выступать в качестве точки, я бы наследовал длякласс объектов из класса POINT (возможно, как некоторый миксин).Затем я импортировал бы символы GET-X и GET-Y в некоторое пространство имен - при необходимости.

Существуют другие языки, которые более отличаются от Lisp / CLOS и которые пытаются (ed) поддерживать мультиметоды:

Кажется, есть много попыток добавить его в Java.

9 голосов
/ 04 марта 2012

В вашем примере «Почему мультиметоды не работают» предполагается, что вы можете определить две универсальные функции с одинаковыми именами в одном и том же пространстве имен языка.Обычно это не так;например, мультиметоды Clojure явно принадлежат пространству имен, поэтому, если у вас есть две такие универсальные функции с одним и тем же именем, вам нужно будет четко указать, какое из них вы используете."всегда будет иметь разные имена или жить в разных пространствах имен.

3 голосов
/ 04 марта 2012

Общие функции должны выполнять один и тот же «глагол» для всех классов, для которых реализован его метод.

В случае «коры» животных / дерева глагол животного «выполнить звуковое действие» и вслучай с деревом, ну, я думаю, это make-environment-shield.

То, что английский называет их обоих "корой", является просто лингвистическим совпадением.

Если у вас есть случайгде несколько разных GF (обобщенных функций) действительно должны иметь одно и то же имя, использование пространств имен для их разделения (вероятно) является правильным.

2 голосов
/ 04 марта 2012

Поскольку get-x () XYZ не имеет концептуального отношения к get-x () GENE, они реализованы как отдельные общие функции

Конечно. Но так как их arglist один и тот же (просто передает объект методу), вы можете «реализовать» их как разные методы в одной и той же универсальной функции.

Единственное ограничение при добавлении метода в обобщенную функцию - это то, что arglist метода совпадает с arglist обобщенной функции.

В более широком смысле методы должны иметь одинаковое количество обязательных и необязательные параметры и должны быть способны принимать любые аргументы соответствует любым параметрам & rest или & key, указанным в generic функция.

Нет ограничений на то, что функции должны быть концептуально связаны. Большую часть времени они (переопределяют суперкласс и т. Д.), Но они, конечно, не должны быть.

Хотя даже это ограничение (нужен тот же arglist) иногда кажется ограничивающим. Если вы посмотрите на Erlang, функции имеют арность, и вы можете определить несколько функций с одинаковым именем, которые имеют разную арность (функции с одинаковым именем и разными арглистами). И тогда своего рода диспетчеризация заботится о вызове правильной функции. Мне это нравится. И в lisp, я думаю, это соответствовало бы тому, чтобы обобщенная функция принимала методы, которые имеют разные arglists. Может быть, это то, что настраивается в MOP?

Хотя здесь и читается немного больше , похоже, что аргументы с ключевыми словами могут позволить программисту достичь универсальной функции, заключающей в себе методы с совершенно разной арностью, используя разные ключи в разных методах, чтобы варьировать их число. аргументы:

Метод может «принимать» & key и & rest аргументы, определенные в его универсальном функция с параметром & rest, с тем же ключом & параметров, либо указав & allow-other-keys вместе с & key. Метод также может указывать & ключевые параметры, не найденные в общем список параметров функции - когда вызывается универсальная функция, любая & ключевой параметр, указанный обобщенной функцией или любым применимым Метод будет принят.

Также обратите внимание, что такого рода размытие, когда различные методы, хранящиеся в универсальной функции, делают концептуально разные вещи, происходит за кулисами в вашем дереве «лай лает», пример «лай собаки». При определении класса дерева вы должны установить автоматический метод получения и установки для слота коры. При определении класса собаки вы должны определить метод лая для типа собаки, который фактически выполняет лай. И оба эти метода сохраняются в обобщенной функции # 'bark.

Поскольку они оба заключены в одну и ту же универсальную функцию, вы должны вызывать их одинаково:

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

как код:

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> 

Я склоняюсь к такому «двойственности синтаксиса» или размыванию функций и т. Д. И я не думаю, что все методы универсальной функции должны быть концептуально схожими. Это всего лишь руководство IMO. Если происходит лингвистическое взаимодействие в английском языке (лаять как существительное и глагол), было бы хорошо иметь язык программирования, который обрабатывает случай изящно.

2 голосов
/ 04 марта 2012

OO передачи сообщений, в общем, не решает проблему пространства имен, о которой вы говорите.ОО-языки со структурными системами типов не различают метод bark в Animal или Tree, если они имеют один и тот же тип.Только потому, что популярные языки OO используют системы номинального типа (например, Java), это выглядит так.

0 голосов
/ 14 января 2018

Это общий вопрос о том, куда поместить таблицу диспетчеризации, на которую многие языки программирования пытаются обращаться удобным способом.

В случае ООП мы помещаем ее в определение класса (у нас есть функция type +таким образом, в сочетании с наследованием он дает все удовольствия от архитектурных проблем).

В случае FP мы помещаем его в диспетчерскую функцию (у нас есть общая централизованная таблица, это обычно не так уж плохо, нотоже не идеально).

Мне нравится интерфейсный подход, когда я могу создать виртуальную таблицу отдельно от любого типа данных И любого определения общей функции (протокол в Clojure).

InJava (извините) будет выглядеть так:

Давайте предположим, что ResponseBody - это интерфейс.

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);
      }
    };
}

Виртуальная таблица создается для этой конкретной функции create.Это полностью решает проблему пространства имен, вы также можете иметь нецентрализованную диспетчеризацию на основе типов (OOP) , если хотите .

Также становится тривиально иметь отдельную реализацию без объявленияновые типы данных для тестирования.

0 голосов
/ 01 мая 2012

Вы работаете с несколькими концепциями и смешиваете их, например: пространства имен, глобальные универсальные функции, локальные универсальные функции (методы), вызов метода, передача сообщений и т. Д.

В некоторых случаях эти концепции могутсинтаксически перекрывать, было трудно реализовать.Мне кажется, что вы также смешиваете множество понятий в своем уме.

Функциональные языки - не моя сила, я проделал некоторую работу с LISP.

Но некоторые из нихпонятия, используются в других парадигмах, таких как процедурная и объектная (класс) ориентация.Возможно, вы захотите проверить, как реализованы эти концепции, и позже вернетесь к своему собственному языку программирования.

Например, что-то, что я считаю очень важным, это использование пространства имен («модулей»), какотделите концепцию от процедурного программирования и избегайте столкновений идентификаторов, о которых вы упомянули.Язык программирования с таким пространством имен, как у вас, будет выглядеть так:

=== в файле 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

=== в файле 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

Приветствия.

...