Чи є метод перевантаження чимось більшим, ніж синтаксичний цукор? [зачинено]


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
@Giorgio: правильно! У визначній статті Маттіаса Феллейзена є чітке визначення про експресивність. В основному: синтаксичний цукор суто локальний. Якщо вам доведеться змінити глобальну структуру програми, щоб видалити використання мовної функції, це не синтаксичний цукор. Тобто переписування поліморфного коду OO в асемблері зазвичай передбачає додавання глобальної логіки відправки, яка не є чисто локальною, тому OO не є "просто синтаксичним цукром для асемблера".
Йорг W Міттаг

Відповіді:


29

Щоб відповісти на це, спочатку потрібно визначення поняття "синтаксичний цукор". Я піду з Вікіпедією :

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

[...]

Зокрема, конструкція в мові називається синтаксичним цукром, якщо її можна видалити з мови, не впливаючи на те, що може зробити мова

Отже, згідно з цим визначенням, такі функції, як вараги Java або Scala для розуміння, є синтаксичним цукром: вони переводяться на основні мовні особливості (масив у першому випадку закликає скласти карту / flatmap / filter у другому), а видалення їх буде не змінювати те, що можна зробити з мовою.

Однак перевантаження методу не є синтаксичним цукром за цим визначенням, оскільки його видалення кардинально змінить мову (ви більше не зможете відправляти на окрему поведінку на основі аргументів).

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


22
Видалення перевантаження не змінить те, що мова може зробити. Ви все ще могли робити точно такі ж речі, як і раніше; вам доведеться просто перейменувати кілька методів. Це більш тривіальна зміна, ніж петлі знежирення.
Doval

9
Як я вже говорив, ви можете скористатися підходом, що всі мови (включаючи машинні мови) є просто синтаксичним цукром поверх машини Тюрінга.
kdgregory

9
Як я бачу, метод перевантаження просто дозволяє робити sum(numbersArray)і sum(numbersList)замість, sumArray(numbersArray)і sumList(numbersList). Я згоден з Doval, це здається простим синтатичним цукром.
Авів Кон

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.дуже схематичним, тому що типи не потрібно визначати ; вони відомі під час компіляції.
Doval

3
"Щоб вважати синтаксичним цукром, java повинен був би робити вигляд, що ви дійсно написали foo_int та foo_double функції, але це не так." - доки ми говоримо про методи перевантаження, а не про поліморфізми, яка буде різниця між foo(int)/ foo(double)і foo_int/ foo_double? Я не дуже добре знаю Java, але думаю, що таке перейменування дійсно трапляється в JVM (ну, мабуть, foo(args)скоріше, ніж тоді foo_args- це робиться принаймні на C ++ з символічним керуванням (нормально - керування символами технічно є деталізацією реалізації, а не частиною). мови).
Maciej Piechotka

2
@Doval: "Я не думаю, що ніхто ніколи не сказав, що перетворення синтаксичного цукру має бути тривіальним". - Правда, але це має бути місцевим . Єдине корисне визначення синтаксичного цукру, про яке я знаю, - це відомий документ Маттіаса Феллейсена про виразність мови, і в основному він говорить про те, що якщо ви можете переписати програму, написану мовою L + y (тобто деякою мовою L з деякою особливістю y ) в мова L (тобто підмножина цієї мови без функції y ), не змінюючи глобальної структури програми (тобто вносячи лише локальні зміни), y - синтаксичний цукор у L + y і робить
Jörg W Mittag

2
… Не збільшувати експресивність L Однак, якщо ви не можете зробити це, тобто , якщо ви повинні внести зміни в глобальну структуру вашої програми, то це НЕ синтаксичний цукор і робить в тому , макіяжі L + у більш виразного , ніж L . Наприклад, Java з розширеним forциклом не є більш виразною, ніж Java без неї. (Це приємніше, стисліше, читабельніше, а все навколо краще, я б заперечив, але не виразніше.) Я не впевнений у справі перевантаження. Напевно, мені доведеться перечитати папір, щоб бути впевненим. Моя кишка каже, що це синтаксичний цукор, але я не впевнений.
Jörg W Mittag,

2
@MaciejPiechotka, якби це було частиною мовного визначення, функції були так перейменовані, і ви могли отримати доступ до функції під тими назвами, я думаю, це був би синтаксичний цукор. Але оскільки його приховано як деталізація реалізації, я думаю, що це позбавляє його від синтаксичного цукру.
Вінстон Еверт

8

Враховуючи, що керування іменами працює, чи це не повинно бути не що інше як синтаксичний цукор?

Це дозволяє абоненту уявити, що він викликає ту саму функцію, коли його немає. Але він міг знати справжні назви всіх своїх функцій. Тільки якби можна було досягти затримки поліморфізму шляхом передачі нетипізованої змінної в набрану функцію та встановити її тип, щоб виклик міг перейти до потрібної версії відповідно до імені, це було б справжньою мовною особливістю.

