Является ли перегрузка метода чем-то большим, чем синтаксический сахар? [закрыто]

19

Является ли метод перегрузки типом полиморфизма? Мне кажется, что это просто дифференциация методов с одинаковыми именами и разными параметрами. Так что stuff(Thing t)и stuff(Thing t, int n)это совершенно разные методы в отношении компилятора и среды выполнения.

Это создает иллюзию со стороны вызывающей стороны, что это один и тот же метод, который по-разному действует на разные типы объектов - полиморфизм. Но это только иллюзия, потому что на самом деле stuff(Thing t)и stuff(Thing t, int n)это совершенно разные методы.

Является ли перегрузка метода чем-то большим, чем синтаксический сахар? Я что-то пропустил?


Распространенным определением синтаксического сахара является то, что он является чисто локальным . Смысл изменения части кода на «подслащенный» эквивалент или наоборот, включает в себя локальные изменения, которые не влияют на общую структуру программы. И я думаю, что метод перегрузки точно соответствует этому критерию. Давайте посмотрим на пример, чтобы продемонстрировать:

Рассмотрим класс:

class Reader {
    public String read(Book b){
        // .. translate the book to text
    }
    public String read(File b){
        // .. translate the file to text
    }
}

Теперь рассмотрим другой класс, который использует этот класс:

/* might not be the best example */
class FileProcessor {
    Reader reader = new Reader();
    public void process(File file){
        String text = reader.read(file);
        // .. do stuff with the text
    }
}

Ладно. Теперь давайте посмотрим, что нужно изменить, если мы заменим перегрузку метода обычными методами:

Эти readметоды в Readerсмену readBook(Book)и readFile(file). Только вопрос изменения их имен.

Код вызова FileProcessorизменяется незначительно: reader.read(file)изменяется на reader.readFile(file).

Вот и все.

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

Я хотел бы услышать ваши возражения, если у вас есть, может быть, я что-то упустил.

Авив Кон
источник
48
В конце концов, любая функция языка программирования является просто синтаксическим сахаром для необработанного ассемблера.
Филипп
31
@Philipp: Извините, но это действительно глупое утверждение. Языки программирования получают свою полезность из семантики, а не синтаксиса. Такие функции, как система типов, дают вам реальные гарантии, хотя на самом деле они могут требовать от вас больше писать .
back2dos
3
Задайте себе вопрос: перегрузка оператора - это просто синтаксический сахар? Какой бы ответ на этот вопрос вы не ответили, это также ответ на заданный вами вопрос;)
back2dos
5
@ back2dos: полностью согласен с вами. Я слишком часто читаю фразу «все просто синтаксический сахар для ассемблера», и это явно неправильно. Синтаксический сахар - это альтернативный (возможно, более приятный) синтаксис для некоторого существующего синтаксиса, который не добавляет никакой новой семантики.
Джорджио
6
@ Джорджио: правильно! В историческом документе Матиаса Феллайзена о выразительности есть точное определение. В основном: синтаксический сахар чисто местный. Если вам нужно изменить глобальную структуру программы, чтобы исключить использование языковой функции, то это не синтаксический сахар. Т.е. переписывание полиморфного ОО-кода в ассемблере обычно включает добавление глобальной логики диспетчеризации, которая не является чисто локальной, поэтому ОО не является «просто синтаксическим сахаром для ассемблера».
Йорг Миттаг

Ответы:

29

Чтобы ответить на это, сначала нужно определить «синтаксический сахар». Я пойду с Википедией :

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

[...]

В частности, конструкция в языке называется синтаксическим сахаром, если она может быть удалена из языка без какого-либо влияния на то, что может делать язык

Таким образом, согласно этому определению, такие функции, как varargs Java или простое понимание Scala, являются синтаксическим сахаром: они переводят в базовые языковые функции (массив в первом случае, вызовы map / flatmap / filter во втором), и удаление их будет не меняйте то, что вы можете сделать с языком.

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

Правда, вы можете имитировать перегрузку метода, если у вас есть какой-то способ доступа к аргументам метода, и вы можете использовать конструкцию «если», основанную на заданных вами аргументах. Но если вы считаете, что синтаксический сахар, вы должны рассмотреть все, что выше машины Тьюринга, чтобы быть синтаксическим сахаром.

