Чому не допускається перевантаження типами повернення? (принаймні у звичайних мовах)


9

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

Я маю на увазі щось подібне:

 int method1 (int num)
 {

 }
 long method1 (int num)
 {

 }

Справа не в тому, що це велике питання для програмування, але в деяких випадках я б вітав це.

Очевидно, що ці мови не могли б підтримати це без способу розмежування того, який метод викликається, але синтаксис для цього може бути таким же простим, як щось на зразок [int] method1 (num) або [long] method1 (num) таким чином компілятор буде знати, що було б тим, що буде називатися.

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

Які причини того, що щось подібне не підтримується?


Можливо, ваше питання стане кращим із прикладом, коли неявних перетворень між двома типами повернення не існує - наприклад, класи Fooта Bar.

Ну чому б така функція була корисною?
Джеймс Янгмен

3
@JamesYoungman: Наприклад, для розбору рядків на різні типи, ви можете мати метод int read (String s), float read (String s) тощо. Кожна перевантажена варіація методу виконує розбір відповідного типу.
Джорджіо

Це, до речі, лише проблема зі статистично набраними мовами. Наявність декількох типів повернення є досить рутинною в динамічно набраних мовах, таких як Javascript або Python.
Gort the Robot

1
@StevenBurnap, гм, ні. Наприклад, з JavaScript, ви взагалі не можете виконувати перевантаження функцій. Отже, це фактично лише проблема з мовами, які підтримують перевантаження імен функції.
Девід Арно

Відповіді:


19

Це ускладнює перевірку типу.

Якщо ви дозволяєте перевантажувати лише на основі типів аргументів і дозволяєте виводити змінні типи з їх ініціалізаторів, вся інформація про ваш тип тече в одному напрямку: вгору до синтаксичного дерева.

var x = f();

given      f   : () -> int  [upward]
given      ()  : ()         [upward]
therefore  f() : int        [upward]
therefore  x   : int        [upward]

Якщо ви дозволяєте інформації про тип рухатися в обох напрямках, наприклад, для виведення типу змінної з її використання, вам потрібно вирішити обмеження (наприклад, алгоритм W для систем типу Hindley – Milner) для визначення типу.

var x = parse("123");
print_int(x);

given      parse        : string -> T  [upward]
given      "123"        : string       [upward]
therefore  parse("123") : ∃T           [upward]
therefore  x            : ∃T           [upward]
given      print_int    : int -> ()    [upward]
therefore  print_int(x) : ()           [upward]
therefore  int -> ()    = ∃T -> ()     [downward]
therefore  ∃T           = int          [downward]
therefore  x            : int          [downward]

Тут нам потрібно було залишити тип xяк невирішену змінну типу ∃T, де все, що ми знаємо про нього, це те, що він піддається синтаксичному аналізу. Лише пізніше, коли xвикористовується в конкретному типі, у нас є достатньо інформації для вирішення обмеження та визначення того ∃T = int, що поширює інформацію типу вниз по синтаксичному дереву з виразу виклику в x.

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

З цього мовний дизайнер може зробити висновок:

  • Це додає складності реалізації.

  • Це робить перевірку типу повільнішою - у патологічних випадках так експоненціально.

  • Складно створювати хороші повідомлення про помилки.

  • Це занадто відрізняється від статусного кво.

  • Мені не подобається це реалізовувати.


1
Також: Буде так важко зрозуміти, що в деяких випадках ваш компілятор зробить вибір, якого програміст абсолютно не очікував, що призведе до важкого пошуку помилок.
gnasher729

1
@ gnasher729: Я не згоден. Я регулярно використовую Haskell, який має цю особливість, і мене ніколи не кусав його вибір перевантаження (саме екземпляр typeclass). Якщо щось неоднозначне, це просто змушує мене додати анотацію про тип. І я все-таки вирішив реалізувати повноцінний висновок у своїй мові, тому що це неймовірно корисно. Цією відповіддю я виступав як захисник диявола.
Джон Перді,

4

Тому що це неоднозначно. Використовуючи C # як приклад:

var foo = method(42);

Яке перевантаження ми повинні використати?

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

Як щодо цього? (Трохи перероблені підписи для ілюстрації пункту.)

short method(int num) { ... }
int method(int num) { ... }

....

long foo = method(42);

Чи слід використовувати intперевантаження чи shortперевантаження? Ми просто не знаємо, нам доведеться вказати це з вашим [int] method1(num)синтаксисом. Що трохи боляче розібратися і написати, якщо чесно.

