Czy metoda przeciążania jest czymś więcej niż cukrem syntaktycznym? [Zamknięte]


19

Czy metoda przeciążająca jest rodzajem polimorfizmu? Wydaje mi się, że to po prostu różnicowanie metod o tej samej nazwie i różnych parametrach. Tak stuff(Thing t)i stuff(Thing t, int n)są całkowicie różne metody miarę kompilator i runtime są zainteresowane.

Stwarza iluzję po stronie dzwoniącego, że jest to ta sama metoda, która działa inaczej na różne rodzaje obiektów - polimorfizm. Ale to tylko złudzenie, bo w rzeczywistości stuff(Thing t)i stuff(Thing t, int n)są całkowicie różne metody.

Czy metoda przeciążania jest czymś więcej niż cukrem syntaktycznym? Czy coś brakuje?


Powszechną definicją cukru syntaktycznego jest to, że jest on wyłącznie lokalny . Oznacza to, że zmiana fragmentu kodu na jego „słodzony” odpowiednik lub odwrotnie, wiąże się z lokalnymi zmianami, które nie wpływają na ogólną strukturę programu. I myślę, że przeciążanie metod dokładnie pasuje do tego kryterium. Spójrzmy na przykład pokazujący:

Rozważ klasę:

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

Teraz rozważ inną klasę, która korzysta z tej klasy:

