Почему не допускается перегрузка с типами возврата? (по крайней мере на обычно используемых языках)

9

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

Я имею в виду что-то вроде этого:

 int method1 (int num)
 {

 }
 long method1 (int num)
 {

 }

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

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

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

По каким причинам подобное не поддерживается?

user2638180
источник
Возможно, ваш вопрос был бы лучше с примером, где неявные преобразования между двумя типами возвращаемых данных не существуют - например, классы 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 для систем типов Хиндли-Милнера), чтобы определить тип.

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возвращали short или int, а тип был определен как long.
RubberDuck
Это кажется немного лучше.
RubberDuck
Я не покупаю ваши аргументы о том, что вы не можете иметь вывод типа, анонимные подпрограммы или понимание монад в языке с перегрузкой возвращаемого типа. Haskell делает это, и Haskell имеет все три. Он также имеет параметрический полиморфизм. Ваша точка зрения о long/ int/ shortбольше связана со сложностями подтипов и / или неявных преобразований, чем с перегрузкой возвращаемого типа. В конце концов, число литералов будут перегружены на их тип возвращаемого значения в C ++, Java, C♯, и многие другие, и что , кажется, не представляет проблемы. Вы можете просто составить правило: например, выбрать наиболее конкретный / общий тип.
Йорг Миттаг
@ JörgWMittag Моя точка зрения заключалась не в том, что это сделает это невозможным, просто в том, что это делает вещи излишне сложными.
RubberDuck
0

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

В большинстве языков можно ожидать, что выражение method1(2)будет иметь определенный тип и более или менее предсказуемое возвращаемое значение. Но если вы разрешаете перегрузку возвращаемых значений, это означает, что невозможно сказать, что означает это выражение в целом, без учета контекста вокруг него. Подумайте, что происходит, когда у вас есть unsigned long long foo()метод, реализация которого заканчивается return method1(2)? Должно ли это вызывать longперегрузку -retning или перегрузку int-retning или просто вызывать ошибку компилятора?

Кроме того, если вам нужно помочь компилятору, аннотируя тип возвращаемого значения, вы не только придумываете больше синтаксиса (что увеличивает все вышеупомянутые затраты на то, чтобы вообще позволить этой функции существовать), но вы фактически делаете то же самое, что и создание два метода с разными именами в «нормальном» языке. Есть [long] method1(2)более интуитивным , чем long_method1(2)?


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

Ixrec
источник
2
«наряду с требованием, чтобы все функции были чистыми и прозрачными по ссылкам»: как это облегчает перегрузку возвращаемого типа?
Джорджио
@ Джорджио Это не так - Rust не применяет функцию puritiy и все равно может выполнять перегрузку возвращаемого типа (хотя ее перегрузка в Rust сильно отличается от других языков (вы можете перегружать только с помощью шаблонов))
Идан Арье,
Часть [long] и [int] должна была иметь способ явного вызова метода, в большинстве случаев, как он должен быть вызван, может быть напрямую определено из типа переменной, которой назначено выполнение метода.
user2638180
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);
}
DeadMG
источник
В этом случае компилятор может: а) отклонить вызов, потому что он неоднозначен; б) выбрать первый / последний; в) оставить тип var1и перейти к выводу типа; как только какое-то другое выражение определяет тип var1использования, который используется для выбора правильная реализация. Суть в том, что случай, когда вывод типа нетривиален, редко доказывает, что точка, отличная от вывода типа, обычно нетривиальна.
back2dos
1
Не сильный аргумент. Например, в Rust интенсивно используется вывод типов, а в некоторых случаях, особенно в случае дженериков, невозможно определить, какой тип вам нужен. В таких случаях вам просто нужно явно указать тип, а не полагаться на вывод типа.
8bittree
1
Э-э ... это не демонстрирует перегруженный тип возвращаемого значения. method1должен быть объявлен для возврата определенного типа.
Gort the Robot