На жаль, я ніколи не бачив, щоб мова це робила. Коли є неоднозначність, ці укладачі не вирішують її, вони наполягають на тому, щоб письменник вирішив це за них.


Функція, яку ви шукаєте там, називається "Багаторазова відправка". Багато мов підтримують його, включаючи Haskell, Scala та (з 4.0) C #.
Ієн Галлоуей,

Я хотів би відокремити параметри на класи від перевантаження прямолінійних методів. У випадку перевантаження прямого методу програміст пише всі версії, компілятор просто знає, як вибрати його. Це просто синтаксичний цукор і вирішується простим керуванням іменами навіть для багаторазового відправлення. --- За наявності параметрів на класах компілятор генерує код у міру необхідності, і це повністю змінює.
Джон Джей Обермарк

2
Я думаю, ти неправильно розумієш. Наприклад, в C #, якщо один з параметрів для методу є dynamicте перевантаження дозвіл відбувається під час виконання, а не під час компіляції . Ось що таке багаторазова відправка, і її неможливо повторити перейменуванням функцій.
Ієн Галлоуей

Досить круто. Я все ще можу перевірити змінний тип, тому це все ще лише вбудована функція, накладена на синтаксичний цукор. Це мовна особливість, але лише ледь.
Джон Джей Обермарк

7

Залежно від мови, це синтаксичний цукор чи ні.

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

Зауважте, що динамічна відправка - це форма перевантаження, що динамічно вирішується за деякими параметрами (для деяких мов лише спеціальна, ця , але не всі мови настільки обмежені), і я б не назвав цю форму перевантаження синтаксичним цукром.


Я не впевнений, як інші відповіді роблять набагато кращі, коли по суті неправильні.
Теластин,

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, і що, якщо ви хочете, щоб він повернув справжнє на основі більш простих або складних умов відповідно? Тоді у вас є вагомий привід використовувати перевантаження:

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наведеним вище прикладом також не працюватиме на цій мові. У C ++ є подібні проблеми, тому що коли ви використовуєте щось на зразок пустотілого вказівника чи якийсь інший подібний механізм, він все одно вимагає від вас вручну відключити тип під час компіляції.

Отже, як говорить перший заголовок ...

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


3
Чи можете ви назвати будь-які мови, які справді обробляють Function-Dispatch під час виконання, а не час компіляції? ВСІ мови, які я знаю, вимагають визначеності часу компіляції, яка функція називається ...
Falco

@Falco ActionScript 3.0 обробляє його під час виконання. Можна, наприклад, використовувати функцію , яка повертає один з трьох рядків випадковим чином , а потім використовувати значення, що повертається для виклику будь-яких із трьох функцій випадковим чином : this[chooseFunctionNameAtRandom](); Якщо chooseFunctionNameAtRandom()повертається або "punch", "kick"або "dodge", то ви можете константи виглядають реалізувати дуже простий випадковий елемент, наприклад, AI ворога у флеш-грі.
Panzercrisis

1
Так - але вони є реальними семантичними методами для отримання динамічної диспетчеризації функцій, у Java є і ці. Але вони відрізняються від перевантаження, перевантаження - це статичний і просто синтаксичний цукор, тоді як динамічна відправка та успадкування - це реальні мовні особливості, які пропонують нові функціональні можливості!
Falco

1
... Я також спробував недійсні покажчики в C ++, а також вказівники базового класу, але компілятор хотів, щоб я його самостійно відключив, перш ніж передати його у функцію. Тож зараз мені цікаво, чи видалити цю відповідь. Це починає виглядати так, що мови майже завжди йдуть аж до поєднання вибору динамічної функції з перевантаженням функції в тій же інструкції чи операції, але потім відступають в останню секунду. Це було б приємною мовною особливістю; можливо, комусь потрібно скласти мову, яка має цю.
Panzercrisis

1
Нехай відповідь залишиться, можливо, подумайте над тим, щоб включити до коментарів у відповідь деякі ваші дослідження?
Falco

2

Це дійсно залежить від вашого визначення поняття "синтаксичний цукор". Я спробую розглянути деякі визначення, які мені спадають на думку:

  1. Особливістю є синтаксичний цукор, коли програма, яка використовує його, завжди може бути переведена в іншу, яка не використовує цю функцію.

    Тут ми припускаємо, що існує примітивний набір функцій, який неможливо перекласти: іншими словами, немає циклів типу "ви можете замінити функцію X за допомогою функції Y" і "ви можете замінити функцію Y на функцію X". Якщо одна з двох істинна, то будь-яка інша особливість може бути виражена в ознаках, які не є першими, або це примітивна особливість.

  2. Те саме, що визначення 1, але з додатковою вимогою, що перекладена програма настільки ж безпечна, як і перша, тобто шляхом дегугації ви не втрачаєте будь-якої інформації.

  3. Визначення ОП: особливістю є синтаксичний цукор, якщо його переклад не змінює структуру програми, а вимагає лише "локальних змін".