kdgregory
источник
22
Устранение перегрузки не изменит того, что может сделать язык. Вы все еще можете делать то же самое, что и раньше; вам просто нужно переименовать несколько методов. Это более тривиальное изменение, чем обесцвечивание циклов.
Довал
9
Как я уже сказал, вы могли бы принять подход, согласно которому все языки (включая машинные языки) являются просто синтаксическим сахаром на вершине машины Тьюринга.
kdgregory
9
Как я вижу, перегрузка метода просто позволяет вам делать, sum(numbersArray)а sum(numbersList)не sumArray(numbersArray)и sumList(numbersList). Я согласен с Довалем, это похоже на простой синтетический сахар.
Авив Кон
3
Большая часть языка. Попробуйте реализующий instanceof, классы, наследование, интерфейсы, дженерики, отражение или спецификаторов доступа , используя if, while, и логические операторы, с точно такой же семантикой . Нет угловых случаев. Обратите внимание, что я не призываю вас вычислять те же вещи, что и конкретные применения этих конструкций. Я уже знаю, что вы можете вычислить все, что угодно, используя логическую логику и ветвления / циклы. Я прошу вас реализовать точные копии семантики этих языковых функций, включая любые статические гарантии, которые они предоставляют (проверки во время компиляции должны выполняться во время компиляции.)
Doval
6
@Doval, kdgregory: чтобы определить синтаксический сахар, вы должны определить его относительно некоторой семантики. Если единственная ваша семантика - «Что вычисляет эта программа?», То ясно, что все является синтаксическим сахаром для машины Тьюринга. С другой стороны, если у вас есть семантика, в которой вы можете говорить об объектах и ​​определенных операциях с ними, то удаление определенного синтаксиса не позволит вам больше выражать эти операции, даже если язык все еще может быть завершен по Тьюрингу.
Джорджио
13

Термин синтаксический сахар обычно относится к случаям, когда признак определяется заменой. Язык не определяет, что делает функция, а определяет, что она в точности эквивалентна чему-то другому. Так, например, для каждого цикла

for(Object alpha: alphas) {
}

становится:

for(Iterator<Object> iter = alpha.iterator(); iter.hasNext()) {
   alpha = iter.next();
}

Или возьмите функцию с переменными аргументами:

void foo(int... args);

foo(3, 4, 5);

Который становится:

void Foo(int[] args);

foo(new int[]{3, 4, 5});

Таким образом, существует тривиальная замена синтаксиса для реализации функции с точки зрения других функций.

Давайте посмотрим на перегрузку метода.

void foo(int a);
void foo(double b);

foo(4.5);

Это может быть переписано как:

void foo_int(int a);
void foo_double(double b);

foo_double(4.5);

Но это не эквивалентно этому. В модели Java это нечто иное. foo(int a)не реализует foo_intфункцию, которая будет создана. Java не реализует перегрузку методов, давая неоднозначные функции забавные имена. Чтобы считаться синтаксическим сахаром, java должен был притворяться, что вы действительно написали foo_intи foo_doubleработаете, но это не так.