long foo = [int] method(42);

Вся справа в тому, що на диво схожий синтаксис із загальним методом у C #.

long foo = method<int>(42);

(C ++ та Java мають подібні функції.)

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

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


Хороший момент щодо дженериків, хоча ви, здається, плутаєте shortі int.
Роберт Харві

Так @RobertHarvey ти маєш рацію. Я боровся за приклад, щоб проілюструвати суть. Було б краще, якби methodповернули короткий або int, а тип був визначений як довгий.
RubberDuck

Це здається трохи кращим.
RubberDuck

Я не купую ваші аргументи про те, що у вас не може бути висновку про тип, анонімні підпрограми чи розуміння монади мовою із перевантаженням типу повернення. Haskell це робить, а Haskell має всі три. Він також має параметричний поліморфізм. Ваша думка про long/ int/ shortбільше стосується складності субтипування та / або неявних перетворень, ніж про перевантаження типів повернення. В кінці кінців, число літералів будуть перевантажені на їх тип повертається значення в C ++, Java, C♯, і багато інших, і що , здається, не викликає особливих проблем. Ви можете просто скласти правило: наприклад, вибрати найбільш конкретний / загальний тип.
Йорг W Міттаг

@ JörgWMittag моя думка полягала не в тому, що це зробить неможливим, а лише те, що робить речі надмірно складними.
RubberDuck

0

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

У більшості мов можна було б очікувати, що вираз method1(2)має певний тип і більш-менш передбачуване повернене значення. Але якщо ви дозволяєте перевантажувати значення, що повертаються, це означає, що неможливо сказати, що означає це вираження взагалі, не враховуючи навколо нього контекст. Поміркуйте, що відбувається, коли у вас є unsigned long long foo()метод, реалізація якого закінчується return method1(2)? Чи повинен це викликати longперевантаження перезавантаження або intперезавантаження або просто дати помилку компілятора?

Крім того, якщо вам потрібно допомогти компілятору, анотувавши тип повернення, ви не тільки вигадуєте більше синтаксису (що збільшує всі вищезгадані витрати на те, щоб функція взагалі існувала), але ви ефективно робите те саме, що створюєте два інакше названих методу «нормальною» мовою. Чи [long] method1(2)інтуїтивніше, ніж long_method1(2)?


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


2
"разом з вимогою, щоб усі функції були чистими і референтно прозорими": Як це полегшує перевантаження типу повернення?
Джорджіо

@Giorgio It doesn’t - Rust не застосовує функцію puritiy, і вона все ще може повернути тип перевантаження (хоча її перевантаження в Rust дуже відрізняється від інших мов (ви можете перевантажуватись лише за допомогою шаблонів))
Idan Arye

Частина [long] та [int] повинна була створити спосіб експліцитного виклику методу; у більшості випадків те, як він повинен бути викликаний, може бути безпосередньо переданий з типу змінної, якій призначено виконання методу.
користувач2638180

0

Він є доступний в Swift і прекрасно працює там. Очевидно, що ви не можете мати неоднозначний тип з обох сторін, тому це потрібно знати зліва.

Я використовував це в простому API кодування / декодування .

public protocol HierDecoder {
  func dech() throws -> String
  func dech() throws -> Int
  func dech() throws -> Bool

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

    private static let typeCode = "ds"
    static func registerFactory() {
        HierCodableFactories.Register(key:typeCode) {
            (from) -> HierCodable in
            return try tgDrawStyle(strokeColor:from.dech(), fillColor:from.dechOpt(), lineWidth:from.dech(), glowWidth: from.dech())
        }
    }
    func typeKey() -> String { return tgDrawStyle.typeCode }
    func encode(to:HierEncoder) {
        to.ench(strokeColor)
        to.enchOpt(fillColor)
        to.ench(lineWidth)
        to.ench(glowWidth)
    }

Якщо ви звернете пильну увагу, ви помітите dechOptдзвінок вище. Я виявив важкий спосіб, що перевантаження того ж імені функції, де диференціатор повертав необов'язковий, було занадто схильним до помилок, оскільки контекст виклику міг ввести очікування, що це необов'язково.


-5
int main() {
    auto var1 = method1(1);
}

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

1
Не сильний аргумент. Наприклад, іржа широко використовує умовиводи типу, а в деяких випадках, особливо з дженериками, не може сказати, який тип потрібен. У таких випадках вам просто потрібно чітко вказати тип, а не покладатися на умовиводи.
8bittree

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