Візьмемо 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, а потім, якщо 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 на дуже просту мову основної мови (що насправді робиться при компілюванні), тому більшість синтаксису Haskell можна розглядати як "синтаксичний цукор" для чогось, що є просто лямбда-численням та трохи нових конструкцій. Однак ми можемо погодитись, що з програмами Haskell набагато простіше в обробці та дуже стислими, тоді як перекладені програми читати чи думати досить важче.


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 ++. Використовуючи ваше визначення, я вважаю, що в Яві я міг би локально написати ряд перевірок типу (що було б крихким, оскільки нові перевантаження fooпотребують додаткових перевірок). Окрім прийняття цієї крихкості, я не можу внести локальну зміну на сайт для виклику, щоб виправити це, натомість мені доведеться відмовитися від написання загального коду. Я б стверджував, що запобігання роздутого коду може бути синтаксичним цукром, але запобігання крихкому коду є більш ніж цим. З цієї причини статичний поліморфізм загалом - це не просто синтаксичний цукор. Ситуація в певній мові може бути різною, залежно від того, наскільки ця мова дозволяє дістатися, не знаючи статичного типу.


У Java перевантаження завантажуються під час компіляції. Враховуючи використання стирання типу, інакше було б неможливо. Крім того, навіть без стирання типу, якщо T:Animalце має тип SiameseCatі існуючі перевантаження є Cat Foo(Animal), SiameseCat Foo(Cat)і Animal Foo(SiameseCat), що перевантаження повинні бути обрані , якщо Tце SiameseCat?
supercat

@supercat: має сенс. Тож я міг би зрозуміти відповідь, не пам'ятаючи (або, звичайно, запустити її). Тому перевантаження Java не є кращими за цукор, так само як і перевантаження C ++ стосуються загального коду. Залишається можливим, що існує якийсь інший спосіб, як вони краще, ніж просто локальна трансформація. Цікаво, чи слід змінити свій приклад на C ++ або залишити його як якийсь уявний Java-що-не-реальний-Java.
Стів Джессоп

Перевантаження можуть бути корисними у випадках, коли методи мають необов’язкові аргументи, але вони також можуть бути небезпечними. Припустимо, рядок long foo=Math.round(bar*1.0001)*5буде змінено на long foo=Math.round(bar)*5. Як це вплине на семантику, якщо вона barдорівнює, наприклад, 123456789L?
supercat

@supercat Я можу стверджувати, що реальна небезпека полягає в неявному переході з longна double.
Doval

@Doval: До double?
supercat

1

Схоже, «синтаксичний цукор» звучить зневажливо, як марний чи легковажний. Ось чому питання викликає багато негативних відповідей.

Але ви маєте рацію, перевантаження методу не додає до мови жодної функції, окрім можливості використовувати те саме ім’я для різних методів. Ви можете зробити тип параметра явним, програма все одно буде працювати однаково.

Те саме стосується імен пакунків. Рядок - це просто синтаксичний цукор для java.lang.String.

Насправді такий метод, як

void fun(int i, String c);

у класі MyClass слід називати щось на кшталт "my_package_MyClass_fun_int_java_lang_String". Це визначило б метод однозначно. (JVM робить щось подібне всередині). Але ти не хочеш цього писати. Ось чому компілятор дозволить вам писати весело (1, "один") та визначати, який це метод.

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

Якщо у вас дві перевантажені процедури

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

не потрібно знати, що існує конкретна версія процедури для дат. addParameter ("привіт", "світ" викличе першу версію, addParameter ("зараз", нова дата ()) зателефонує другій.

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


1

Цікаво, що відповідь на це питання залежатиме від мови.

Зокрема, існує взаємодія між перевантаженням та загальним програмуванням (*), і залежно від того, як реалізується загальне програмування, це може бути просто синтаксичним цукром (Іржа) або абсолютно необхідним (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 ++), видалення одного чи іншого не матиме чистого ефекту, оскільки будь-який може використовуватися для реалізації необов'язкових аргументів.


Haskell - хороший приклад того, чому перевантаження оператора не є синтаксичним цукром, але я думаю, що кращим прикладом може бути деконструкція типу алгебраїчних даних із узгодженням шаблону (те, що, наскільки я знаю, неможливо без відповідності шаблону).
11684

@ 11684: Ви можете вказати на приклад? Я, чесно кажучи, взагалі не знаю Haskell, але знайшов його узор, який відповідає піднесено елегантно, коли я побачив цей приклад фіб (на комп'ютеріфілі на 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_String та mangle_int - оскільки список аргументів є частиною можливого ідентифікатора функції, це практично те, що відбувається -> і тому це лише синтаксичний цукор.

Тепер, якщо є мова, де функція дійсно визначається під час виконання, як з перекритими методами в об'єктах, це було б інакше. Але я не думаю, що існує така мова, оскільки метод завантаження схильний до неоднозначності, яку компілятор не може вирішити і з якою повинен обробляти програміст із чітким складом. Це неможливо зробити під час виконання.


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

і амплуа на довгий буде необов’язковим.

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

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.