Уинстон Эверт
источник
2
Я не думаю, что кто-либо когда-либо говорил, что преобразование для синтаксического сахара должно быть тривиальным. Даже если это так, я нахожу это утверждение But, the transformation isn't trivial. At the least, you have to determine the types of the parameters.очень схематичным, потому что типы не нужно определять ; они известны во время компиляции.
Довал
3
«Чтобы считаться синтаксическим сахаром, java должен был делать вид, что вы действительно написали функции foo_int и foo_double, но это не так». - Пока мы говорим о методах перегрузки, а не о полиморфизмах, какая разница между foo(int)/ foo(double)и foo_int/ foo_double? Я не очень хорошо знаю Java, но я бы предположил, что такое переименование действительно происходит в JVM (ну, возможно, с использованием foo(args)скорее, чем foo_argsэто происходит - по крайней мере, в C ++ с манипулированием символами (хорошо - маниглинг символов - технически деталь реализации, а не часть). языка).
Мацей Печотка
2
@Doval: «Я не думаю, что кто-то когда-либо говорил, что преобразование синтаксического сахара должно быть тривиальным». - Правда, но это должно быть локально . Единственное полезное определение синтаксического сахара, о котором я знаю, взято из известной статьи Матиаса Феллайзена о выразительности языка, и в основном говорится, что если вы можете переписать программу, написанную на языке L + y (то есть некоторый язык L с некоторой функцией y ) в язык L (т. е. подмножество этого языка без функции y ) без изменения глобальной структуры программы (т. е. только с локальными изменениями), тогда y является синтаксическим сахаром в L + y и выполняет
Йорг Миттаг
2
… Не увеличивать выразительность L. Однако, если вы не можете сделать это, то есть , если вы должны внести изменения в глобальную структуру вашей программы, то это не синтаксический сахар и делает в том , макияже L + у более выразительного , чем L . Например, Java с расширенным forциклом не более выразительна, чем Java без него. (Я бы поспорил, что это лучше, лаконичнее, удобочитаемее и лучше вокруг, но не более выразительно.) Однако я не уверен насчет случая перегрузки. Вероятно, мне придется перечитать статью, чтобы быть уверенным. Моя кишка говорит, что это синтаксический сахар, но я не уверен.
Йорг Миттаг
2
@MaciejPiechotka, если бы это было частью определения языка, функции были бы так переименованы, и вы могли бы получить доступ к функции под этими именами, я думаю, это был бы синтаксический сахар. Но так как он скрыт как деталь реализации, я думаю, что это лишает его синтаксического сахара.
Уинстон Эверт
8

Учитывая, что искажение имен работает, разве это не должно быть ничем иным, как синтаксическим сахаром?

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

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

Джон Джей Обермарк
источник
Функция, которую вы ищете, называется «Многократная отправка». Множество языков поддерживают его, включая Haskell, Scala и (с 4.0) C #.
Иэн Галлоуэй,
Я хотел бы отделить параметры классов от прямой перегрузки метода. В случае прямой перегрузки метода программист записывает все версии, а компилятор просто знает, как выбрать одну. Это просто синтаксический сахар, и он решается простым искажением имен даже для многократной отправки. --- При наличии параметров в классах компилятор генерирует код по мере необходимости, и это полностью его меняет.
Джон Джей Обермарк
2
Я думаю, вы неправильно поняли. Например, в C #, если один из параметров для метода является dynamicто перегрузка разрешение происходит во время выполнения, а не во время компиляции . Вот что такое множественная диспетчеризация, и она не может быть реплицирована переименованием функций.
Иэн Галлоуэй
Довольно круто. Однако я все еще могу проверить тип переменной, так что это все еще встроенная функция, наложенная на синтаксический сахар. Это особенность языка, но едва ли.
Джон Джей Обермарк
7

В зависимости от языка, это синтаксический сахар или нет.

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

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

AProgrammer
источник
Я не уверен, как другие ответы делают намного лучше, когда по существу неверны.
Теластин
5

Для современных языков это просто синтаксический сахар; в совершенно не зависящем от языка виде, это нечто большее.

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

Вот почему это должно быть больше.

Рассмотрим язык, который поддерживает как перегрузку метода, так и нетипизированные переменные. Вы можете иметь следующие прототипы методов:

bool someFunction(int arg);

bool someFunction(string arg);

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

dict roomNumber; // some hotels use numbers, some use letters, and some use
                 // alphanumerical strings.  In some languages, built-in dictionary
                 // types automatically use untyped values for their keys to map to,
                 // so it makes more sense then to allow for both ints and strings in
                 // your code.

Что же делать, если вы хотите подать заявку someFunctionна один из этих номеров? Вы называете это:

someFunction(roomNumber[someSortOfKey]);

Является ли someFunction(int)называется, или someFunction(string)называется? Здесь вы видите один пример, где это не полностью ортогональные методы, особенно в языках более высокого уровня. Язык должен выяснить - во время выполнения - какой из них вызывать, поэтому он все равно должен рассматривать их как, по крайней мере, один и тот же метод.

Почему бы просто не использовать шаблоны? Почему бы просто не использовать нетипизированный аргумент?

Гибкость и более тонкий контроль. Иногда лучше использовать шаблоны / нетипизированные аргументы, но иногда это не так.

