Przeciążenie funkcji? Tak lub nie [zamknięte]

17

Opracowuję statycznie i silnie typowany, skompilowany język i ponownie zastanawiam się nad tym, czy włączyć funkcję przeciążania funkcji jako funkcję języka. Uświadomiłem sobie, że jestem trochę stronniczy, pochodząc głównie z C[++|#]tła.

Jakie są najbardziej przekonujące argumenty za i przeciw uwzględnieniu przeciążenia funkcji w języku?


EDYCJA: Czy nie ma nikogo, kto ma przeciwne zdanie?

Bertrand Meyer (twórca Eiffla w 1985/1986) wywołuje metodę przeciążającą to: (źródło)

mechanizm próżności, który nie wnosi nic do semantycznej potęgi języka OO, ale utrudnia czytelność i komplikuje zadanie każdego

Są to teraz pewne ogólne uogólnienia, ale jest on mądrym facetem, więc myślę, że można bezpiecznie powiedzieć, że mógłby je w razie potrzeby poprzeć. W rzeczywistości prawie przekonał Brada Abramsa (jednego z programistów CLSv1) do przekonania, że ​​.NET nie powinien obsługiwać przeciążania metod. (źródło) To kilka potężnych rzeczy. Czy ktoś może rzucić nieco światła na jego myśli i czy jego punkt widzenia jest nadal uzasadniony 25 lat później?

Uwaga do samodzielnego wymyślenia imienia
źródło

Odpowiedzi:

24

Przeciążenie funkcji jest absolutnie niezbędne dla kodu szablonu w stylu C ++. Jeśli muszę używać różnych nazw funkcji dla różnych typów, nie mogę pisać kodu ogólnego. To wyeliminowałoby dużą i intensywnie używaną część biblioteki C ++ oraz znaczną część funkcjonalności C ++.

Zwykle występuje w nazwach funkcji składowych. A.foo()może wywoływać zupełnie inną funkcję B.foo(), ale obie funkcje są nazwane foo. Jest obecny w operatorach, podobnie +jak różne rzeczy w przypadku liczb całkowitych i liczb zmiennoprzecinkowych, i często jest używany jako operator konkatenacji łańcuchów. Wydaje się dziwne, że nie pozwala się na to również w zwykłych funkcjach.

Umożliwia korzystanie z „multimetod” w stylu Common Lisp, w których wywoływana funkcja zależy od dwóch typów danych. Jeśli nie programowałeś w Common Lisp Object System, wypróbuj go, zanim nazwiesz to bezużytecznym. Jest to niezbędne w przypadku strumieni C ++.

We / wy bez przeciążenia funkcji (lub funkcji wariadycznych, które są gorsze) wymagałoby wielu różnych funkcji, albo do drukowania wartości różnych typów, albo do konwersji wartości różnych typów na typ wspólny (jak String).

Bez przeciążania funkcji, jeśli zmienię typ jakiejś zmiennej lub wartości, muszę zmienić każdą funkcję, która z niej korzysta. Znacznie trudniej jest zmienić kod.

Ułatwia korzystanie z interfejsów API, gdy użytkownik nie musi pamiętać, która konwencja nazewnictwa jest w użyciu, a użytkownik może po prostu zapamiętać standardowe nazwy funkcji.

Bez przeciążania operatora musielibyśmy oznaczyć każdą funkcję typami, których używa, jeśli ta podstawowa operacja może być użyta na więcej niż jednym typie. Jest to zasadniczo węgierski zapis, zły sposób na zrobienie tego.

Ogólnie rzecz biorąc, sprawia, że ​​język jest znacznie bardziej użyteczny.

David Thornley
źródło
1
+1, wszystkie bardzo dobre punkty. I uwierzcie mi, nie sądzę, by multimetody były bezużyteczne ... Przeklinam samą klawiaturę, którą piszę za każdym razem, gdy jestem zmuszony do korzystania ze wzoru odwiedzającego.
Uwaga dla siebie - wymyśl imię
Czy ten facet opisujący funkcję nie jest nadpisywany, a nie przeciążany?
dragosb
8

Polecam przynajmniej mieć świadomość klas typów w Haskell. Klasy typów zostały stworzone, aby być zdyscyplinowanym podejściem do przeciążania operatorów, ale znalazły inne zastosowania i do pewnego stopnia sprawiły, że Haskell jest tym, czym jest.

Na przykład, oto przykład przeciążenia ad-hoc (niezupełnie prawidłowy Haskell):

(==) :: Int -> Int -> Bool
x == y = ...
x /= y = not (x == y)

(==) :: Char -> Char -> Bool
x == y = ...
x /= y = not (x == y)

A oto ten sam przykład przeciążenia klasami typów:

class Eq a where
    (==) :: a -> a -> Bool
    (/=) :: a -> a -> Bool

    x /= y  =  not (x == y)

instance Eq Int where
    x == y  = ...

instance Eq Char where
    x == y  = ...

Minusem jest to, że trzeba wymyślić z funky nazw dla wszystkich typeclasses (jak w Haskell, masz dość abstrakcyjny Monad, Functor, Applicativejak również prostsze i bardziej rozpoznawalne Eq, Numi Ord).

Zaletą jest to, że gdy znasz już klasę, wiesz, jak używać dowolnego typu w tej klasie. Ponadto łatwo jest chronić funkcje przed typami, które nie implementują potrzebnych klas, jak w:

group :: (Eq a) => [a] -> [[a]]
group = groupBy (==)

Edycja: Jeśli w Haskell chcesz ==operatora, który akceptuje dwa różne typy, możesz użyć klasy typu wieloparametrowego:

class Eq a b where
    (==) :: a -> b -> Bool
    (/=) :: a -> b -> Bool

    x /= y  =  not (x == y)

instance Eq Int Int where
    x == y  = ...

instance Eq Char Char where
    x == y  = ...

instance Eq Int Float where
    x == y  = ...

Oczywiście jest to prawdopodobnie zły pomysł, ponieważ pozwala wyraźnie porównywać jabłka i pomarańcze. Możesz jednak rozważyć to +, ponieważ dodawanie Word8do Intnaprawdę jest rozsądną rzeczą do zrobienia w niektórych kontekstach.

Joey Adams
źródło
+1, staram się zrozumieć tę koncepcję, odkąd ją przeczytałem, a to pomaga. Czy ten paradygmat uniemożliwia na przykład zdefiniowanie (==) :: Int -> Float -> Boolgdziekolwiek? (niezależnie od tego, czy jest to dobry pomysł, oczywiście)
Uwaga dla siebie - wymyśl imię
Jeśli dopuścisz klasy typów wieloparametrowych (które Haskell obsługuje jako rozszerzenie), możesz to zrobić. Zaktualizowałem odpowiedź za pomocą przykładu.
Joey Adams
Hmm interesujące. Zasadniczo class Eq a ...byłoby przetłumaczone na pseudo-C-rodzinę interface Eq<A> {bool operator==(A x, A y);}i zamiast używać kodu szablonowego do porównywania dowolnych obiektów, używasz tego „interfejsu”. Czy to prawda?
Uwaga do siebie - wymyśl imię
Dobrze. Możesz także rzucić okiem na interfejsy w Go. Mianowicie nie musisz deklarować typu jako implementującego interfejs, wystarczy zaimplementować wszystkie metody dla tego interfejsu.
Joey Adams
1
@ Notetoself-thinkofaname: W odniesieniu do dodatkowych operatorów - tak i nie. Pozwala ==być w innej przestrzeni nazw, ale nie pozwala na przesłonięcie. Pamiętaj, że Preludedomyślnie dostępna jest pojedyncza przestrzeń nazw ( ), ale możesz zapobiec jej ładowaniu, używając rozszerzeń lub importując ją jawnie ( import Prelude ()nic nie importuje Preludei import qualified Prelude as Pnie wstawia symboli do bieżącej przestrzeni nazw).
Maciej Piechotka,
4

Zezwól na przeciążenie funkcji, nie możesz wykonać następujących czynności z opcjonalnymi parametrami (lub jeśli możesz, nie ładnie).

trywialny przykład zakłada brak base.ToString() metody

string ToString(int i) {}
string ToString(double d) {}
string ToString(DateTime d) {}
...
Binarny Worrier
źródło
W przypadku silnie napisanego języka tak. W przypadku słabo wpisanego języka nie. Możesz to zrobić tylko za pomocą jednej funkcji w słabo napisanym języku.
spex
2

Zawsze wolałem parametry domyślne od przeciążenia funkcji. Przeciążone funkcje zazwyczaj wywołują „domyślną” wersję z domyślnymi parametrami. Po co pisać

int indexOf(char ch)
{
  return self.indexOf(ch, 0);
}

int indexOf(char ch, int fromIndex)
{
  // Do whatever
}

Kiedy mogłem zrobić:

int indexOf(char ch, int fromIndex=0)
{
  // Do whatever
}

To powiedziawszy, zdaję sobie sprawę, że czasami przeciążone funkcje robią różne rzeczy, zamiast wywoływać inny wariant z domyślnymi parametrami ... ale w takim przypadku nie jest to zły pomysł (w rzeczywistości prawdopodobnie dobry pomysł), aby po prostu dać mu inna nazwa.

(Także argumenty słów kluczowych w stylu Pythona działają naprawdę ładnie z parametrami domyślnymi.)

mipadi
źródło
Dobra, spróbujmy jeszcze raz, a tym razem postaram się nadać sens ... A co Array Slice(int start, int length) {...}z przeciążeniem Array Slice(int start) {return this.Slice(start, this.Count - start);}? Tego nie można zakodować przy użyciu parametrów domyślnych. Czy uważasz, że należy im nadać inne nazwy? Jeśli tak, to jak byś je nazwał?
Uwaga dla siebie - wymyśl imię
To nie dotyczy wszystkich zastosowań przeciążenia.
MetalMikester
@MetalMikester: Czego według Ciebie brakuje?
mipadi
@mipadi Brakuje takich rzeczy jak indexOf(char ch)+ indexOf(Date dt)na liście. Lubię też wartości domyślne, ale nie można ich zamieniać z typowaniem statycznym.
Mark
2

Właśnie opisałeś Javę. Lub C #.

Dlaczego wymyślasz koło?

Upewnij się, że typ zwrotu jest częścią podpisu metody i przeciążasz zawartość twojego serca, to naprawdę czyści kod, kiedy nie musisz mówić.

function getThisFirstWay(int type)
{ ... }
function getThisSecondWay(int type, double limit)
{ ... }
function getThisThirdWay(int type, String match)
{ ... }
Josh K.
źródło
7
Istnieje powód, dla którego typ zwracany nie jest częścią sygnatury metody - a przynajmniej nie jest częścią, która może być użyta do rozwiązania problemu przeciążenia - w dowolnym znanym mi języku. Kiedy wywołujesz funkcję jako procedurę, bez przypisywania wyniku do zmiennej lub właściwości, w jaki sposób kompilator powinien dowiedzieć się, którą wersję wywołać, jeśli wszystkie pozostałe argumenty są identyczne?
Mason Wheeler,
@Mason: Możesz heurystycznie wykryć oczekiwany typ zwrotu na podstawie tego, co ma zostać zwrócone, jednak nie oczekuję, że zostanie to zrobione.
Josh K
1
Umm ... skąd twój heurysta wie, co ma zostać zwrócone, kiedy nie oczekujesz żadnej wartości zwrotu?
Mason Wheeler,
1
Heurystyka typu oczekiwanie typu faktycznie jest na miejscu ... możesz robić takie rzeczy EnumX.Flag1 | Flag2 | Flag3. Jednak nie będę tego wdrażał. Gdybym to zrobił i typ zwrotu nie był używany, szukałbym typu zwrotu void.
Uwaga dla siebie - wymyśl imię
1
@Mason: To dobre pytanie, ale w takim przypadku szukałbym funkcji void (jak wspomniano). Również w teorii można wybrać dowolny z nich, bo wszyscy oni pełnią tę samą funkcję, wystarczy zwrócić dane w innym formacie.
Josh K
2

Grrr .. niewystarczający przywilej, aby komentować jeszcze ..

@Mason Wheeler: Pamiętaj więc o Adzie, która przeciąża typ zwrotu. Również mój język Felix robi to w niektórych kontekstach, szczególnie gdy funkcja zwraca inną funkcję i pojawia się wywołanie takie jak:

f a b  // application is left assoc: (f a) b

typ b może być użyty do rozwiązania w przypadku przeciążenia. Również C ++ przeciąża typ zwracany w niektórych okolicznościach:

int (*f)(int) = g; // choses g based on type, not just signature

W rzeczywistości istnieją algorytmy przeciążania typu zwracanego za pomocą wnioskowania typu. W rzeczywistości nie jest tak trudno zrobić z maszyną, problem polega na tym, że ludzie mają trudności. (Myślę, że kontur jest podany w Dragon Book, algorytm nazywa się algorytmem saw-saw, jeśli dobrze pamiętam)

Yttrill
źródło
2

Przypadek użycia przeciwko implementacji przeciążenia funkcji: 25 podobnie nazwanych metod, które robią te same rzeczy, ale z zupełnie innymi zestawami argumentów w szerokiej gamie wzorców.

Przypadek użycia przeciwko niezaimplementowaniu przeładowania funkcji: 5 podobnie nazwanych metod z bardzo podobnymi zestawami typów w dokładnie tym samym wzorcu.

Na koniec dnia nie mogę się doczekać przeczytania dokumentacji dotyczącej interfejsu API opracowanego w obu przypadkach.

Ale w jednym przypadku chodzi o to, co użytkownicy mogą zrobić. W innym przypadku użytkownicy muszą to zrobić z powodu ograniczeń językowych. IMO lepiej jest przynajmniej pozwolić, aby autorzy programów byli wystarczająco inteligentni, aby rozsądnie przeciążać bez powodowania dwuznaczności. Kiedy uderzysz ich w dłonie i zabierzesz opcję, w zasadzie będziesz gwarantować dwuznaczność. Bardziej ufam użytkownikowi, że zrobi dobrą rzecz, niż zakładam, że zawsze zrobi coś złego. Z mojego doświadczenia wynika, że ​​protekcjonizm prowadzi do jeszcze gorszych zachowań społeczności językowej.

Erik Reppen
źródło
1

Zdecydowałem się zapewnić zwykłe przeciążanie i klasy typów w moim języku Felix.

Uważam (otwarte) przeładowanie za niezbędne, szczególnie w języku, który jako wiele typów liczbowych (Felix ma wszystkie typy liczbowe C). Jednak w przeciwieństwie do C ++, który nadużywa przeciążania poprzez uzależnianie szablonów od niego, polimorfizm Felixa jest parametryczny: potrzebujesz przeciążenia szablonów w C ++, ponieważ szablony w C ++ są źle zaprojektowane.

Klasy typów są również dostępne w Felix. Dla tych, którzy znają C ++, ale nie grokują Haskella, zignoruj ​​tych, którzy opisują to jako przeciążenie. Nie przypomina to przeciążania, jest raczej specjalizacją od szablonów: deklarujesz szablon, którego nie implementujesz, a następnie zapewniasz implementacje dla konkretnych przypadków, gdy są potrzebne. Wpisywanie jest parametrycznie polimorficzne, implementacja odbywa się przez tworzenie instancji ad hoc, ale nie ma być nieograniczona: musi implementować zamierzoną semantykę.

W Haskell (i C ++) nie można podać semantyki. W C ++ idea „Koncepcji” jest z grubsza próbą przybliżenia semantyki. W Felixie możesz przybliżać intencję za pomocą aksjomatów, redukcji, lematów i twierdzeń.

Główny i jedyny zaletą (otwartego) przeciążenia w dobrze funkcjonującym języku, takim jak Felix, jest to, że ułatwia zapamiętanie nazw funkcji bibliotecznych, zarówno dla twórcy programu, jak i dla recenzenta kodu.

Podstawową wadą przeciążenia jest złożony algorytm wymagany do jego wdrożenia. Nie jest też zbyt dobrze oparty na wnioskowaniu typu: chociaż oba nie są całkowicie wykluczające, algorytm do wykonania obu jest wystarczająco złożony, programista prawdopodobnie nie byłby w stanie przewidzieć wyników.

W C ++ jest to również problem, ponieważ ma on niepoprawny algorytm dopasowywania i obsługuje również automatyczne konwersje typów: w Felixie „naprawiłem” ten problem, wymagając dokładnego dopasowania i braku automatycznych konwersji typów.

Myślę, że masz wybór: przeciążenie lub wnioskowanie typu. Wnioskowanie jest urocze, ale bardzo trudne do wdrożenia w sposób, który prawidłowo diagnozuje konflikty. Na przykład Ocaml mówi ci, gdzie wykrywa konflikt, ale nie, skąd wywnioskował oczekiwany typ.

Przeładowanie nie jest dużo lepsze, nawet jeśli masz kompilator jakości, który próbuje powiedzieć ci wszystkich kandydatów, może być trudno odczytać, czy kandydaci są polimorficzni, a nawet gorzej, jeśli jest to hackery szablonów C ++.

Yttrill
źródło
Brzmi interesująco. Chciałbym przeczytać więcej, ale link do dokumentów na stronie internetowej Felix jest zepsuty.
Uwaga do siebie -
Tak, cała strona jest obecnie w budowie (ponownie), przepraszam.
Yttrill,
0

Wszystko sprowadza się do kontekstu, ale myślę, że przeciążenie sprawia, że ​​klasa jest znacznie bardziej przydatna, gdy używam jednej napisanej przez kogoś innego. Często kończy się to mniejszą redundancją.

Morgan Herlocker
źródło
0

Jeśli chcesz mieć użytkowników zaznajomionych z językami rodziny C. Tak, powinieneś, ponieważ użytkownicy będą się tego spodziewać.

Conrad Frix
źródło