Dlaczego niedozwolone jest przeciążanie typami zwrotów? (przynajmniej w zwykle używanych językach)


9

Nie znam wszystkich języków programowania, ale jasne jest, że zwykle nie jest możliwe przeładowanie metody, biorąc pod uwagę jej typ zwracany (zakładając, że jej argumenty mają tę samą liczbę i ten sam typ).

Mam na myśli coś takiego:

 int method1 (int num)
 {

 }
 long method1 (int num)
 {

 }

To nie jest tak, że to duży problem z programowaniem, ale przy niektórych okazjach byłbym z tego zadowolony.

Oczywiście nie byłoby sposobu, aby te języki mogły to obsługiwać bez sposobu na odróżnienie wywoływanej metody, ale składnia tego może być tak prosta, jak coś takiego jak [int] method1 (num) lub [long] method1 (num) w ten sposób kompilator będzie wiedział, który będzie nazywany.

Nie wiem, jak działają kompilatory, ale nie wydaje się to takie trudne, więc zastanawiam się, dlaczego coś takiego zwykle nie jest wdrażane.

Jakie są powody, dla których coś takiego nie jest obsługiwane?


Być może twoje pytanie byłoby lepsze na przykładzie, w którym nie istnieje niejawna konwersja między dwoma typami zwracanymi - np. Klasy Fooi Bar.

Dlaczego taka funkcja byłaby przydatna?
James Youngman

3
@JamesYoungman: Na przykład, aby parsować ciągi znaków na różne typy, możesz mieć metodę int read (String), float read (String s) i tak dalej. Każda przeciążona odmiana metody wykonuje analizę dla odpowiedniego typu.
Giorgio

Nawiasem mówiąc, to tylko problem ze statycznie wpisywanymi językami. Posiadanie wielu typów zwrotów jest dość rutyną w dynamicznie wpisywanych językach, takich jak Javascript lub Python.
Gort the Robot

1
@StevenBurnap, um, nie. Dzięki np. JavaScript nie można w ogóle przeciążać funkcji. W rzeczywistości jest to tylko problem z językami, które obsługują przeciążanie nazw funkcji.
David Arno

Odpowiedzi:


19

Utrudnia to sprawdzanie typu.

Gdy zezwalasz na przeciążanie tylko na podstawie typów argumentów i zezwalasz tylko na dedukowanie typów zmiennych z ich inicjatorów, wszystkie informacje o typie płyną w jednym kierunku: w górę drzewa składniowego.

var x = f();

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

Jeśli zezwalasz na przesyłanie informacji o typie w obu kierunkach, na przykład poprzez dedukcję typu zmiennej na podstawie jej użycia, potrzebujesz solvera ograniczającego (takiego jak Algorytm W dla systemów typu Hindley – Milner), aby określić typ.

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]

Tutaj musieliśmy pozostawić xtyp zmiennej typu nierozwiązanego ∃T, a wszystko, co wiemy o tym, to to, że jest on analizowalny. Dopiero później, gdy xzostanie użyty w konkretnym typie, mamy wystarczającą ilość informacji, aby rozwiązać ograniczenie i określić to ∃T = int, co propaguje informacje o typie w dół drzewa składni od wyrażenia wywołania do x.

Jeśli nie uda nam się określić typu x, albo ten kod również zostanie przeciążony (tak więc wywołujący określi typ), albo będziemy musieli zgłosić błąd dotyczący niejednoznaczności.

Na tej podstawie projektant języka może stwierdzić:

  • Dodaje złożoności implementacji.

  • Powoduje spowolnienie sprawdzania typu - w przypadkach patologicznych wykładniczo.

  • Trudniej jest wytwarzać dobre komunikaty o błędach.

  • To zbyt różni się od status quo.

  • Nie mam ochoty go wdrażać.


1
Ponadto: Trudno będzie zrozumieć, że w niektórych przypadkach kompilator dokona wyborów, których programista absolutnie się nie spodziewał, co doprowadzi do trudności w znalezieniu błędów.
gnasher729