Вы должны подумать о случаях, когда, например, у вас может быть две сигнатуры метода, каждая из которых принимает аргументы inta и a string, но порядок в каждой сигнатуре различен. У вас вполне может быть веская причина для этого, так как реализация каждой подписи может делать в основном одно и то же, но только с немного другим поворотом; регистрация может быть другой, например. Или даже если они делают одну и ту же вещь, вы можете автоматически получать определенную информацию только из того порядка, в котором были указаны аргументы. Технически вы могли бы просто использовать операторы псевдопереключателя, чтобы определить тип каждого из переданных аргументов, но это может привести к путанице.

Так что это следующий пример плохой практики программирования?

bool stringIsTrue(int arg)
{
    if (arg.toString() == "0")
    {
        return false;
    }
    else
    {
        return true;
    }
}

bool stringIsTrue(Object arg)
{
    if (arg.toString() == "0")
    {
        return false;
    }
    else
    {
        return true;
    }
}

bool stringIsTrue(string arg)
{
    if (arg == "0")
    {
        return false;
    }
    else
    {
        return true;
    }
}

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

bool stringIsTrue(untyped arg)
{
    if (arg.toString() == "0")
    {
        return false;
    }
    else
    {
        return true;
    }
}

Но что, если вам нужно использовать это только для ints и strings, и что, если вы хотите, чтобы он возвращал true, основываясь на более простых или более сложных условиях соответственно? Тогда у вас есть веская причина использовать перегрузку:

bool appearsToBeFirstFloor(int arg)
{
    if (arg.digitAt(0) == 1)
    {
        return true;
    }
    else
    {
        return false;
    }
}

bool appearsToBeFirstFloor(string arg)
{
    string firstCharacter = arg.characterAt(0);
    if (firstCharacter.isDigit())
    {
        return appearsToBeFirstFloor(int(firstCharacter));
    }
    else if (firstCharacter.toUpper() == "A")
    {
        return true;
    }
    else
    {
        return false;
    }
}

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

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

appearsToBeFirstFloor(roomNumber[someSortOfKey]);

// will treat ints and strings differently, without you having to write extra code
// every single spot where the function is being called

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

Но ... Вот почему это не более чем синтаксический сахар в современных языках.

Falco поднял вопрос в комментариях о том, что современные языки в основном не смешивают перегрузку методов и динамический выбор функций в пределах одного шага. Ранее я понимал, что некоторые языки работают, так как вы могли перегрузить appearsToBeFirstFloorв приведенном выше примере, а затем язык во время выполнения будет определять, какую версию функции вызывать, в зависимости от значения времени выполнения нетипизированной переменной. Эта путаница частично возникла из-за работы с языками ECMA-типов, такими как ActionScript 3.0, в которых вы можете легко рандомизировать, какая функция вызывается для определенной строки кода во время выполнения.

Как вы, возможно, знаете, ActionScript 3 не поддерживает перегрузку методов. Что касается VB.NET, вы можете объявлять и устанавливать переменные без явного присвоения типа, но когда вы пытаетесь передать эти переменные в качестве аргументов перегруженным методам, он все равно не хочет читать значение времени выполнения, чтобы определить, какой метод вызывать; вместо этого он хочет найти метод с аргументами типа Objectили без типа или что-то подобное. Таким образом, приведенный выше пример intпротив stringне будет работать и на этом языке. С ++ имеет аналогичные проблемы, например, когда вы используете что-то вроде указателя void или какой-то другой механизм, подобный этому, он все равно требует, чтобы вы вручную устраняли неоднозначность типа во время компиляции.

Так как первый заголовок говорит ...

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

