Mój podstawowy język to statyczny typ (Java). W Javie musisz zwrócić jeden typ z każdej metody. Na przykład nie możesz mieć metody, która warunkowo zwraca a String
lub warunkowo zwraca an Integer
. Ale na przykład w JavaScript jest to bardzo możliwe.
W statycznie pisanym języku rozumiem, dlaczego to zły pomysł. Jeśli każda metoda zwróci Object
(wspólny element nadrzędny, który dziedziczą wszystkie klasy), to Ty i kompilator nie macie pojęcia, z czym mamy do czynienia. Będziesz musiał odkryć wszystkie swoje błędy w czasie wykonywania.
Ale w dynamicznie pisanym języku może nie być nawet kompilatora. W dynamicznie pisanym języku nie jest dla mnie oczywiste, dlaczego funkcja zwracająca wiele typów jest złym pomysłem. Moje doświadczenie w językach statycznych sprawia, że unikam pisania takich funkcji, ale obawiam się, że jestem blisko myśli o funkcji, która może uczynić mój kod czystszym w sposób, którego nie widzę.
Edycja : zamierzam usunąć mój przykład (dopóki nie wymyślę lepszego). Myślę, że to kieruje ludźmi, aby odpowiedzieć na pytanie, którego nie próbuję rozwiązać.
źródło
(coerce var 'string)
plony astring
lub(concatenate 'string this that the-other-thing)
podobnie. Pisałem też takie rzeczyThingLoader.getThingById (Class<extends FindableThing> klass, long id)
. I tam mogę zwrócić tylko coś, coloader.getThingById (SubclassA.class, 14)
SubclassB
SubclassA
Odpowiedzi:
W przeciwieństwie do innych odpowiedzi, istnieją przypadki, w których zwracanie różnych typów jest dopuszczalne.
Przykład 1
W niektórych statycznie typowanych językach wiąże się to z przeciążeniem, dlatego możemy wziąć pod uwagę, że istnieje kilka metod zwracających predefiniowany, stały typ. W językach dynamicznych może to być ta sama funkcja, zaimplementowana jako:
Ta sama funkcja, różne typy wartości zwracanej.
Przykład 2
Wyobraź sobie, że otrzymujesz odpowiedź ze składnika OpenID / OAuth. Niektórzy dostawcy OpenID / OAuth mogą zawierać więcej informacji, na przykład wiek osoby.
Inni mieliby minimum, czy byłby to adres e-mail czy pseudonim.
Znowu ta sama funkcja, różne wyniki.
W tym przypadku korzyść ze zwracania różnych typów jest szczególnie ważna w kontekście, w którym nie obchodzi Cię typy i interfejsy, ale jakie obiekty faktycznie zawierają. Wyobraźmy sobie na przykład, że witryna zawiera dojrzały język. Następnie
findCurrent()
można użyć następującego:Przeformułowanie tego na kod, w którym każdy dostawca będzie miał własną funkcję, która zwróci dobrze zdefiniowany, ustalony typ nie tylko pogorszy bazę kodu i spowoduje duplikację kodu, ale także nie przyniesie żadnych korzyści. Można skończyć z takimi okropnościami jak:
źródło
sum
przykład.sum :: Num a => [a] -> a
. Możesz zsumować listę wszystkiego, co jest liczbą. W przeciwieństwie do javascript, jeśli spróbujesz zsumować coś, co nie jest liczbą, błąd zostanie wychwycony podczas kompilacji.Iterator[A]
ma metodędef sum[B >: A](implicit num: Numeric[B]): B
, która ponownie pozwala na sumowanie dowolnego rodzaju liczb i jest sprawdzana w czasie kompilacji.+
łańcuchy (implementującNum
, co jest okropnym pomysłem, ale legalne) lub wymyślić inny operator / funkcję, która jest przeciążona liczbami całkowitymi i łańcuchami odpowiednio, kolejno. Haskell ma polimorfizm ad hoc poprzez klasy typów. Lista zawierająca zarówno liczby całkowite, jak i ciągi znaków jest znacznie trudniejsza (być może niemożliwa bez rozszerzeń językowych), ale raczej inna sprawa.null
wartościami.Ogólnie rzecz biorąc, jest to zły pomysł z tych samych powodów, dla których ekwiwalent moralny w języku o typie statycznym jest złym pomysłem: nie masz pojęcia, który konkretny typ jest zwracany, więc nie masz pojęcia, co możesz zrobić z wynikiem (oprócz kilka rzeczy, które można zrobić z dowolną wartością). W systemie typu statycznego masz sprawdzone przez kompilator adnotacje typów zwracanych i tak dalej, ale w dynamicznym języku ta sama wiedza wciąż istnieje - jest nieformalna i przechowywana w mózgach i dokumentacji, a nie w kodzie źródłowym.
Jednak w wielu przypadkach istnieje rym i powód, dla którego zwracany jest typ, a efekt jest podobny do przeciążenia lub polimorfizmu parametrycznego w systemach typu statycznego. Innymi słowy, typ wyniku jest przewidywalny, po prostu nie jest tak prosty do wyrażenia.
Należy jednak pamiętać, że mogą istnieć inne powody, dla których określona funkcja jest źle zaprojektowana: Na przykład
sum
funkcja zwracająca wartość false w przypadku nieprawidłowych danych wejściowych jest złym pomysłem, głównie dlatego, że ta zwracana wartość jest bezużyteczna i podatna na błędy (0 <-> fałszywe zamieszanie).źródło
NaN
to „kontrapunkt”. NaN jest w rzeczywistości prawidłowym wejściem do wszelkich obliczeń zmiennoprzecinkowych. Możesz zrobić wszystko,NaN
co możesz zrobić z rzeczywistą liczbą. To prawda, że wynik końcowy wspomnianych obliczeń może nie być dla ciebie bardzo przydatny, ale nie doprowadzi to do dziwnych błędów w czasie wykonywania i nie musisz go cały czas sprawdzać - możesz to sprawdzić tylko raz na końcu seria obliczeń. NaN nie jest innym typem , to tylko specjalna wartość , coś w rodzaju Null Object .NaN
podobnie jak obiekt zerowy, ma tendencję do ukrywania się w przypadku wystąpienia błędów, tylko po to, aby pojawiały się później. Na przykład twój program prawdopodobnie wybuchnie, jeśliNaN
wpadnie w pętlę warunkową.NaN
wartość (a) nie jest właściwie innym typem zwrotu i (b) ma dobrze zdefiniowaną semantykę zmiennoprzecinkową, a zatem tak naprawdę nie kwalifikuje się jako odpowiednik wartości zmiennej o zmiennym typie. Naprawdę nie różni się to wcale od zera w liczbach całkowitych; jeśli zero „przypadkowo” wpada do twoich obliczeń, to często możesz skończyć z zerowym wynikiem lub błędem dzielenia przez zero. Czy wartości „nieskończoności” zdefiniowane przez zmiennoprzecinkowy IEEE są również złe?W językach dynamicznych nie należy pytać, czy zwracane są różne typy, ale obiekty z różnymi interfejsami API . Ponieważ większość języków dynamicznych tak naprawdę nie dba o typy, ale używa różnych wersji pisania kaczego .
Zwracając różne typy mają sens
Na przykład ta metoda ma sens:
Ponieważ zarówno plik, jak i lista ciągów są (w języku Python) iterowalnymi, które zwracają ciąg. Bardzo różne typy, ten sam interfejs API (chyba że ktoś próbuje wywołać metody plików na liście, ale to inna historia).
Możesz warunkowo zwrócić
list
lubtuple
(tuple
jest niezmienna lista w pythonie).Formalnie nawet robiąc:
lub:
zwraca różne typy, ponieważ zarówno Python, jak
None
i Javascriptnull
są typami same w sobie.Wszystkie te przypadki użycia miałyby swój odpowiednik w językach statycznych, funkcja po prostu zwróciłaby odpowiedni interfejs.
Dobrym pomysłem jest warunkowo zwracanie obiektów z innym interfejsem API
Jeśli chodzi o to, czy zwracanie różnych interfejsów API jest dobrym pomysłem, IMO w większości przypadków nie ma sensu. Jedynym sensownym przykładem, który przychodzi mi na myśl, jest coś podobnego do tego, co powiedział @MainMa : jeśli Twój interfejs API może zapewniać różną ilość szczegółów, warto zwrócić więcej szczegółów, jeśli są dostępne.
źródło
do_file
zwraca iterowalną ciąg znaków w obu kierunkach.iter()
zarówno listę, jak i plik, aby upewnić się, że wynik może być użyty tylko jako iterator w obu przypadkach.None or something
jest zabójcą optymalizacji wydajności wykonywanej przez narzędzia takie jak PyPy, Numba, Pyston i inne. Jest to jeden z Python-ism, który sprawia, że Python nie jest tak szybki.Twoje pytanie sprawia, że chcę trochę płakać. Nie w podanym przykładzie użycia, ale dlatego, że ktoś nieświadomie podejmie to podejście zbyt daleko. To krótki krok od śmiesznie niemożliwego do utrzymania kodu.
Przypadek użycia rodzaju błędu ma sens, a wzorzec zerowy (wszystko musi być wzorzec) w językach o typie statycznym robi to samo. Wywołanie funkcji zwraca
object
lub zwracanull
.Ale to mały krok do powiedzenie „mam zamiar to wykorzystać, aby stworzyć wzór fabryczny ” i wrócić albo
foo
albobar
albobaz
w zależności od nastroju funkcji. Debugowanie tego stanie się koszmarem, gdy dzwoniący oczekuje,foo
ale został podanybar
.Więc nie sądzę, że jesteś zamknięty. Zachowujesz odpowiednią ostrożność w korzystaniu z funkcji języka.
Ujawnienie: moje pochodzenie jest w językach o typie statycznym i ogólnie pracowałem w większych, różnorodnych zespołach, w których potrzeba utrzymania kodu była dość wysoka. Więc moja perspektywa jest prawdopodobnie również wypaczona.
źródło
Korzystanie z Generics w Javie pozwala zwrócić inny typ, zachowując jednocześnie bezpieczeństwo typu statycznego. Po prostu określ typ, który chcesz zwrócić, w ogólnym parametrze typu wywołania funkcji.
To, czy możesz zastosować podobne podejście w JavaScript, jest oczywiście pytaniem otwartym. Ponieważ Javascript jest językiem dynamicznie wpisywanym, zwracanie
object
wydaje się oczywistym wyborem.Jeśli chcesz wiedzieć, gdzie może działać scenariusz dynamicznego powrotu, gdy jesteś przyzwyczajony do pracy w języku o typie statycznym, rozważ spojrzenie na
dynamic
słowo kluczowe w C #. Rob Conery był w stanie z powodzeniem napisać obiektowo-mapujący obiekt mapujący w 400 liniach kodu za pomocądynamic
słowa kluczowego.Oczywiście wszystko, co
dynamic
naprawdę robi, to owinięcieobject
zmiennej z pewnym bezpieczeństwem typu wykonawczego.źródło
Myślę, że złym pomysłem jest warunkowe zwracanie różnych typów. Jednym ze sposobów, w jaki często się to zdarza, jest to, że funkcja może zwrócić jedną lub więcej wartości. Jeśli trzeba zwrócić tylko jedną wartość, rozsądne może być po prostu zwrócenie wartości zamiast spakowania jej do tablicy, aby uniknąć konieczności rozpakowywania jej w funkcji wywołującej. Jednak to (i większość innych tego przypadków) nakłada na osobę dzwoniącą obowiązek rozróżnienia i obsługi obu typów. Funkcja będzie łatwiejsza do uzasadnienia, jeśli zawsze zwraca ten sam typ.
źródło
„Zła praktyka” istnieje niezależnie od tego, czy Twój język jest wpisany statycznie. Język statyczny robi więcej, aby odciągnąć Cię od tych praktyk, a możesz znaleźć więcej użytkowników, którzy narzekają na „złe praktyki” w języku statycznym, ponieważ jest to język bardziej formalny. Jednak podstawowe problemy występują w dynamicznym języku i można ustalić, czy są one uzasadnione.
Oto budząca zastrzeżenia część tego, co proponujesz. Jeśli nie wiem, jaki typ jest zwracany, nie mogę natychmiast użyć wartości zwracanej. Muszę coś „odkryć” na ten temat.
Często tego rodzaju zmiana kodu jest po prostu złą praktyką. Utrudnia to odczytanie kodu. W tym przykładzie widać, dlaczego popularne jest zgłaszanie i wyłapywanie przypadków wyjątków. Mówiąc inaczej: jeśli twoja funkcja nie może zrobić tego, co mówi, to nie powinna powrócić . Jeśli wywołam twoją funkcję, chcę to zrobić:
Ponieważ pierwszy wiersz powraca pomyślnie, zakładam, że
total
faktycznie zawiera on sumę tablicy. Jeśli to się nie powiedzie, przejdziemy do innej strony mojego programu.Użyjmy innego przykładu, który nie dotyczy tylko propagowania błędów. Może sum_of_array próbuje być mądry i w niektórych przypadkach zwraca ciąg czytelny dla człowieka, na przykład „To moja kombinacja szafek!” tylko wtedy, gdy tablica ma wartość [11,7,19]. Mam problem z wymyśleniem dobrego przykładu. W każdym razie dotyczy tego samego problemu. Musisz sprawdzić wartość zwracaną, zanim będziesz mógł cokolwiek z tym zrobić:
Możesz argumentować, że użyteczne byłoby, aby funkcja zwróciła liczbę całkowitą lub zmiennoprzecinkową, np .:
Ale te wyniki nie są różnymi typami, jeśli o ciebie chodzi. Będziesz traktował je oboje jak liczby i tylko na tym ci zależy. Tak więc sum_of_array zwraca typ liczby. Na tym polega polimorfizm.
Istnieją więc pewne praktyki, które możesz naruszać, jeśli funkcja może zwracać wiele typów. Ich znajomość pomoże ci ustalić, czy konkretna funkcja i tak powinna zwrócić wiele typów.
źródło
W rzeczywistości zwracanie różnych typów nie jest wcale rzadkie, nawet w języku o typie statycznym. Właśnie dlatego mamy na przykład typy związków.
W rzeczywistości metody w Javie prawie zawsze zwracają jeden z czterech typów: jakiś obiekt lub
null
wyjątek albo nigdy nie zwracają w ogóle.W wielu językach warunki błędu są modelowane jako podprogramy zwracające albo typ wyniku, albo typ błędu. Na przykład w Scali:
To oczywiście głupi przykład. Typ zwracany oznacza „albo zwróć ciąg, albo dziesiętny”. Zgodnie z konwencją lewy typ to typ błędu (w tym przypadku ciąg z komunikatem o błędzie), a prawy typ to typ wyniku.
Jest to podobne do wyjątków, z tym wyjątkiem, że wyjątki są również konstrukcjami przepływu sterowania. W rzeczywistości są one równoważne pod względem siły ekspresji
GOTO
.źródło
Unit
(metoda nie jest wcale wymagana do powrotu). To prawda, że tak naprawdę nie używaszreturn
słowa kluczowego, ale mimo to jest to wynik metody. Z zaznaczonymi wyjątkami jest nawet wyraźnie wymienione w podpisie typu.GOTO
).Żadna odpowiedź nie wspomniała jeszcze o SOLIDNYCH zasadach. W szczególności należy postępować zgodnie z zasadą podstawienia Liskowa, że każda klasa otrzymująca typ inny niż oczekiwany może nadal pracować z tym, co otrzyma, bez robienia czegokolwiek, aby sprawdzić, jaki typ jest zwracany.
Tak więc, jeśli rzucisz jakieś dodatkowe właściwości na obiekt lub zawrócisz zwróconą funkcję jakimś dekoratorem, który nadal osiąga to, co pierwotna funkcja miała osiągnąć, jesteś dobry, o ile żaden kod wywołujący twoją funkcję nigdy nie polega na tym zachowanie w dowolnej ścieżce kodu.
Zamiast zwracać ciąg lub liczbę całkowitą, lepszym przykładem może być zwrócenie systemu zraszania lub kota. Jest to w porządku, jeśli wszystko, co zrobi kod wywołujący, to wywołanie funkcji functionInQuestion.hiss (). W rzeczywistości masz niejawny interfejs, którego oczekuje kod wywołujący, a dynamicznie pisany język nie zmusza cię do jawnego interfejsu.
Niestety, twoi współpracownicy prawdopodobnie tak zrobią, więc prawdopodobnie i tak musisz wykonać tę samą pracę w dokumentacji, z tym wyjątkiem, że nie ma powszechnie akceptowanego, zwięzłego, analizowanego maszynowo sposobu, jak to ma miejsce, gdy definiujesz interfejs w języku, który je ma.
źródło
Jedynym miejscem, w którym widzę, że wysyłam różne typy, są nieprawidłowe dane wejściowe lub „złe wyjątki”, w których „wyjątkowy” stan nie jest wyjątkowy. Na przykład z mojego repozytorium funkcji narzędziowych PHP ten skrócony przykład:
Funkcja nominalnie zwraca wartość BOOLEAN, jednak zwraca wartość NULL w przypadku nieprawidłowego wejścia. Zauważ, że od PHP 5.3 wszystkie wewnętrzne funkcje PHP również zachowują się w ten sposób . Dodatkowo niektóre wewnętrzne funkcje PHP zwracają FAŁSZ lub INT na nominalnym wejściu, patrz:
źródło
null
, ponieważ (a) głośno zawiedzie , więc jest bardziej prawdopodobne, że zostanie naprawiony na etapie programowania / testowania, (b) jest łatwy w obsłudze, ponieważ mogę złapać wszystkie rzadkie / nieoczekiwane wyjątki raz całą moją aplikację (w moim przednim kontrolerze) i po prostu ją zaloguj (zazwyczaj wysyłam e-maile do zespołu programistów), aby można ją było później naprawić. I tak naprawdę nienawidzę tego, że standardowa biblioteka PHP używa głównie metody „return null” - to naprawdę sprawia, że kod PHP jest bardziej podatny na błędy, chyba że sprawdzasz wszystko za pomocąisset()
, co jest po prostu zbyt dużym obciążeniem.null
z błędem, proszę wyjaśnij to w docblock (np.@return boolean|null
), Więc jeśli kiedyś napotkam twój kod, nie będę musiał sprawdzać funkcji / metody.Nie sądzę, że to zły pomysł! W przeciwieństwie do tej najczęstszej opinii, jak już zauważył Robert Harvey, języki o typie statycznym, takie jak Java, wprowadziły Generics dokładnie w sytuacjach takich jak ta, o którą pytasz. W rzeczywistości Java próbuje zachować (tam, gdzie to możliwe) bezpieczeństwo typów w czasie kompilacji, a czasami generycy unikają powielania kodu, dlaczego? Ponieważ możesz napisać tę samą metodę lub tę samą klasę, która obsługuje / zwraca różne typy . Zrobię tylko bardzo krótki przykład, aby pokazać ten pomysł:
Java 1.4
Java 1.5+
W dynamicznym języku pisanym, ponieważ w czasie kompilacji nie masz bezpieczeństwa typu, masz całkowitą swobodę pisania tego samego kodu, który działa dla wielu typów. Ponieważ nawet w statycznie typowanym języku wprowadzono Generics w celu rozwiązania tego problemu, jest to wyraźnie wskazówka, że napisanie funkcji zwracającej różne typy w dynamicznym języku nie jest złym pomysłem.
źródło
Tworzenie oprogramowania to sztuka i umiejętność zarządzania złożonością. Próbujesz zawęzić system w punktach, na które Cię stać, i ograniczyć opcje w innych punktach. Interfejs funkcji to umowa, która pomaga zarządzać złożonością kodu, ograniczając wiedzę wymaganą do pracy z dowolnym fragmentem kodu. Zwracając różne typy znacznie rozszerzasz interfejs funkcji, dodając do niej wszystkie interfejsy różnych typów, które zwracasz ORAZ dodając nieoczywiste reguły określające, który interfejs jest zwracany.
źródło
Perl używa tego bardzo często, ponieważ to, co robi funkcja, zależy od jej kontekstu . Na przykład funkcja może zwrócić tablicę, jeśli zostanie użyta w kontekście listy, lub długość tablicy, jeśli zostanie użyta w miejscu, w którym oczekiwana jest wartość skalarna. Z samouczka, który jest pierwszym hitem dla „kontekstu perla” , jeśli:
Zatem @now jest zmienną tablicową (to właśnie oznacza @) i będzie zawierać tablicę podobną do (40, 51, 20, 9, 0, 109, 5, 8, 0).
Jeśli zamiast tego wywołujesz funkcję w taki sposób, że jej wynikiem będzie musiał być skalar, z (zmienne $ są skalarami):
wtedy robi coś zupełnie innego: $ teraz będzie coś w stylu „Pt 9 stycznia 20:51:40 2009”.
Innym przykładem, jaki mogę wymyślić, jest implementacja interfejsów API REST, gdzie format zwracanego pliku zależy od tego, czego chce klient. Np. HTML lub JSON lub XML. Chociaż technicznie są to wszystkie strumienie bajtów, pomysł jest podobny.
źródło
String
. Teraz ludzie proszą o dokumentację dotyczącą rodzaju zwrotu. Narzędzia Java mogą generować je automatycznie, jeśli zwrócę klasę, ale ponieważ wybrałem,String
nie mogę automatycznie wygenerować dokumentacji. Z perspektywy czasu / moralności tej historii chciałbym zwrócić jeden typ na metodę.W dynamicznej krainie chodzi o pisanie kaczek. Najbardziej odpowiedzialną czynnością związaną z ujawnieniem / upublicznieniem jest zawinięcie potencjalnie różnych typów w opakowanie, które daje im ten sam interfejs.
Czasami może mieć sens odkładanie różnych typów w ogóle bez ogólnego opakowania, ale szczerze mówiąc w ciągu 7 lat pisania JS, nie jest to coś, co uważam za rozsądne lub wygodne, aby robić to bardzo często. Najczęściej robię to w kontekście zamkniętych środowisk, takich jak wnętrze obiektu, w którym wszystko się składa. Ale nie robię tego wystarczająco często, że przychodzą mi na myśl wszelkie przykłady.
Radziłbym przede wszystkim, abyście przestali tak samo myśleć o typach. Z typami radzisz sobie w dynamicznym języku. Już nie. O to chodzi. Nie sprawdzaj typu każdego argumentu. Kusiłoby mnie to tylko w środowisku, w którym ta sama metoda mogłaby dawać niespójne wyniki w nieoczywisty sposób (więc zdecydowanie nie rób czegoś takiego). Ale nie jest to typ, który ma znaczenie, to jak cokolwiek mi dajesz, działa.
źródło