/* 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
    }
}

W porządku. Zobaczmy teraz, co należy zmienić, jeśli zastąpimy przeciążenie metody zwykłymi metodami:

Te readmetody Readerzmiany readBook(Book)i readFile(file). Tylko kwestia zmiany ich nazw.

Kod wywołujący FileProcessorzmienia się nieznacznie: reader.read(file)zmienia się na reader.readFile(file).

I to wszystko.

Jak widać, różnica między używaniem przeciążenia metody a nieużywaniem jej jest czysto lokalna . I dlatego uważam, że kwalifikuje się jako czysty cukier syntaktyczny.

Chciałbym usłyszeć twoje zastrzeżenia, jeśli masz jakieś, może coś mi umknęło.


48
W końcu każda funkcja języka programowania to po prostu cukier składniowy dla surowego asemblera.
Philipp

31
@Philipp: Przepraszam, ale to naprawdę głupie stwierdzenie. Języki programowania czerpią swoją użyteczność z semantyki, a nie ze składni. Funkcje takie jak system typów dają rzeczywiste gwarancje, nawet jeśli mogą wymagać od ciebie napisania więcej .
back2dos

3
Zadaj sobie to pytanie: czy przeciążanie operatora to tylko cukier syntaktyczny? Jakakolwiek odpowiedź na to pytanie, którą
uznajesz za

5
@ back2dos: Całkowicie się z tobą zgadzam. Zbyt często czytam zdanie „wszystko to tylko cukier syntaktyczny dla asemblera” i jest to oczywiście błąd. Cukier syntaktyczny jest alternatywną (prawdopodobnie ładniejszą) składnią dla niektórych istniejących składni, która nie dodaje żadnej nowej semantyki.
Giorgio

6
@Giorgio: racja! Dokładna definicja przełomowej pracy Matthiasa Felleisena zawiera ekspresję. Zasadniczo: cukier składniowy ma charakter wyłącznie lokalny. Jeśli musisz zmienić globalną strukturę programu, aby usunąć użycie funkcji języka, to nie jest to cukier składniowy. Tzn. Przepisywanie polimorficznego kodu OO w asemblerze zazwyczaj wymaga dodania globalnej logiki wysyłki, która nie jest czysto lokalna, dlatego OO nie jest „tylko syntaktycznym cukrem dla asemblera”.
Jörg W Mittag,

Odpowiedzi:


29

Aby odpowiedzieć na to pytanie, najpierw potrzebujesz definicji „cukru syntaktycznego”. Pójdę z Wikipedią :

W informatyce cukier syntaktyczny jest składnią w języku programowania, który ma na celu ułatwienie czytania lub wyrażania. Dzięki temu język jest „słodszy” do użytku przez ludzi: rzeczy mogą być wyrażone jaśniej, bardziej zwięźle lub w alternatywnym stylu, który niektórzy mogą preferować.

[...]

Konkretnie, konstrukt w języku nazywa się cukrem syntaktycznym, jeśli można go usunąć z języka bez żadnego wpływu na jego możliwości

Tak więc, zgodnie z tą definicją, funkcje takie jak varargs Javy lub zrozumienie Scali są cukrem syntaktycznym: tłumaczą się na podstawowe funkcje językowe (w pierwszym przypadku tablica, w drugim wywołania map / flatmap / filter), a ich usunięcie spowodowałoby nie zmieniaj rzeczy, które możesz zrobić z językiem.

Jednak przeciążenie metodami nie jest cukrem składniowym w tej definicji, ponieważ usunięcie go zasadniczo zmieniłoby język (nie byłoby już możliwe wysyłanie odrębnych zachowań opartych na argumentach).

To prawda, że ​​możesz symulować przeciążenie metody, o ile masz jakiś sposób na uzyskanie dostępu do argumentów metody, i możesz użyć konstrukcji „jeśli” na podstawie podanych argumentów. Ale jeśli weźmiesz pod uwagę cukier syntaktyczny, będziesz musiał rozważyć wszystko nad maszyną Turinga, aby podobnie był cukrem syntaktycznym.


22
Usunięcie przeciążenia nie zmieniłoby możliwości języka. Nadal możesz robić dokładnie te same rzeczy, co wcześniej; wystarczy zmienić nazwę kilku metod. To bardziej trywialna zmiana niż usuwanie pętli.
Doval

9
Jak powiedziałem, można przyjąć podejście, zgodnie z którym wszystkie języki (w tym języki maszynowe) to po prostu cukier składniowy na maszynie Turinga.
kdgregory,

9
Jak widzę, przeciążanie metod po prostu pozwala sum(numbersArray)i sum(numbersList)zamiast sumArray(numbersArray)i sumList(numbersList). Zgadzam się z Doval, wygląda na zwykły cukier syntetyczny.
Aviv Cohn,

3
Większość języka. Spróbuj wykonawczych instanceof, klas, dziedziczenie, interfejsy, rodzajowych, odbicie lub specyfikatory dostępu używane if, whilei operatorów logicznych, z dokładnie tych samych semantyki . Brak skrzynek narożnych. Pamiętaj, że nie wymagam od ciebie obliczania tych samych rzeczy, co określone zastosowania tych konstrukcji. Wiem już , że możesz obliczyć wszystko przy użyciu logiki logicznej i rozgałęziania / zapętlania. Proszę o zaimplementowanie doskonałych kopii semantyki tych funkcji językowych, w tym wszelkich statycznych gwarancji, które zapewniają (kontrole czasu kompilacji muszą być nadal wykonywane w czasie kompilacji.)
Doval

6
@Doval, kdgregory: Aby zdefiniować cukier składniowy, musisz zdefiniować go w odniesieniu do niektórych semantyków. Jeśli jedyną semantyką jest „Co oblicza ten program?”, To jasne jest, że wszystko jest tylko cukrem syntaktycznym dla maszyny Turinga. Z drugiej strony, jeśli masz semantykę, w której możesz mówić o obiektach i niektórych operacjach na nich, to usunięcie pewnej składni nie pozwoli ci już wyrazić tych operacji, nawet jeśli język nadal może być kompletny w Turingu.
Giorgio

13

Termin cukier syntaktyczny zazwyczaj odnosi się do przypadków, w których cecha jest zdefiniowana przez podstawienie. Język nie określa, co robi funkcja, ale określa, że ​​jest dokładnie równoważny z czymś innym. Na przykład dla każdej pętli

for(Object alpha: alphas) {
}

Staje się:

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

Lub weź funkcję ze zmiennymi argumentami:

void foo(int... args);

foo(3, 4, 5);

Który staje się:

void Foo(int[] args);

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

Istnieje więc trywialne podstawienie składni w celu zaimplementowania tej funkcji w odniesieniu do innych funkcji.

Spójrzmy na przeciążenie metody.

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

foo(4.5);

Można to przepisać jako:

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

foo_double(4.5);

Ale to nie jest równoważne. W modelu Java jest to coś innego. foo(int a)nie implementuje foo_intfunkcji, która ma zostać utworzona. Java nie implementuje przeciążania metod, nadając dwuznacznym funkcjom śmieszne nazwy. Aby liczyć się jako cukier składniowy, Java musiałaby udawać, że naprawdę napisałeś foo_inti foo_doubledziała, ale tak nie jest.


2
Nie sądzę, żeby ktokolwiek powiedział, że transformacja cukru składniowego musi być trywialna. Nawet jeśli tak, uważam to twierdzenie za But, the transformation isn't trivial. At the least, you have to determine the types of the parameters.bardzo szkicowe, ponieważ nie trzeba określać typów ; są znane w czasie kompilacji.
Doval

3
„Aby liczyć się jako cukier składniowy, Java musiałaby udawać, że naprawdę napisałeś funkcje foo_int i foo_double, ale tak nie jest.” - o ile mówimy o metodach przeciążania, a nie o polimorfizmach, jaka byłaby różnica między foo(int)/ foo(double)i foo_int/ foo_double? Nie znam się zbyt dobrze na Javie, ale wyobrażam sobie, że taka zmiana nazwy tak naprawdę dzieje się w JVM (cóż - prawdopodobnie foo(args)raczej wtedy foo_args- robi to przynajmniej w C ++ z manglingiem symboli (ok - mangling symboli jest technicznie szczegółem implementacji, a nie częścią) języka)
Maciej Piechotka,

2
@Doval: „Nie sądzę, żeby ktokolwiek powiedział, że transformacja cukru składniowego musi być trywialna”. - To prawda, ale musi być lokalny . Jedyna przydatna definicja cukru syntaktycznego, którą znam, pochodzi ze słynnego artykułu Matthiasa Felleisena na temat ekspresji językowej i zasadniczo mówi, że jeśli możesz ponownie napisać program napisany w języku L + y (tj. W języku L z pewną cechą y ) w język L (tj. podzbiór tego języka bez cechy y ) bez zmiany globalnej struktury programu (tj. tylko wprowadzanie lokalnych zmian), wtedy y jest cukrem syntaktycznym w L + y i robi
Jörg W Mittag,

2
… Nie zwiększaj ekspresji L. Jednakże, jeśli nie może tego zrobić, to znaczy, jeśli trzeba dokonać zmian w globalnej strukturze programu, to nie cukier syntaktyczny i robi się fakt make L + y bardziej wyraziste niż L . Na przykład Java bez rozszerzonej forpętli nie jest bardziej ekspresyjna niż Java bez niej. (Jest ładniejszy, bardziej zwięzły, bardziej czytelny i ogólnie lepszy, twierdziłbym, ale nie bardziej wyrazisty.) Jednak nie jestem pewien co do przypadku przeładowania. Pewnie będę musiał ponownie przeczytać artykuł. Moje jelita mówią, że to cukier składniowy, ale nie jestem pewien.
Jörg W Mittag,

2
@MaciejPiechotka, gdyby częścią definicji języka była zmiana nazwy funkcji i można było uzyskać dostęp do funkcji pod tymi nazwami, myślę, że byłby to cukier składniowy. Ale ponieważ jest ukryty jako szczegół implementacji, myślę, że dyskwalifikuje go od bycia cukrem syntaktycznym.
Winston Ewert,

8

Biorąc pod uwagę, że zmiana nazwy działa, czy nie musi to być jedynie cukier składniowy?

Pozwala to dzwoniącemu wyobrazić sobie, że wywołuje tę samą funkcję, kiedy tak nie jest. Ale znał prawdziwe nazwiska wszystkich swoich funkcji. Tylko jeśli możliwe byłoby osiągnięcie opóźnionego polimorfizmu poprzez przekazanie zmiennej bez typu do funkcji typowanej i ustalenie jej typu, aby wywołanie mogło przejść do właściwej wersji według nazwy, byłoby to prawdziwą cechą językową.

Niestety, nigdy nie widziałem, żeby język to robił. Kiedy występuje dwuznaczność, te kompilatory nie rozwiązują tego, nalegają, aby pisarz go rozwiązał.


Funkcja, której szukasz, nosi nazwę „Wysyłka wielokrotna”. Obsługuje go wiele języków, w tym Haskell, Scala i (od 4.0) C #.
Iain Galloway

Chciałbym oddzielić parametry na klasach od przeciążenia metody prostej. W przypadku przeciążenia metodą prostą programista zapisuje wszystkie wersje, kompilator po prostu wie, jak wybrać jedną. To tylko cukier syntaktyczny, który można rozwiązać poprzez proste przekłamywanie nazwy, nawet w przypadku wielu wysyłek. --- W obecności parametrów na klasach kompilator generuje kod w razie potrzeby, co całkowicie go zmienia.
Jon Jay Obermark

2
Myślę, że źle zrozumiałeś. Na przykład w języku C #, jeśli jednym z parametrów metody jest, dynamicwówczas rozwiązanie przeciążenia występuje w czasie wykonywania, a nie w czasie kompilacji . Taka jest wielokrotna wysyłka i nie można jej replikować przez zmianę nazw funkcji.
Iain Galloway,

Całkiem fajne. Nadal mogę jednak testować zmienny typ, więc jest to nadal tylko wbudowana funkcja nakładana na cukier syntaktyczny. Jest to funkcja językowa, ale tylko z trudem.
Jon Jay Obermark

7

W zależności od języka jest to cukier składniowy lub nie.

Na przykład w C ++ możesz robić rzeczy przy użyciu przeciążenia i szablonów, co nie byłoby możliwe bez komplikacji (ręcznie wpisz wszystkie wystąpienia szablonu lub dodaj wiele parametrów szablonu).

Zauważ, że dynamiczna wysyłka jest formą przeciążania, dynamicznie rozwiązywaną dla niektórych parametrów (dla niektórych języków tylko jeden specjalny, to , ale nie wszystkie języki są tak ograniczone), i nie nazwałbym tego rodzaju przeciążeniem cukrem składniowym.


Nie jestem pewien, jak inne odpowiedzi radzą sobie o wiele lepiej, gdy są zasadniczo niepoprawne.
Telastyn

5

W przypadku współczesnych języków jest to po prostu cukier składniowy; w sposób całkowicie niezależny od języka, to coś więcej.

Wcześniej ta odpowiedź mówiła po prostu, że to coś więcej niż cukier syntaktyczny, ale jeśli zauważysz w komentarzach, Falco podniósł kwestię, że istnieje jedna część układanki, której brakuje współczesnym językom; nie łączą przeciążenia metody z dynamicznym określaniem, którą funkcję wywołać w tym samym kroku. Wyjaśnimy to później.

Oto dlaczego powinno być więcej.

Rozważ język, który obsługuje zarówno przeciążanie metod, jak i zmienne bez typów. Możesz mieć następujące prototypy metody:

bool someFunction(int arg);

bool someFunction(string arg);

W niektórych językach prawdopodobnie będziesz skłonny wiedzieć w czasie kompilacji, który z nich zostałby wywołany przez dany wiersz kodu. Ale w niektórych językach nie wszystkie zmienne są wpisywane (lub wszystkie są domyślnie wpisywane jako Objectlub cokolwiek innego), więc wyobraź sobie zbudowanie słownika, którego klucze odwzorowują wartości różnych typów:

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.

A co teraz, jeśli chcesz złożyć podanie someFunctionna jeden z tych numerów pokoi? Nazywasz to:

someFunction(roomNumber[someSortOfKey]);

Jest someFunction(int)nazywany lub jest someFunction(string)nazywany? Oto jeden przykład, w którym nie są to całkowicie ortogonalne metody, szczególnie w językach wyższego poziomu. Język musi ustalić - w czasie wykonywania - który z nich wywołać, więc nadal musi traktować je jako przynajmniej w pewnym stopniu tę samą metodę.

Dlaczego nie skorzystać z szablonów? Dlaczego nie użyć po prostu nietypowego argumentu?

Elastyczność i dokładniejsza kontrola. Czasem użycie szablonów / nietypowych argumentów jest lepszym podejściem, ale czasem nie.

Musisz pomyśleć o przypadkach, w których na przykład możesz mieć dwie sygnatury metod, z których każda przyjmuje argumenty inti stringjako argument, ale gdzie kolejność jest inna w każdej sygnaturze. Być może masz dobry powód, aby to zrobić, ponieważ implementacja każdego podpisu może w dużej mierze zrobić to samo, ale z nieco innym zwrotem; rejestrowanie może być na przykład inne. Lub nawet jeśli robią to samo dokładnie, możesz być w stanie automatycznie zebrać pewne informacje tylko z kolejności, w której argumenty zostały określone. Technicznie rzecz biorąc, możesz po prostu użyć instrukcji pseudo-switch, aby określić typ każdego z przekazywanych argumentów, ale robi się to bałagan.

Czy ten kolejny przykład jest złą praktyką programistyczną?

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

Tak, ogólnie rzecz biorąc. W tym konkretnym przykładzie może powstrzymać kogoś od prób zastosowania tego do pewnych prymitywnych typów i odzyskania nieoczekiwanego zachowania (co może być dobrą rzeczą); ale załóżmy, że skróciłem powyższy kod i że w rzeczywistości masz przeciążenia dla wszystkich typów pierwotnych, a także dla Objects. Zatem ten następny fragment kodu jest naprawdę bardziej odpowiedni:

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

Ale co, jeśli musisz użyć tego tylko dla ints i strings, a co, jeśli chcesz, aby zwrócił prawdę na podstawie odpowiednio prostszych lub bardziej skomplikowanych warunków? Masz dobry powód, aby użyć przeciążenia:

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

Ale hej, dlaczego nie nadać tym funkcjom dwóch różnych nazw? Nadal masz taką samą drobnoziarnistą kontrolę, prawda?

Ponieważ, jak wspomniano wcześniej, niektóre hotele używają liczb, niektóre używają liter, a niektóre używają kombinacji cyfr i liter:

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

To wciąż nie jest dokładnie ten sam kod, którego używałbym w prawdziwym życiu, ale powinien on ilustrować punkt, który robię dobrze.

Ale ... Oto dlaczego we współczesnych językach jest to cukier syntaktyczny.

Falco podniósł w komentarzach, że obecne języki w zasadzie nie łączą przeciążenia metod i dynamicznego wyboru funkcji w tym samym kroku. Sposób, w jaki wcześniej rozumiałem niektóre języki, był taki, że można przeciążać appearsToBeFirstFloorw powyższym przykładzie, a następnie język określałby w czasie wykonywania, która wersja funkcji ma zostać wywołana, w zależności od wartości środowiska uruchomieniowego zmiennej bez typu. To zamieszanie częściowo wynikało z pracy z językami podobnymi do ECMA, takimi jak ActionScript 3.0, w których można z łatwością losować, która funkcja jest wywoływana w określonym wierszu kodu w czasie wykonywania.

Jak zapewne wiesz, ActionScript 3 nie obsługuje przeciążania metod. Jeśli chodzi o VB.NET, możesz deklarować i ustawiać zmienne bez jawnego przypisywania typu, ale kiedy próbujesz przekazać te zmienne jako argumenty do przeciążonych metod, nadal nie chce odczytać wartości środowiska wykonawczego w celu ustalenia, którą metodę wywołać; zamiast tego chce znaleźć metodę z argumentami typu Objectlub bez typu albo coś podobnego. Więc w intporównaniu stringpowyższym przykładzie nie będzie działać w tym języku, albo. C ++ ma podobne problemy, ponieważ gdy używasz czegoś takiego jak wskaźnik pustki lub jakiś inny podobny mechanizm, nadal wymaga ręcznego rozróżnienia typu w czasie kompilacji.

Tak jak mówi pierwszy nagłówek ...

W przypadku współczesnych języków jest to po prostu cukier składniowy; w sposób całkowicie niezależny od języka, to coś więcej. Zwiększenie użyteczności i przydatności przeciążania metod, tak jak w powyższym przykładzie, może być dobrą cechą do dodania do istniejącego języka (jak powszechnie domagano się domyślnie w AS3) lub może też służyć jako jeden z wielu różnych fundamentalnych filarów stworzenie nowego języka proceduralnego / obiektowego.


3
Czy potrafisz wymienić języki, które naprawdę obsługują funkcję Dispatch w czasie wykonywania, a nie czas kompilacji? WSZYSTKIE języki, które znam, wymagają pewności w czasie kompilacji, która funkcja nazywa się ...
Falco

@Falco ActionScript 3.0 obsługuje go w czasie wykonywania. Można na przykład użyć funkcji, która zwraca jedną z trzech ciągów losowo, a następnie użyć jej wartości zwracanej zadzwonić jeden z trzech funkcji losowo: this[chooseFunctionNameAtRandom](); Jeśli chooseFunctionNameAtRandom()powraca albo "punch", "kick"lub "dodge", po czym można thusly wdrożyć bardzo prosty losowy element, na przykład, AI przeciwnika w grze Flash.
Panzercrisis

1
Tak - ale oba są prawdziwymi metodami semantycznymi, aby uzyskać dynamiczne wysyłanie funkcji, Java też je ma. Różnią się one jednak od przeciążenia, przeciążenie jest statyczne i po prostu cukrem składniowym, podczas gdy dynamiczna wysyłka i dziedziczenie to cechy prawdziwego języka, które oferują nową funkcjonalność!
Falco

1
... Próbowałem także void pointers w C ++, a także wskaźników klasy bazowej, ale kompilator chciał, żebym sam ujednoznacznił go przed przekazaniem go do funkcji. Zastanawiam się więc, czy usunąć tę odpowiedź. Zaczyna wyglądać, jakby języki prawie zawsze zaczynały od połączenia dynamicznego wyboru funkcji z przeciążeniem funkcji w tej samej instrukcji lub instrukcji, ale potem odejdź w ostatniej sekundzie. Byłaby to jednak fajna funkcja językowa; może ktoś musi stworzyć taki język.
Panzercrisis

1
Niech odpowiedź pozostanie, może pomyśl o uwzględnieniu niektórych twoich badań na podstawie komentarzy w odpowiedzi?
Falco

2

To naprawdę zależy od twojej definicji „cukru syntaktycznego”. Spróbuję odnieść się do niektórych definicji, które przychodzą mi do głowy:

  1. Cechą jest cukier składniowy, gdy program, który go używa, zawsze może zostać przetłumaczony na inny, który nie korzysta z tej funkcji.

    Zakładamy tutaj, że istnieje prymitywny zestaw funkcji, których nie można przetłumaczyć: innymi słowy, nie ma pętli w rodzaju „można zastąpić cechę X za pomocą cechy Y” i „można zastąpić cechę Y funkcją X”. Jeśli jedna z tych dwóch prawd jest prawdą, to drugą z tych cech można wyrazić w kategoriach cech, które nie są pierwsze lub jest to cecha pierwotna.

  2. Taki sam jak definicja 1, ale z dodatkowym wymogiem, że przetłumaczony program jest tak samo bezpieczny dla typu jak pierwszy, tzn. Po wykasowaniu nie tracisz żadnych informacji.

  3. Definicja OP: cechą jest cukier składniowy, jeśli jego tłumaczenie nie zmienia struktury programu, a wymaga jedynie „lokalnych zmian”.

Weźmy Haskell za przykład przeciążenia. Haskell zapewnia zdefiniowane przez użytkownika przeciążenie poprzez klasy typów. Na przykład operacje +i *są zdefiniowane w Numklasie typu i można użyć dowolnego typu, który ma (kompletną) instancję takiej klasy +. Na przykład:

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

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

Jedną z dobrze znanych rzeczy na temat klas typów Haskella jest to cech że można się ich pozbyć . To znaczy możesz przetłumaczyć dowolny program, który używa klas typów w równoważnym programie, który ich nie używa.

Tłumaczenie jest dość proste:

  • Biorąc pod uwagę definicję klasy:

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

    Możesz to przetłumaczyć na algebraiczny typ danych:

    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
    }
    

    Tutaj X_P_ii X_op_iselektory . Czyli podana wartość typu X azastosowana X_P_1do wartości zwróci wartość zapisaną w tym polu, więc są to funkcje typuX a -> P_i a (lub X a -> t_i).

    W przypadku bardzo szorstkiej anologii można by pomyśleć o wartościach typu X ajako o structs, a następnie xo typie X awyrażenia:

    X_P_1 x
    X_op_1 x
    

    może być postrzegane jako:

    x.X_P_1
    x.X_op_1
    

    (Łatwo jest używać tylko pól pozycyjnych zamiast nazwanych, ale nazwane pola są łatwiejsze do obsługi w przykładach i unikają kodu z tablicy kotła).

  • Biorąc pod uwagę deklarację instancji:

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

    Możesz przetłumaczyć go na funkcję, która podając słowniki dla C_1 a_1, ..., C_n a_nklas zwraca wartość słownika (tj. Wartość typu X a) dla typuT a_1 ... a_n .

    Innymi słowy powyższą instancję można przetłumaczyć na funkcję taką jak:

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

    (Uwaga, że nmoże być0 ).

    I faktycznie możemy to zdefiniować jako:

    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 = ...
    

    gdzie op_1 = ...się op_m = ...są definicje znaleźć w instancezgłoszeniu, oraz get_P_i_Tto funkcje określone przez P_iwystąpienie Ttypu (to musi istnieć z powodu P_iS są superklasy z X).

  • Otrzymano wywołanie funkcji przeciążonej:

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

    Możemy jawnie przekazać słowniki związane z ograniczeniami klasowymi i uzyskać równoważne wywołanie:

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

    Zauważ, że ograniczenia klasowe stały się po prostu nowym argumentem. W +przetłumaczonym programie jest selektor, jak wyjaśniono wcześniej. Innymi słowy, przetłumaczona addfunkcja, biorąc pod uwagę słownik typu argumentu, najpierw „rozpakuje” rzeczywistą funkcję do obliczenia wyniku, (+) dictNuma następnie zastosuje tę funkcję do argumentów.

To tylko bardzo szybki szkic na temat całej sprawy. Jeśli jesteś zainteresowany, powinieneś przeczytać artykuły Simona Peytona Jonesa i in.

Uważam, że podobne podejście można zastosować również do przeciążania w innych językach.

Pokazuje to jednak, że jeśli twoja definicja cukru syntaktycznego to (1), to przeciążenie to cukier syntaktyczny . Ponieważ możesz się go pozbyć.

Jednak przetłumaczony program traci informacje o oryginalnym programie. Na przykład nie wymusza istnienia instancji klas nadrzędnych. (Mimo że operacje wyodrębnienia słowników rodzica muszą być nadal tego typu, możesz przekazać undefinedlub inne wartości polimorficzne, abyś mógł zbudować wartość X ybez budowania wartości dla P_i y, więc tłumaczenie nie straci wszystkiego rodzaj bezpieczeństwa). Dlatego nie jest to cukier syntacti według (2)

Co do (3). Nie wiem, czy odpowiedź powinna brzmieć „tak” czy „nie”.

Powiedziałbym „nie”, ponieważ na przykład deklaracja instancji staje się definicją funkcji. Funkcje, które są przeciążone, otrzymują nowy parametr (co oznacza, że ​​zmienia zarówno definicję, jak i wszystkie wywołania).

Powiedziałbym „tak”, ponieważ dwa programy wciąż mapują jeden na jeden, więc „struktura” nie uległa tak dużym zmianom.


To powiedziawszy, powiedziałbym, że pragmatyczne korzyści wynikające z przeciążenia są tak duże, że użycie terminu „uwłaczającego”, takiego jak „cukier składniowy”, nie wydaje się prawidłowe.

Możesz przetłumaczyć całą składnię Haskell na bardzo prosty język Core (co jest faktycznie wykonywane podczas kompilacji), więc większość składni Haskell może być postrzegana jako „cukier składniowy” dla czegoś, co jest tylko rachunkiem lambda i nieco nowymi konstrukcjami. Możemy jednak zgodzić się, że programy Haskell są znacznie łatwiejsze w obsłudze i bardzo zwięzłe, podczas gdy przetłumaczone programy są trudniejsze do odczytania lub przemyślenia.


2

Jeśli wysyłka jest rozstrzygana w czasie kompilacji, w zależności tylko od statycznego typu wyrażenia argumentu, możesz z pewnością argumentować, że jest to „cukier składniowy” zastępujący dwie różne metody różnymi nazwami, pod warunkiem, że programista „zna” typ statyczny i może po prostu użyć właściwej nazwy metody zamiast przeciążonej nazwy. Tak też jest forma statycznego polimorfizmu, ale w tej ograniczonej formie zwykle nie jest bardzo silna.

Oczywiście uciążliwością byłaby zmiana nazw wywoływanych metod przy każdej zmianie typu zmiennej, ale na przykład w języku C jest to uważane za uciążliwe, więc C nie ma przeciążenia funkcji (chociaż ma teraz ogólne makra).

W szablonach C ++ iw każdym języku, który dokonuje nietrywialnej dedukcji typu statycznego, tak naprawdę nie można argumentować, że jest to „cukier syntaktyczny”, chyba że argumentujesz również, że dedukcja typu statycznego to „cukier syntaktyczny”. Uciążliwym byłoby nie mieć szablonów, aw kontekście C ++ byłoby to „niedogodne do opanowania”, ponieważ są one tak idiomatyczne dla języka i jego standardowych bibliotek. Tak więc w C ++ jest to coś więcej niż miły pomocnik, jest ważne dla stylu języka, więc myślę, że musisz to nazwać bardziej niż „cukier składniowy”.

W Javie możesz uznać to za coś więcej niż wygodę, biorąc pod uwagę na przykład liczbę przeciążeń PrintStream.printi PrintStream.println. Ale jest tyle DataInputStream.readXmetod, ponieważ Java nie przeciąża typu zwracanego, więc w pewnym sensie jest to tylko dla wygody. Wszystkie dotyczą prymitywnych typów.

Nie pamiętam, co się dzieje w Javie, jeśli mam zajęcia Ai Bwydłużanie O, ja przeciążać metody foo(O), foo(A)a foo(B), a następnie w ogólnym z <T extends O>zadzwonię foo(t)gdzie tjest instancją T. W przypadku, gdy Tjest Auzyskać wysyłkę na podstawie przeciążenia czy jest to jakby zadzwoniłem foo(O)?

Jeśli to pierwsze, to przeciążenia metody Java są lepsze niż cukier w taki sam sposób, jak przeciążenia C ++. Używając twojej definicji, przypuszczam, że w Javie mógłbym lokalnie napisać serię kontroli typu (co byłoby kruche, ponieważ nowe przeciążenia foowymagałyby dodatkowych kontroli). Oprócz zaakceptowania tej niestabilności nie mogę dokonać lokalnej zmiany w witrynie wywoływania, aby zrobić to dobrze, zamiast tego musiałbym zrezygnować z pisania ogólnego kodu. Twierdziłbym, że zapobieganie rozdętemu kodowi może być cukrem składniowym, ale zapobieganie niestabilnemu kodowi to coś więcej. Z tego powodu statyczny polimorfizm ogólnie jest czymś więcej niż tylko cukrem syntaktycznym. Sytuacja w danym języku może być inna, w zależności od tego, jak daleko język pozwala przejść, „nie znając” typu statycznego.


W Javie przeciążenia są rozwiązywane w czasie kompilacji. Biorąc pod uwagę użycie typu wymazywania, byłoby inaczej. Ponadto, nawet bez typu usunięcia, jeśli T:Animalto jest rodzaj SiameseCati istniejące przeciążenia są Cat Foo(Animal), SiameseCat Foo(Cat)i Animal Foo(SiameseCat), co przeciążenie powinien być wybrany, jeśli Tjest SiameseCat?
supercat

@supercat: ma sens. Mogłem więc znaleźć odpowiedź, nie pamiętając (lub, oczywiście, uruchom ją). Dlatego przeciążenia Java nie są lepsze niż cukier w taki sam sposób, jak przeciążenia C ++ odnoszą się do kodu ogólnego. Jest możliwe, że istnieje inny sposób, w jaki są lepsze niż tylko lokalna transformacja. Zastanawiam się, czy powinienem zmienić przykład na C ++, czy zostawić go jako trochę wyimaginowanej Java, która nie jest prawdziwą Javą.
Steve Jessop

Przeciążenia mogą być pomocne w przypadkach, gdy metody mają opcjonalne argumenty, ale mogą być również niebezpieczne. Załóżmy, że linia long foo=Math.round(bar*1.0001)*5zostanie zmieniona na long foo=Math.round(bar)*5. Jak wpłynęłoby to na semantykę, gdyby była barrówna np. 123456789L?
supercat

@supercat będę argumentować rzeczywiste niebezpieczeństwo istnieje niejawna konwersja od longcelu double.
Doval

@Doval: To double?
supercat

1

Wygląda na to, że „cukier syntaktyczny” brzmi obraźliwie, jak bezużyteczny lub niepoważny. Dlatego pytanie wywołuje wiele negatywnych odpowiedzi.

Ale masz rację, przeciążanie metod nie dodaje żadnej funkcji do języka, z wyjątkiem możliwości użycia tej samej nazwy dla różnych metod. Możesz jawnie określić typ parametru, program będzie nadal działał tak samo.

To samo dotyczy nazw pakietów. Łańcuch jest po prostu cukrem syntaktycznym dla java.lang.String.

W rzeczywistości metoda taka

void fun(int i, String c);

w klasie MyClass należy nazwać „my_package_MyClass_fun_int_java_lang_String”. Oznaczałoby to jednoznaczną identyfikację metody. (JVM robi coś takiego wewnętrznie). Ale nie chcesz tego pisać. Właśnie dlatego kompilator pozwoli ci pisać zabawnie (1, „jeden”) i określa, którą to metodę.

Jest jednak jedna rzecz, którą można zrobić z przeciążeniem: jeśli przeciążymy metodę z taką samą liczbą argumentów, kompilator automatycznie ustali, która wersja najlepiej odpowiada argumentowi podanemu przez dopasowanie argumentów, nie tylko z równymi typami, ale także gdzie dany argument jest podklasą zadeklarowanego argumentu.

Jeśli masz dwie przeciążone procedury

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

nie musisz wiedzieć, że istnieje konkretna wersja procedury Daty. addParameter („hello”, „world) wywoła pierwszą wersję, addParameter („ teraz ”, nowa data ()) wywoła drugą.

Oczywiście powinieneś unikać przeciążania metody inną metodą, która robi coś zupełnie innego.


1

Co ciekawe, odpowiedź na to pytanie zależy od języka.

W szczególności istnieje interakcja między przeciążeniem a programowaniem ogólnym (*), a w zależności od sposobu realizacji programowania ogólnego może to być po prostu cukier składniowy (Rust) lub absolutnie niezbędny (C ++).

Oznacza to, że gdy programowanie ogólne jest implementowane z wyraźnymi interfejsami (w Rust lub Haskell byłyby to klasy typu), wówczas przeciążenie jest po prostu cukrem syntaktycznym; lub może nawet nie być częścią języka.

Z drugiej strony, gdy programowanie ogólne jest wdrażane za pomocą pisania kaczego (dynamicznego lub statycznego), nazwa metody jest istotną umową, a zatem przeciążenie jest obowiązkowe, aby system mógł działać.

(*) Używany w sensie jednokrotnego napisania metody, aby operować na różnych typach w jednolity sposób.


0

W niektórych językach jest to bez wątpienia jedynie cukier syntaktyczny. Jednak to, co jest cukrem, zależy od twojego punktu widzenia. Zostawię tę dyskusję na później w tej odpowiedzi.

Na razie chciałbym zauważyć, że w niektórych językach z pewnością nie jest to cukier składniowy. Przynajmniej nie bez konieczności stosowania zupełnie innej logiki / algorytmu do implementacji tego samego. To tak, jakby twierdzić, że rekurencja jest cukrem składniowym (tak jest, ponieważ można napisać cały algorytm rekurencyjny za pomocą pętli i stosu).

Jeden przykład bardzo trudnego do zastąpienia użycia pochodzi z języka, który jak na ironię nie nazywa tej funkcji „przeciążeniem funkcji”. Zamiast tego nazywa się to „dopasowywaniem wzorców” (co może być postrzegane jako nadzbiór przeciążania, ponieważ możemy przeciążać nie tylko typy, ale wartości).

Oto klasyczna naiwna implementacja funkcji Fibonacciego w Haskell:

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

Prawdopodobnie te trzy funkcje mogą być zastąpione przez if / else, ponieważ jest to zwykle wykonywane w każdym innym języku. Ale to zasadniczo czyni całkowicie prostą definicję:

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

znacznie bardziej chaotyczny i nie wyraża bezpośrednio matematycznego pojęcia sekwencji Fibonacciego.

Czasami może to być cukier składniowy, jeśli jedynym zastosowaniem jest umożliwienie wywołania funkcji z różnymi argumentami. Ale czasami jest to o wiele bardziej fundamentalne.


Teraz porozmawiajmy o tym, do czego przeciążenie operatora może być cukrem. Zidentyfikowałeś jeden przypadek użycia - można go użyć do implementacji podobnych funkcji, które przyjmują różne argumenty. Więc:

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

można alternatywnie zaimplementować jako:

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

lub nawet:

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

Ale przeciążenie operatora może być również cukrem do implementacji opcjonalnych argumentów (niektóre języki mają przeciążenie operatora, ale nie opcjonalne argumenty):

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

może być wykorzystany do wdrożenia:

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

W takim języku (Google „Ferite language”) usunięcie przeciążenia operatora drastycznie usuwa jedną funkcję - opcjonalne argumenty. Przyznane w językach, w których obie funkcje (c ++) usuwają jedną lub drugą, nie będą miały żadnego efektu netto, ponieważ można użyć dowolnego z nich do implementacji opcjonalnych argumentów.


Haskell jest dobrym przykładem tego, dlaczego przeciążenie operatora nie jest cukrem składniowym, ale myślę, że lepszym przykładem byłoby dekonstruowanie algebraicznego typu danych z dopasowaniem wzorca (co, o ile wiem, jest niemożliwe bez dopasowania wzorca).
11684

@ 11684: Czy możesz wskazać przykład? Szczerze mówiąc, wcale nie znam Haskella, ale uznałem, że jego wzór jest wyjątkowo elegancki, gdy zobaczyłem ten przykład Fibra (na komputerzephile na youtube).
slebetman

Biorąc pod uwagę typ danych, data PaymentInfo = CashOnDelivery | Adress String | UserInvoice CustomerInfoktóry można dopasować do wzorca konstruktorów typów.
11684,

Tak: 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. Mam nadzieję, że ten komentarz jest nieco zrozumiały.
11684

0

Myślę, że jest to prosty cukier syntaktyczny w większości języków (przynajmniej wszystko, co wiem ...), ponieważ wszystkie wymagają jednoznacznego wywołania funkcji w czasie kompilacji. A kompilator po prostu zastępuje wywołanie funkcji wyraźnym wskaźnikiem do odpowiedniej sygnatury implementacji.

Przykład w Javie:

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

W końcu można go całkowicie zastąpić prostym makro-kompilatorem z wyszukiwaniem i zamienianiem, zastępując przeciążony menedżer funkcji mangle_String i mangle_int - ponieważ lista argumentów jest częścią ostatecznego identyfikatora funkcji, to praktycznie tak się dzieje -> i dlatego jest to tylko cukier syntaktyczny.

Teraz, jeśli istnieje język, w którym funkcja jest naprawdę określana w czasie wykonywania, tak jak w przypadku przesłoniętych metod w obiektach, byłoby inaczej. Ale nie sądzę, aby istniał taki język, ponieważ metoda. Przeciążenie jest podatne na dwuznaczności, których kompilator nie może rozwiązać i które muszą być obsługiwane przez programistę z jawną obsadą. Nie można tego zrobić w czasie wykonywania.


0

W typie Java informacje są kompilowane, a które z przeciążeń jest wywoływane, decyduje w czasie kompilacji.

Poniżej znajduje się fragment sun.misc.Unsafekodu (narzędzie dla Atomics), jak pokazano w edytorze plików klas 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]

jak widać informacje o typie wywoływanej metody (linia 4) są zawarte w wywołaniu.

Oznacza to, że można utworzyć kompilator Java, który pobiera informacje o typie. Na przykład przy użyciu takiej notacji powyższym źródłem byłoby:

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

a obsada na długi byłaby opcjonalna.

W innych statycznie skompilowanych językach zobaczysz podobną konfigurację, w której kompilator zdecyduje, które przeciążenie zostanie wywołane w zależności od typu i uwzględni go w powiązaniu / wywołaniu.

Wyjątkiem są biblioteki dynamiczne C, w których nie podano informacji o typie, a próba utworzenia przeciążonej funkcji spowoduje, że linker narzeka.

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.