Panzercrisis
источник
3
Можете ли вы назвать какие-нибудь языки, которые действительно обрабатывают функцию-диспетчеризацию во время выполнения, а не во время компиляции? ВСЕ языки, которые я знаю, требуют, чтобы во время компиляции была определена, какая функция вызывается ...
Falco
@Falco ActionScript 3.0 обрабатывает его во время выполнения. Можно, например, использовать функцию , которая возвращает один из трех строк случайным образом , а затем использовать возвращаемое значение для вызова любых из трех функций случайным образом : this[chooseFunctionNameAtRandom](); Если chooseFunctionNameAtRandom()возвращается либо "punch", "kick"или "dodge", то вы можете константы выглядят реализовать очень простой случайный элемент, например, ИИ противника во флэш-игре.
Panzercrisis
1
Да - но оба они являются реальными семантическими методами для получения динамической диспетчеризации функций, в Java они есть. Но они отличаются от перегрузки, перегрузка является статической и просто синтаксическим сахаром, в то время как динамическая диспетчеризация и наследование являются реальными языковыми функциями, которые предлагают новую функциональность!
Falco
1
... Я также пробовал указатели void в C ++, а также указатели базовых классов, но компилятор хотел, чтобы я устранял неоднозначность сам перед тем, как передать его функции. Так что теперь мне интересно, можно ли удалить этот ответ. Начинает казаться, что языки почти всегда подходят к сочетанию динамического выбора функции с перегрузкой функции в одной и той же инструкции или выражении, но затем отступают в последнюю секунду. Это была бы хорошая языковая особенность; Может быть, кто-то должен сделать язык, который имеет это.
Panzercrisis
1
Позвольте ответу остаться, возможно, подумайте о включении некоторых ваших исследований из комментариев в ответ?
Falco
2

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

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

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

  2. То же, что и в определении 1, но с дополнительным требованием, чтобы переведенная программа была такой же типобезопасной, как и первая, то есть, обескураживая, вы не теряете никакой информации.

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

Давайте возьмем Haskell в качестве примера для перегрузки. Haskell обеспечивает пользовательскую перегрузку через классы типов. Например, +и *операции определены в Numклассе типа и любой тип , который имеет (полный) экземпляр такого класса может быть использован с +. Например:

instance Num a => Num (b, a) where
    (x, y) + (_, y') = (x, y + y')
    -- other definitions

("Hello", 1) + ("World", 3) -- -> ("Hello", 4)

Одна известная вещь о классах типов в Haskell - это то, что вы можете избавиться от них . Т.е. вы можете перевести любую программу, которая использует классы типов, в эквивалентную программу, которая их не использует.

Перевод довольно прост:

  • Дано определение класса:

    class (P_1 a, ..., P_n a) => X a where
        op_1 :: t_1   ... op_m :: t_m
    

    Вы можете перевести его в алгебраический тип данных:

    data X a = X {
        X_P_1 :: P_1 a, ... X_P_n :: P_n a,
        X_op_1 :: t_1, ..., X_op_m :: t_m
    }
    

    Вот X_P_iи X_op_iесть селекторы . Т.е. данное значение типа, X aприменяемое X_P_1к значению, вернет значение, хранящееся в этом поле, поэтому они являются функциями с типом X a -> P_i a(или X a -> t_i).

    Для очень грубой анологии вы можете думать о значениях для типа X aкак structs, а затем if xимеет тип X aвыражений:

    X_P_1 x
    X_op_1 x
    

    можно рассматривать как:

    x.X_P_1
    x.X_op_1
    

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

  • Учитывая объявление экземпляра:

    instance (C_1 a_1, ..., C_n a_n) => X (T a_1 ... a_n) where
        op_1 = ...; ...;  op_m = ...
    

    Вы можете перевести его в функцию, которая при наличии словарей для C_1 a_1, ..., C_n a_nклассов возвращает значение словаря (то есть значение типа X a) для типа T a_1 ... a_n.

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

    f :: C_1 a_1 -> ... -> C_n a_n -> X (T a_1 ... a_n)
    

    (Обратите внимание, что nможет быть 0).

    И на самом деле мы можем определить это как:

    f c1 ... cN = X {X_P_1=get_P_1_T, X_P_n=get_P_n_T,
                     X_op_1=op_1, ..., X_op_m=op_m}
        where
            op_1 = ...
            ...
            op_m = ...
    

    где op_1 = ...в op_m = ...приведены определения , найденные в instanceдекларации и get_P_i_Tявляются функции , определенные P_iэкземпляром Tтипа (они должны существовать , потому что P_iей являются суперклассами X).

  • Учитывая вызов перегруженной функции:

    add :: Num a => a -> a -> a
    add x y = x + y
    

    Мы можем явно передать словари относительно ограничений класса и получить эквивалентный вызов:

    add :: Num a -> a -> a -> a
    add dictNum x y = ((+) dictNum) x y
    

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

Это просто очень быстрый набросок обо всем этом. Если вы заинтересованы, вы должны прочитать статьи Саймона Пейтона Джонс и соавт.

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

Однако это показывает, что если ваше определение синтаксического сахара (1), то перегрузка является синтаксическим сахаром . Потому что вы можете избавиться от этого.

