Как устранить неоднозначность определений операторов между объектами / классами в языке программирования? - PullRequest
6 голосов
/ 16 марта 2011

Я проектирую свой собственный язык программирования (называемый Lima, если вы заботитесь о нем на сайте www.btetrud.com), и я пытаюсь понять, как реализовать перегрузку операторов.Я решил связать операторы с конкретными объектами (это язык на основе прототипа).(Это также динамический язык, где «var» похож на «var» в javascript - переменная, которая может содержать любой тип значения).

Например, это будет объект с переопределенным оператором +:

x = 
{  int member

   operator + 
    self int[b]:
       ret b+self
    int[a] self:
       ret member+a
}

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

Проблема в том, что делать, когда у вас есть два объекта, которые определяют оператор открытым способомкак это.Например, что вы делаете в этом сценарии:

A = 
{ int x
  operator +
   self var[b]:
    ret x+b
}

B = 
{ int x
  operator +
   var[a] self:
    ret x+a
}

a+b   ;; is a's or b's + operator used?

Таким образом, простой ответ на этот вопрос - «ну, не делайте двусмысленных определений», но это не так просто.Что делать, если вы включите модуль, который имеет тип объекта A, а затем определил объект типа B.

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

C ++ имеет перегрузку операторов, определенную как "члены" классов.Как C ++ справляется с такой неопределенностью?

Ответы [ 4 ]

3 голосов
/ 16 марта 2011

Большинство языков отдают приоритет классу слева. Я полагаю, что C ++ вообще не позволяет перегружать операторы в правой части. Когда вы определяете operator+, вы определяете дополнение для того, когда этот тип находится слева, для всего, что справа.

На самом деле, не имеет смысла, если вы позволите вашему operator + работать, когда тип находится справа. Это работает для +, но рассмотрим -. Если тип A определяет operator - определенным образом, а я делаю int x - A y, я не хочу, чтобы A's operator - вызывался, потому что он вычислит вычитание в обратном порядке!

В Python, который имеет более расширенные правила перегрузки операторов , существует отдельный метод для обратного направления. Например, существует метод __sub__, который перегружает оператор -, когда этот тип находится слева, и __rsub__, который перегружает оператор -, когда этот тип находится справа. Это похоже на возможность на вашем языке разрешить «я» появляться слева или справа, но это вносит двусмысленность.

Python отдает приоритет вещи слева - это работает лучше на динамическом языке. Если Python встречает x - y, он сначала вызывает x.__sub__(y), чтобы узнать, знает ли x, как вычесть y. Это может либо привести к результату, либо вернуть специальное значение NotImplemented. Если Python обнаруживает, что NotImplemented был возвращен, он пытается выполнить другой путь. Он вызывает y.__rsub__(x), который был бы запрограммирован, зная, что y был справа. Если это также возвращает NotImplemented, то TypeError повышается, потому что типы были несовместимы для этой операции.

Я думаю, что это идеальная стратегия перегрузки операторов для динамических языков.

Редактировать: Чтобы подвести итог, у вас сложилась неоднозначная ситуация, поэтому у вас есть только три варианта:

  • Дайте преимущество одной или другой стороне (обычно слева). Это предотвращает захват класса с перегрузкой с правой стороны класса с перегрузкой с левой стороны, но не наоборот. (Это лучше всего работает в динамических языках, так как методы могут решить, могут ли они справиться с этим, и динамически отложить до другого.)
  • Сделать ошибку (как подсказывает @dave в своем ответе). Если существует более одного жизнеспособного выбора, это ошибка компилятора. (Это лучше всего работает на статических языках, где вы можете поймать эту вещь заранее.)
  • Позволяет только крайнему левому классу определять перегрузки операторов, как в C ++. (Тогда ваш класс B будет незаконным.)

Единственный другой вариант - ввести сложную систему приоритета перегрузок оператора, но затем вы сказали, что хотите уменьшить когнитивные издержки.

2 голосов
/ 16 марта 2011

Я собираюсь ответить на этот вопрос, сказав: «Не делайте двусмысленных определений».

Если я воссоздаю ваш пример в C ++ (используя функцию f вместо оператора +int / float вместо A / B, но на самом деле нет особой разницы) ...

template<class t>
void f(int a, t b)
{
    std::cout << "me! me! me!";
}

template<class t>
void f(t a, float b)
{
    std::cout << "no, me!";
}

int main(void)
{
    f(1, 1.0f);
    return 0;
}

... компилятор скажет мне точно, что: error C2668: 'f' : ambiguous call to overloaded function