1
@ gnasher729: Nie zgadzam się. Regularnie używam Haskell, który ma tę funkcję, i nigdy nie byłem ugryziony przez wybór przeciążenia (mianowicie instancji typu typeclass). Jeśli coś jest niejednoznaczne, to po prostu zmusza mnie do dodania adnotacji typu. Nadal zdecydowałem się na wdrożenie pełnego wnioskowania na temat typu w moim języku, ponieważ jest to niezwykle przydatne. Ta odpowiedź była dla mnie adwokatem diabła.
Jon Purdy

4

Ponieważ to niejednoznaczne. Używając C # jako przykładu:

var foo = method(42);

Jakiego przeciążenia powinniśmy użyć?

Ok, może to było trochę niesprawiedliwe. Kompilator nie może ustalić, jakiego typu użyć, jeśli nie powiemy tego w twoim hipotetycznym języku. Tak więc niejawne pisanie jest niemożliwe w twoim języku i są tam anonimowe metody i Linq ...

Co powiesz na ten? (Nieznacznie przedefiniowano podpisy, aby zilustrować ten punkt).

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

....

long foo = method(42);

Czy powinniśmy użyć intprzeciążenia, czy shortprzeciążenia? Po prostu nie wiemy, będziemy musieli to określić przy pomocy twojej [int] method1(num)składni. Co jest trochę kłopotliwe, aby parsować i pisać szczerze.

long foo = [int] method(42);

Chodzi o to, że jest zaskakująco podobna składnia do ogólnej metody w C #.

long foo = method<int>(42);

(C ++ i Java mają podobne funkcje).

Krótko mówiąc, projektanci języków postanowili rozwiązać problem w inny sposób, aby uprościć analizę i włączyć znacznie bardziej zaawansowane funkcje językowe.

Mówisz, że niewiele wiesz o kompilatorach. Gorąco polecam naukę gramatyki i parserów. Kiedy zrozumiesz, czym jest gramatyka bezkontekstowa, będziesz miał o wiele lepsze pojęcie o tym, dlaczego dwuznaczność jest złą rzeczą.


Dobra uwaga na temat leków generycznych, choć wydaje się, że jesteś złą shorti int.
Robert Harvey

Tak @RobertHarvey masz rację. Próbowałem zilustrować ten przykład. Lepiej by działało, gdyby methodzwróciło skrót lub int, a typ zdefiniowano jako długi.
RubberDuck

To wydaje się trochę lepsze.
RubberDuck

Nie kupuję twoich argumentów, że nie możesz mieć wnioskowania o typie, anonimowych podprogramów ani interpretacji monad w języku z przeciążeniem typu zwracanego. Haskell to robi, a Haskell ma wszystkie trzy. Ma również polimorfizm parametryczny. Twoja uwaga na temat long/ int/ shortdotyczy bardziej złożoności subtypingu i / lub niejawnych konwersji niż przeciążenia typu zwrotu. W końcu literały liczbowe przeciążone na typie zwracanym w C ++, Java, C♯ i wielu innych, i to nie wydaje się stanowić problemu. Możesz po prostu stworzyć regułę: np. Wybrać najbardziej konkretny / ogólny typ.
Jörg W Mittag

@ JörgWMittag nie miałem na myśli tego, że to uniemożliwiłoby to, tylko że sprawi, że sprawy będą niepotrzebnie skomplikowane.
RubberDuck

0

Wszystkie funkcje językowe zwiększają złożoność, więc muszą zapewniać wystarczającą korzyść, aby uzasadnić nieuniknione problemy, przypadki narożne i zamieszanie użytkownika, które powoduje każda funkcja. W przypadku większości języków ten po prostu nie zapewnia wystarczających korzyści, aby to uzasadnić.