Однако переведенная программа теряет некоторую информацию об исходной программе. Например, это не означает, что экземпляры для родительских классов существуют. (Даже если операции по извлечению родительских словарей по-прежнему должны быть этого типа, вы можете передавать undefinedили другие полиморфные значения, чтобы иметь возможность создать значение X yбез создания значений дляP_i y , чтобы перевод не потерял все тип безопасности). Следовательно, это не синтактический сахар согласно (2)

Что касается (3). Я не знаю, должен ли ответ быть да или нет.

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

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


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

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

Bakuriu
источник
2

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

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

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

В Java вы можете рассмотреть это не просто как удобство, учитывая, например, сколько существует перегрузок PrintStream.printи PrintStream.println. Но тогда существует столько же DataInputStream.readXметодов, поскольку Java не перегружает возвращаемый тип, поэтому в некотором смысле это просто для удобства. Это все для примитивных типов.

Я не помню, что происходит в Java, если у меня есть классы Aи Bрасширения O, я перегружаю методы foo(O), foo(A)и foo(B), затем, в общем, <T extends O>я вызываю foo(t)где tэкземпляр T. В том случае , когда Tэто Aя получаю депешу на основе перегрузки или это , как если бы я назвал foo(O)?

Если первое, то перегрузки метода Java лучше, чем сахара, так же, как перегрузки C ++. Используя ваше определение, я предполагаю, что в Java я мог бы локально написать серию проверок типов (которые были бы хрупкими, потому что новые перегрузки fooпотребовали бы дополнительных проверок). Помимо принятия этой хрупкости, я не могу вносить локальные изменения на сайте вызовов, чтобы сделать это правильно, вместо этого мне пришлось бы отказаться от написания общего кода. Я бы сказал, что предотвращение вздутого кода может быть синтаксическим сахаром, но предотвращение хрупкого кода - это нечто большее. По этой причине статический полиморфизм в целом - это больше, чем просто синтаксический сахар. Ситуация на конкретном языке может отличаться, в зависимости от того, насколько далеко язык позволяет вам пройти, «не зная» статического типа.

Стив Джессоп
источник
В Java перегрузки разрешаются во время компиляции. Учитывая использование стирания типа, для них было бы невозможно быть иначе. Кроме того, даже без стирания типа, если T:Animalэто имеет тип SiameseCatи существующие перегрузки являются Cat Foo(Animal), SiameseCat Foo(Cat)и Animal Foo(SiameseCat), что перегрузка должны быть выбраны , если Tэто SiameseCat?
суперкат
@supercat: имеет смысл. Так что я мог бы выяснить ответ, не помня (или, конечно, запустить его). Следовательно, перегрузки Java не лучше, чем сахар, так же, как перегрузки C ++ относятся к универсальному коду. По-прежнему возможно, есть какой-то другой способ, которым они лучше, чем просто локальная трансформация. Интересно, стоит ли мне изменить мой пример на C ++ или оставить его как-то воображаемое-Java-это-не-реальное-Java?
Стив Джессоп
Перегрузки могут быть полезны в случаях, когда методы имеют необязательные аргументы, но они также могут быть опасными. Предположим, что строка long foo=Math.round(bar*1.0001)*5меняется на long foo=Math.round(bar)*5. Как это повлияет на семантику, если barравно, например, 123456789L?
суперкат
@supercat Я бы сказал, что реальная опасность заключается в неявном преобразовании из longв double.
Довал
@Doval: Кому double?
суперкат
1

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

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

То же самое относится и к именам пакетов. Строка - это просто синтаксический сахар для java.lang.String.

На самом деле такой метод, как

void fun(int i, String c);

в классе MyClass должно называться что-то вроде «my_package_MyClass_fun_int_java_lang_String». Это идентифицирует метод однозначно. (JVM делает что-то подобное внутри). Но ты не хочешь писать это. Вот почему компилятор позволит вам написать fun (1, «one») и определить, какой это метод.

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

Если у вас есть две перегруженные процедуры

addParameter(String name, Object value);
addParameter(String name, Date value);

