Czy źle jest zwracać różne typy danych z jednej funkcji w dynamicznie pisanym języku?

65

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 Stringlub 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ć.

Daniel Kaplan
źródło
Dlaczego nie zgłosić wyjątku w przypadku błędu?
TrueWill
1
@TrueWill Zajmę się tym w następnym zdaniu.
Daniel Kaplan
Czy zauważyłeś, że jest to powszechna praktyka w bardziej dynamicznych językach? Jest to powszechne w językach funkcjonalnych , ale tam zasady są bardzo wyraźne. Wiem, że często, szczególnie w JavaScript, dopuszcza się, aby argumenty były różnych typów (ponieważ jest to w zasadzie jedyny sposób na przeciążenie funkcji), ale rzadko widziałem, aby było to stosowane do zwracanych wartości. Jedynym przykładem, jaki mogę wymyślić, jest PowerShell z jego w większości automatycznym zawijaniem / rozpakowywaniem tablic wszędzie, a języki skryptowe są trochę wyjątkowym przypadkiem.
Aaronaught
3
Nie wspomniano, ale istnieje wiele przykładów (nawet w języku Java Generics) funkcji, które przyjmują typ zwracany jako parametr; np. w Common Lisp mamy (coerce var 'string)plony a stringlub (concatenate 'string this that the-other-thing)podobnie. Pisałem też takie rzeczy ThingLoader.getThingById (Class<extends FindableThing> klass, long id). I tam mogę zwrócić tylko coś, co loader.getThingById (SubclassA.class, 14)SubclassBSubclassA
podklasuje
1
Języki wpisywane dynamicznie są jak łyżka w filmie Matrix . Nie próbuj definiować łyżki jako ciągu lub liczby . To byłoby niemożliwe. Zamiast tego ... staraj się tylko zrozumieć prawdę. Że nie ma Łyżki .
Reactgular

Odpowiedzi:

42

W przeciwieństwie do innych odpowiedzi, istnieją przypadki, w których zwracanie różnych typów jest dopuszczalne.

Przykład 1

sum(2, 3)  int
sum(2.1, 3.7)  float

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:

var sum = function (a, b) {
    return a + b;
};

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.

var user = authProvider.findCurrent();
// user is now:
// {
//     provider: 'Facebook',
//     name: {
//         firstName: 'Hello',
//         secondName: 'World',
//     },
//     email: '[email protected]',
//     age: 27
// }

Inni mieliby minimum, czy byłby to adres e-mail czy pseudonim.

var user = authProvider.findCurrent();
// user is now:
// {
//     provider: 'Google',
//     email: '[email protected]'
// }

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:

var user = authProvider.findCurrent();
if (user.age || 0 >= 16) {
    // The person can stand mature language.
    allowShowingContent();
} else if (user.age) {
    // OpenID/OAuth gave the age, but the person appears too young to see the content.
    showParentalAdvisoryRequestedMessage();
} else {
    // OpenID/OAuth won't tell the age of the person. Ask the user himself.
    askForAge();
}

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:

var age;
if (['Facebook', 'Yahoo', 'Blogger', 'LiveJournal'].contains(user.provider)) {
    age = user.age;
}
Arseni Mourzenko
źródło
5
„W językach o typie statycznym wiąże się to z przeciążeniem”. Myślę, że miałeś na myśli „w niektórych językach o typie statycznym” :) Dobre języki o typie statycznym nie wymagają przeciążania dla czegoś takiego jak twój sumprzykład.
Andres F.,
17
Dla konkretnego przykładu rozważmy Haskell: 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.
Andres F.
3
@MainMa In Scala, 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.
Petr Pudlák
3
@ Bakuriu Oczywiście możesz napisać taką funkcję, choć najpierw musisz albo przeciążać +łańcuchy (implementując Num, 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.
2
Nie jestem pod wrażeniem twojego drugiego przykładu. Te dwa obiekty są tym samym koncepcyjnym „typem”, po prostu w niektórych przypadkach pewne pola nie są zdefiniowane ani wypełnione. Można to dobrze przedstawić nawet w języku o typie statycznym z nullwartościami.
Aaronaught
31

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 sumfunkcja 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
40
Moje dwa centy: nienawidzę tego, kiedy to się dzieje. Widziałem biblioteki JS, które zwracają wartość NULL, gdy nie ma żadnych wyników, obiekt na jednym wyniku i tablica na dwóch lub więcej wynikach. Więc zamiast po prostu zapętlać tablicę, musisz określić, czy jest ona pusta, a jeśli nie, czy to jest tablica, a jeśli tak, to zrób jedną rzecz, w przeciwnym razie zrób coś innego. Zwłaszcza, że ​​w normalnym sensie każdy rozsądny programista po prostu doda obiekt do tablicy i przetworzy logikę programu z obsługi tablicy.
phyrfox
10
@Izkata: W ogóle nie uważam, że NaNto „kontrapunkt”. NaN jest w rzeczywistości prawidłowym wejściem do wszelkich obliczeń zmiennoprzecinkowych. Możesz zrobić wszystko, NaNco 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 .
Aaronaught
5
@Aaronaught Z drugiej strony, NaNpodobnie 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śli NaNwpadnie w pętlę warunkową.
Izkata
2
@Izkata: NaN i null mają zupełnie inne zachowania. Referencja zerowa zostanie wysadzona natychmiast po uzyskaniu dostępu, propaguje się NaN. To, dlaczego tak się dzieje, jest oczywiste, pozwala na pisanie wyrażeń matematycznych bez konieczności sprawdzania wyniku każdego podwyrażenia. Osobiście uważam, że zero jest o wiele bardziej szkodliwe, ponieważ NaN reprezentuje właściwą koncepcję liczbową, bez której nie można uciec, a matematycznie propagowanie jest właściwym zachowaniem.
Phoshi
4
Nie wiem, dlaczego zmieniło się to w argument „zerowy vs. wszystko inne”. Po prostu wskazałem, że NaNwartość (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?
Aaronaught
26

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:

def load_file(file): 
    if something: 
       return ['a ', 'list', 'of', 'strings'] 
    return open(file, 'r')

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ć listlub tuple( tuplejest niezmienna lista w pythonie).

Formalnie nawet robiąc:

def do_something():
    if ...: 
        return None
    return something_else

lub:

function do_something(){
   if (...) return null; 
   return sth;
}

zwraca różne typy, ponieważ zarówno Python, jak Nonei Javascript nullsą 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.

jb.
źródło
4
Dobra odpowiedź. Warto zauważyć, że odpowiednikiem typu Java / statycznie jest funkcja zwracająca interfejs, a nie konkretny konkretny typ, przy czym interfejs reprezentuje interfejs API / abstrakcję.
mikera
1
Wydaje mi się, że naciągasz, w Pythonie lista i krotka mogą być różnych „typów”, ale są one tego samego „typu kaczego”, tzn. Obsługują ten sam zestaw operacji. I takie przypadki są właśnie tym, dlaczego wprowadzono pisanie kaczek. Możesz także wykonać long x = getInt () w Javie.
Kaerber
„Zwracanie różnych typów” oznacza „zwracanie obiektów z różnymi interfejsami API”. (Niektóre języki sprawiają, że sposób konstruowania obiektu jest podstawową częścią API, niektóre nie; to kwestia ekspresji systemu typów.) W przykładzie listy / pliku do_filezwraca iterowalną ciąg znaków w obu kierunkach.
Gilles
1
W tym przykładzie w języku Python można również wywołać iter()zarówno listę, jak i plik, aby upewnić się, że wynik może być użyty tylko jako iterator w obu przypadkach.
RemcoGerlich
1
returning None or somethingjest 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.
Matt
9

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 objectlub zwraca null.

Ale to mały krok do powiedzenie „mam zamiar to wykorzystać, aby stworzyć wzór fabryczny ” i wrócić albo fooalbo baralbo bazw zależności od nastroju funkcji. Debugowanie tego stanie się koszmarem, gdy dzwoniący oczekuje, fooale został podany bar.

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.

doppelgreener
źródło
6

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 objectwydaje 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 dynamicsłowo kluczowe w C #. Rob Conery był w stanie z powodzeniem napisać obiektowo-mapujący obiekt mapujący w 400 liniach kodu za pomocą dynamicsłowa kluczowego.

Oczywiście wszystko, co dynamicnaprawdę robi, to owinięcie objectzmiennej z pewnym bezpieczeństwem typu wykonawczego.

Robert Harvey
źródło
4

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.

użytkownik116240
źródło
4

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

total = sum_of_array([20, 30, 'q', 50])
if (type_of(total) == Boolean) {
  display_error(...)
} else {
  record_number(total)
}

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ć:

total = sum_of_array([20, 30, 'q', 50])
display_number(total)

Ponieważ pierwszy wiersz powraca pomyślnie, zakładam, że totalfaktycznie 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ć:

total = sum_of_array([20, 30, 40, 50])
if (type_of(total) == String) {
  write_message(total)
} else {
  record_number(total)
}

Możesz argumentować, że użyteczne byłoby, aby funkcja zwróciła liczbę całkowitą lub zmiennoprzecinkową, np .:

sum_of_array(20, 30, 40) -> int
sum_of_array(23.45, 45.67, 67.789044) -> float

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.

Travis Wilson
źródło
Przepraszam, wprowadziłem cię w błąd moim biednym przykładem. Zapomnij, że wspomniałem o tym w pierwszej kolejności. Twój pierwszy akapit jest na temat. Podoba mi się również twój drugi przykład.
Daniel Kaplan
4

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 nullwyją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:

def transferMoney(amount: Decimal): Either[String, Decimal]

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.

Jörg W Mittag
źródło
metoda w Javie zwracająca wyjątek brzmi dziwnie. " Return to wyjątek " ... hmm
komara
3
Wyjątkiem jest jeden z możliwych wyników metody w Javie. Właściwie zapomniałem czwartego typu: Unit(metoda nie jest wcale wymagana do powrotu). To prawda, że ​​tak naprawdę nie używasz returnsłowa kluczowego, ale mimo to jest to wynik metody. Z zaznaczonymi wyjątkami jest nawet wyraźnie wymienione w podpisie typu.
Jörg W Mittag
Widzę. Twój punkt wydaje się mieć pewne zalety, ale sposób, w jaki jest prezentowany, nie sprawia, że ​​wygląda atrakcyjnie ... raczej przeciwnie
komnata
4
W wielu językach warunki błędu są modelowane jako podprogram zwracający typ wyniku lub typ błędu. Wyjątki są podobnym pomysłem, z tym że są również konstrukcjami kontrolującymi przepływ (równymi pod względem mocy ekspresyjnej do GOTO).
Jörg W Mittag
4

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

psr
źródło
3

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:

function ensure_fields($consideration)
{
        $args = func_get_args();
        foreach ( $args as $a ) {
                if ( !is_string($a) ) {
                        return NULL;
                }
                if ( !isset($consideration[$a]) || $consideration[$a]=='' ) {
                        return FALSE;
                }
        }

        return TRUE;
}

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:

strpos('Hello', 'e');  // Returns INT(1)
strpos('Hello', 'q');  // Returns BOOL(FALSE)
dotancohen
źródło
4
I dlatego nienawidzę PHP. W każdym razie jeden z powodów.
Aaronaught
1
Opinia negatywna, ponieważ ktoś nie lubi języka, w którym rozwijam się zawodowo?!? Poczekaj, aż dowiedzą się, że jestem Żydem! :)
dotancohen
3
Nie zawsze zakładaj, że ludzie, którzy komentują i osoby, które głosują, to ci sami ludzie.
Aaronaught
1
Wolę mieć wyjątek zamiast 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.
scriptin
1
Ponadto, jeśli powrócisz nullz 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.
scriptin
3

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

public static Boolean getBoolean(String property){
    return (Boolean) properties.getProperty(property);
}
public static Integer getInt(String property){
    return (Integer) properties.getProperty(property);
}

Java 1.5+

public static <T> getValue(String property, Class<T> clazz) throws WhateverCheckedException{
    return clazz.getConstructor(String.class).newInstance(properties.getProperty(property));
}
//the call will be
Boolean b = getValue("useProxy",Boolean.class);
Integer b = getValue("proxyPort",Integer.class);

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.

thermz
źródło
Kolejna opinia bez wyjaśnienia! Dzięki społeczności SE!
thermz
2
Miej współczucie +1. Powinieneś spróbować otworzyć swoją odpowiedź bardziej jednoznaczną i bezpośrednią odpowiedzią i powstrzymać się od komentowania różnych odpowiedzi. (Dałbym ci kolejne +1, skoro się z tobą zgadzam, ale mam tylko jeden.)
DougM
3
Generics nie istnieje w dynamicznie pisanych językach (o których wiem). Nie byłoby dla nich żadnego powodu. Większość rodzajów ogólnych to szczególny przypadek, w którym „kontener” jest bardziej interesujący niż to, co się w nim znajduje (dlatego niektóre języki, takie jak Java, w rzeczywistości używają usuwania typu). Typ ogólny jest właściwie rodzajem własnego. Metody ogólne , bez rzeczywistego typu ogólnego, jak w twoim przykładzie, prawie zawsze są tylko cukrem składniowym dla semantycznego przypisania i obsady. Niezbyt przekonujący przykład tego, o co tak naprawdę pyta pytanie.
Aaronaught
1
Staram się być nieco bardziej syntetyczny: „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”. Lepiej teraz? Sprawdź odpowiedź Roberta Harveya lub komentarz BRPocock, a zdasz sobie sprawę, że przykład Generics jest związany z tym pytaniem
thermz
1
Nie czuj się źle, @thermz. Przegłosowani często mówią więcej o czytelniku niż pisarzu.
Karl Bielefeldt
2

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.

Kaerber
źródło
1

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:

my @now = localtime();

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

my $now = localtime();

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.

RemcoGerlich
źródło
Możesz wspomnieć o konkretnym sposobie, w jaki robi to w Perlu - niechlujny ... i może link do dyskusji, jeśli jest dobra lub zła - Używanie niechcianej treści uznanej za szkodliwą w PerlMonks
Przez przypadek mam do czynienia z tworzonym przeze mnie interfejsem API Java Rest. Umożliwiłem, aby jeden zasób zwrócił XML lub JSON. Najniższy typ wspólnego mianownika w tym przypadku to 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, Stringnie mogę automatycznie wygenerować dokumentacji. Z perspektywy czasu / moralności tej historii chciałbym zwrócić jeden typ na metodę.
Daniel Kaplan
Ściśle mówiąc, jest to forma przeciążenia. Innymi słowy, istnieje wiele funkcji federacyjnych i w zależności od szczegółów połączenia wybierana jest jedna lub druga wersja.
Ian
1

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.

function ThingyWrapper(thingy){ //a function constructor (class-like thingy)

    //thingy is effectively private and persistent for ThingyWrapper instances

    if(typeof thingy === 'array'){
        this.alertItems = function(){
            thingy.forEach(function(el){ alert(el); });
        }
    }
    else {
        this.alertItems = function(){
            for(var x in thingy){ alert(thingy[x]); }
        }
    }
}

function gimmeThingy(){
    var
        coinToss = Math.round( Math.random() ),//gives me 0 or 1
        arrayThingy = [1,2,3],
        objectThingy = { item1:1, item2:2, item3:3 }
    ;

    //0 dynamically evaluates to false in JS
    return new ThingyWrapper( coinToss ? arrayThingy : objectThingy );
}

gimmeThingy().alertItems(); //should be same every time except order of numbers - maybe

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.

Erik Reppen
źródło