Если вы создадите достаточно мощный язык, всегда будет возможно создать в нем то, что не имеет смысла.Когда это происходит, то, вероятно, можно просто поднять руки и сказать: «Это не имеет смысла».

1 голос
/ 12 июля 2014

Я бы предположил, что с учетом X + Y компилятор должен искать как X.op_plus(Y), так и Y.op_added_to(X);каждая реализация должна включать атрибут, указывающий, должна ли она быть «предпочтительной», «нормальной», «резервной» реализацией, и, необязательно, также указывающая, что она является «общей».Если обе реализации определены, и они имеют разные приоритеты (например, «предпочтительный» и «нормальный»), используйте тип для выбора предпочтения.Если оба определены как имеющие одинаковый приоритет, и оба являются «общими», предпочтение отдается форме X.op_plus(Y).Если оба определены с одним и тем же приоритетом и не являются «общими», отметьте ошибку.

Я бы предположил, что возможность расставлять приоритеты по перегрузкам и преобразованиям будет ИМХО очень важной функцией для языка.Для языков бесполезно грызть неоднозначные перегрузки в тех случаях, когда оба кандидата будут делать одно и то же, но языки должны грызть в тех случаях, когда две возможные перегрузки будут иметь разные значения, каждое из которых будет полезно в определенных контекстах.Например, учитывая someFloat==someDouble или someDouble==someLong, компилятор должен squawk, поскольку может оказаться полезным знать, совпадают ли числовые величины, представленные двумя значениями, и также может быть полезно знать, является лилевый операнд содержит лучшее возможное представление (для своего типа) значения в правом операнде.Java и C # не отмечают неоднозначность в любом случае, вместо этого выбрав использовать первое значение для первого выражения и второе для второго, хотя любое значение может быть полезно в любом случае.Я бы предположил, что было бы лучше отказаться от таких сравнений, чем от того, что они реализуют противоречивую семантику.

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

Добавление

Я кратко просмотрел ваше предложение;он видит, что вы ожидаете, что привязки будут полностью динамичными.Я работал с таким языком (HyperTalk, около 1988 года), и это было «интересно».Рассмотрим, например, что «2X» <«3» <4 <10 <«11» <«2X».Двойная диспетчеризация иногда может быть полезна, но только в тех случаях, когда перегрузки операторов с различной семантикой (например, сравнения строк и чисел) ограничены работой с непересекающимися наборами вещей.Запретить неоднозначные операции во время компиляции - это хорошо, так как программист сможет указать, что предполагается.Наличие такой неоднозначности запускает ошибку времени выполнения, это плохая вещь, потому что программист может давно уйти к тому времени, когда ошибка появляется.Следовательно, я действительно не могу дать никаких советов о том, как выполнить двойную диспетчеризацию во время выполнения для операторов, кроме как сказать «не», если только во время компиляции вы не ограничите операнды комбинациями, где любая возможная перегрузка всегда будет иметь одинаковую семантику.. </p>

Например, если у вас был абстрактный тип «неизменяемый список чисел» с членом, сообщающим длину или возвращающим число по определенному индексу, вы можете указать, что два экземпляра равны, если они имеют одинаковую длину,и каждый для каждого индекса они возвращают одно и то же число.Хотя можно было бы сравнить любые два экземпляра на равенство, исследуя каждый элемент, это могло бы быть неэффективно, если бы, например, один экземпляр был типом «BunchOfZeroes», который просто содержал целое число N = 1000000 и фактически не сохранял никаких элементов, идругой был "NCopiesOfArray", который содержал N = 500000 и {0,0} как массив для копирования.Если многие экземпляры этих типов будут сравниваться, эффективность можно было бы повысить, если бы такие сравнения вызывали метод, который после проверки общей длины массива проверяет, содержит ли массив «шаблон» какие-либо ненулевые элементы.Если это не так, то он может быть представлен как равный массиву с нулями без необходимости выполнять 1 000 000 сравнений элементов.Обратите внимание, что вызов такого метода двойной диспетчеризацией не изменит поведения программы - он просто позволит выполнить ее быстрее.

1 голос
/ 16 марта 2011

В C ++ значение op b означает a.op (b), поэтому оно однозначно; заказ решает это. Если в C ++ вы хотите определить оператор, левый операнд которого является встроенным типом, то оператор должен быть глобальной функцией с двумя аргументами, а не членом; опять же, однако, порядок операндов определяет, какой метод вызывать. Недопустимо определять оператор, в котором оба операнда имеют встроенные типы.

...