вам не нужно знать, что существует конкретная версия процедуры для дат. addParameter ("hello", "world) вызовет первую версию, addParameter (" now ", new Date ()) вызовет вторую.

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

Флориан Ф
источник
1

Интересно, что ответ на этот вопрос будет зависеть от языка.

В частности, существует взаимодействие между перегрузкой и универсальным программированием (*), и в зависимости от того, как реализовано универсальное программирование, оно может быть просто синтаксическим сахаром (Rust) или абсолютно необходимым (C ++).

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

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

(*) Используется в смысле написания метода один раз, чтобы работать с различными типами единообразно.

Матье М.
источник
0

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

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

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

Вот классическая наивная реализация функции Фибоначчи в Haskell:

fib 0 = 0
fib 1 = 1
fib n = fib (n-1) + fib (n-2)

Возможно, три функции могут быть заменены на if / else, как это обычно делается на любом другом языке. Но это в основном делает совершенно простое определение:

fib n = fib (n-1) + fib (n-2)

гораздо сложнее и прямо не выражает математическое понятие последовательности Фибоначчи.

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


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

function print (string x) { stdout.print(x) };
function print (number x) { stdout.print(x.toString) };

альтернативно может быть реализован как:

function printString (string x) {...}
function printNumber (number x) {...}

или даже:

function print (auto x) {
    if (x instanceof String) {...}
    if (x instanceof Number) {...}
}

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

function print (string x) {...}
function print (string x, stream io) {...}

может быть использован для реализации:

function print (string x, stream io=stdout) {...}

На таком языке (Google "Ferite language") удаление перегрузки операторов резко удаляет одну особенность - необязательные аргументы. Допускается, что в языках с обеими функциями (c ++) удаление одного или другого не будет иметь никакого эффекта, поскольку любой из них может быть использован для реализации необязательных аргументов.

slebetman
источник
Haskell - хороший пример того, почему перегрузка операторов не является синтаксическим сахаром, но я думаю, что лучшим примером будет деконструкция алгебраического типа данных с сопоставлением с образцом (что, насколько я знаю, невозможно без сопоставления с образцом).
11684
@ 11684: Можете ли вы указать на пример? Я, честно говоря, вообще не знаю Haskell, но нашел, что его паттерн совпадает с изящной элегантностью, когда я увидел этот пример с фибом (на computerphile на youtube).
Slebetman
Имея такой тип данных, как data PaymentInfo = CashOnDelivery | Adress String | UserInvoice CustomerInfoвы, вы можете сопоставить шаблон с конструкторами типов.
11684
Как это: getPayment :: PaymentInfo -> a getPayment CashOnDelivery = error "Should have been paid already" getPayment (Adress addr) = -- code to notify administration to send a bill getPayment (UserInvoice cust) = --whatever. I took the data type from a Haskell tutorial and have no idea what an invoice is. Я надеюсь, что этот комментарий несколько понятен.
11684
0

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

Пример в Java:

String s; int i;
mangle(s);  // Translates to CALL ('mangle':LString):(s)
mangle(i);  // Translates to CALL ('mangle':Lint):(i)

Таким образом, в конце он может быть полностью заменен простым макросом-компилятором с поиском и заменой, заменяя перегруженные функции mangle на mangle_String и mangle_int - поскольку список аргументов является частью возможного идентификатора функции, это практически то, что происходит -> и поэтому это только синтаксический сахар.

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

Falco
источник
0

В Java информация о типе компилируется, и какая из перегрузок вызывается, решает во время компиляции.

Ниже приведен фрагмент из sun.misc.Unsafe(утилита для Atomics), который просматривается в редакторе файлов классов Eclipse.

  // Method descriptor #143 (Ljava/lang/Object;I)I (deprecated)
  // Stack: 4, Locals: 3
  @java.lang.Deprecated
  public int getInt(java.lang.Object arg0, int arg1);
    0  aload_0 [this]
    1  aload_1 [arg0]
    2  iload_2 [arg1]
    3  i2l
    4  invokevirtual sun.misc.Unsafe.getInt(java.lang.Object, long) : int [231]
    7  ireturn
      Line numbers:
        [pc: 0, line: 213]

Как вы можете видеть, информация о типе вызываемого метода (строка 4) включена в вызов.

Это означает, что вы можете создать Java-компилятор, который принимает информацию о типе. Например, используя такую ​​запись, источник вышеупомянутого будет:

@Deprecated
public in getInt(Object arg0, int arg1){
     return getInt$(Object,long)(arg0, arg1);
}

и приведение к длинному будет необязательным.

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

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

чокнутый урод
источник