W większości języków można oczekiwać, że wyrażenie method1(2)będzie miało określony typ i mniej lub bardziej przewidywalną wartość zwracaną. Ale jeśli zezwolisz na przeciążanie zwracanych wartości, oznacza to, że nie można powiedzieć, co to wyrażenie ogólnie oznacza, bez uwzględnienia otaczającego go kontekstu. Zastanów się, co się dzieje, gdy masz unsigned long long foo()metodę, której implementacja kończy się return method1(2)? Czy powinno to wywołać longprzeciążenie intpowrotu lub przeciążenie powrotu, czy po prostu dać błąd kompilatora?

Ponadto, jeśli musisz pomóc kompilatorowi, dodając adnotację typu zwracanego, nie tylko wymyślasz więcej składni (co podnosi wszystkie wyżej wymienione koszty związane z dopuszczeniem funkcji w ogóle), ale skutecznie robisz to samo, co tworzenie dwie metody o różnych nazwach w „normalnym” języku. Czy jest [long] method1(2)bardziej intuicyjny niż long_method1(2)?


Z drugiej strony niektóre języki funkcjonalne, takie jak Haskell z bardzo silnymi statycznymi systemami typów, pozwalają na tego rodzaju zachowanie, ponieważ ich wnioskowanie o typach jest na tyle silne, że rzadko trzeba opisywać typ zwracany w tych językach. Ale jest to możliwe tylko dlatego, że języki te naprawdę egzekwują bezpieczeństwo tekstu, bardziej niż jakikolwiek tradycyjny język, a także wymagają, aby wszystkie funkcje były czyste i referencyjnie przejrzyste. To nie jest coś, co kiedykolwiek byłoby wykonalne w większości języków OOP.


2
„wraz z wymaganiem, aby wszystkie funkcje były czyste i referencyjnie przejrzyste”: Jak to ułatwia przeciążenie typu powrotu?
Giorgio

@Giorgio Nie robi tego - Rust nie wymusza czyszczenia funkcji i nadal może powodować przeciążenie typu return (chociaż jej przeciążenie w Rust jest inne niż w innych językach (można przeciążać tylko przy użyciu szablonów))
Idan Arye

Część [long] i [int] miała mieć sposób na jawne wywołanie metody, w większości przypadków sposób jej wywołania można było bezpośrednio określić na podstawie typu zmiennej, do której przypisane jest wykonanie metody.
user2638180

0

To jest dostępny w Swift i działa dobrze tam. Oczywiście nie możesz mieć dwuznacznego typu po obu stronach, więc musisz wiedzieć po lewej.

Użyłem tego w prostym API do kodowania / dekodowania .

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

Oznacza to, że wywołania, w których znane są typy parametrów, takie jak initobiekt, działają bardzo prosto:

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

Jeśli zwracasz szczególną uwagę, zauważysz dechOptpołączenie powyżej. Dowiedziałem się na własnej skórze, że przeciążenie tej samej nazwy funkcji, w której wyróżnik zwraca wartość opcjonalną, jest zbyt podatne na błędy, ponieważ kontekst wywołania może wprowadzić oczekiwanie, że jest ona opcjonalna.


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

W takim przypadku kompilator może a) odrzucić wywołanie, ponieważ jest ono dwuznaczne b) wybrać pierwszy / ostatni jeden c) pozostawić typ var1i kontynuować wnioskowanie o typie, a gdy tylko inne wyrażenie określi typ var1użycia do wyboru właściwe wdrożenie. W dolnej linii pokazanie przypadku, w którym wnioskowanie o typie nie jest trywialne, rzadko dowodzi, że punkt inny niż wnioskowanie o typie nie jest ogólnie trywialne.
back2dos 28.04.16

1
Niezbyt silny argument. Na przykład Rust często korzysta z wnioskowania o typie, aw niektórych przypadkach, szczególnie w przypadku generycznych, nie jest w stanie powiedzieć, jakiego typu potrzebujesz. W takich przypadkach musisz po prostu podać typ, zamiast polegać na wnioskowaniu o typie.
8bittree

1
Uh ... to nie pokazuje przeciążonego typu zwrotu. method1należy zadeklarować, aby zwrócić określony typ.
Gort the Robot
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.