Czy istnieje powód, dla którego funkcje w większości (?) Językach programowania są zaprojektowane do obsługi dowolnej liczby parametrów wejściowych, ale tylko jednej wartości zwracanej?
W większości języków możliwe jest obejście tego ograniczenia, np. Poprzez użycie parametrów zewnętrznych, zwracanie wskaźników lub definiowanie / zwracanie struktur / klas. Ale wydaje się dziwne, że języki programowania nie zostały zaprojektowane do obsługi wielu zwracanych wartości w bardziej „naturalny” sposób.
Czy jest na to wyjaśnienie?
def f(): return (1,2,3)
i wtedy można użyć krotka-rozpakowaniu do „split” krotki:a,b,c = f() #a=1,b=2,c=3
. Nie trzeba tworzyć tablicy i ręcznie wyodrębniać elementów, nie trzeba definiować żadnej nowej klasy.[a, b] = f()
Vs.[a, b, c] = f()
) i uzyskiwana wewnątrzf
przeznargout
. Nie jestem wielkim fanem Matlaba, ale czasami jest to całkiem przydatne.Odpowiedzi:
Niektóre języki, takie jak Python , obsługują natywnie wiele wartości zwracanych, podczas gdy niektóre języki, takie jak C #, obsługują je za pośrednictwem bibliotek podstawowych.
Ale ogólnie, nawet w językach, które je obsługują, wiele wartości zwracanych nie jest często używanych, ponieważ są niechlujne:
Łatwo jest pomylić kolejność zwracanych wartości
(Z tego samego powodu wiele osób unika posiadania zbyt wielu parametrów funkcji; niektórzy nawet twierdzą, że funkcja nigdy nie powinna mieć dwóch parametrów tego samego typu!)
Są silniej typowane, utrzymują zwracane wartości pogrupowane jako jedna jednostka logiczna i zachowują nazwy (właściwości) zwracanych wartości spójne dla wszystkich zastosowań.
Jedynym dogodnym miejscem są języki (np. Python), w których wiele wartości zwracanych z jednej funkcji może być używanych jako wiele parametrów wejściowych do drugiej. Ale przypadki użycia, w których jest to lepszy projekt niż użycie klasy, są dość wąskie.
źródło
()
. Czy to jedno, czy zero rzeczy? Osobiście powiedziałbym to jedno. Mogę przypisaćx = ()
dobrze, tak jak mogę przypisaćx = randomTuple()
. W tym ostatnim, jeśli zwrócona krotka jest pusta, czy nie, nadal mogę przypisać do tej zwróconej krotkix
.Ponieważ funkcje są konstrukcjami matematycznymi, które wykonują obliczenia i zwracają wynik. Rzeczywiście, wiele z tego, co jest „pod maską” kilku języków programowania, koncentruje się wyłącznie na jednym wejściu i jednym wyjściu, przy czym wiele wejść jest tylko cienkim opakowaniem wokół wejścia - a gdy wyjście pojedynczej wartości nie działa, użycie jednego
Maybe
wyjściowa jest spójna struktura (lub krotka lub ) (chociaż ta „pojedyncza” wartość zwrotna składa się z wielu wartości).Nie zmieniło się to, ponieważ programiści stwierdzili, że
out
parametry są niezręcznymi konstrukcjami, które są użyteczne tylko w ograniczonym zestawie scenariuszy. Podobnie jak w przypadku wielu innych rzeczy, nie ma wsparcia, ponieważ nie ma potrzeby / zapotrzebowania.źródło
W matematyce „dobrze zdefiniowana” funkcja to taka, w której dla danego wejścia jest tylko 1 wyjście (na marginesie, możesz mieć tylko pojedyncze funkcje wejściowe i nadal semantycznie uzyskać wiele danych wejściowych za pomocą curry ).
W przypadku funkcji wielowartościowych (np. Kwadratowy pierwiastek dodatniej liczby całkowitej, na przykład) wystarczy zwrócić kolekcję lub sekwencję wartości.
Jeśli chodzi o typy funkcji, o których mówisz (tj. Funkcje, które zwracają wiele wartości, różnych typów ) widzę to nieco inaczej niż wydaje się: wydaje mi się, że potrzebuję / wykorzystuję nasze parametry jako obejście dla lepszego projektu lub bardziej użyteczna struktura danych. Na przykład wolałbym, aby
*.TryParse(...)
metodyMaybe<T>
zwracały monadę zamiast używania parametru out. Pomyśl o tym kodzie w F #:Obsługa kompilatora / IDE / analizy jest bardzo dobra dla tych konstrukcji. To rozwiązałoby wiele „potrzeb” naszych parametrów. Szczerze mówiąc, nie mogę wymyślić żadnych innych metod, które nie byłyby rozwiązaniem.
W przypadku innych scenariuszy - tych, których nie pamiętam - wystarczy zwykła Tuple.
źródło
var (value, success) = ParseInt("foo");
sprawdzany byłby typ kompilacji, ponieważ(int, bool) ParseInt(string s) { }
został zadeklarowany. Ja wiem, można to zrobić z rodzajowych, ale nadal będzie to zrobić za miły dodatek językowej.Oprócz tego, co zostało już powiedziane, kiedy spojrzysz na paradygmaty używane w asemblerze, gdy funkcja zwraca, pozostawia wskaźnik do zwracanego obiektu w określonym rejestrze. Gdyby użyli rejestrów zmiennych / wielokrotnych, funkcja wywołująca nie wiedziałaby, skąd uzyskać zwrócone wartości, gdyby ta funkcja znajdowała się w bibliotece. Utrudnia to łączenie z bibliotekami i zamiast ustawiać dowolną liczbę zwrotnych wskaźników, wybrali jeden. Języki wyższego poziomu nie mają tej samej wymówki.
źródło
Wiele przypadków użycia, w których używałbyś wielu zwracanych wartości w przeszłości, po prostu nie jest już potrzebnych z nowoczesnymi funkcjami językowymi. Chcesz zwrócić kod błędu? Zgłasz wyjątek lub zwróć an
Either<T, Throwable>
. Chcesz zwrócić opcjonalny wynik? Zwróć anOption<T>
. Chcesz zwrócić jeden z kilku rodzajów? ZwróćEither<T1, T2>
lub oznaczony związek.I nawet w przypadkach, w których naprawdę potrzebujesz zwrócić wiele wartości, współczesne języki zwykle obsługują krotki lub jakąś strukturę danych (listę, tablicę, słownik) lub obiekty, a także jakąś formę niszczenia powiązania lub dopasowania wzorca, co sprawia, że pakowanie się twoje wiele wartości w jedną wartość, a następnie przekształcenie jej ponownie w wiele wartości banalne.
Oto kilka przykładów języków, które nie obsługują zwracania wielu wartości. Naprawdę nie rozumiem, jak dodanie obsługi wielu zwracanych wartości uczyniłoby je znacznie bardziej wyrazistymi, aby zrównoważyć koszty nowej funkcji językowej.
Rubin
Pyton
Scala
Haskell
Perl6
źródło
sort
funkcja Matlab :sorted = sort(array)
zwraca tylko posortowaną tablicę, podczas gdy[sorted, indices] = sort(array)
zwraca obie. Jedynym sposobem, w jaki mogę wymyślić w Pythonie, byłoby przekazanie flagisort
wzdłuż liniisort(array, nout=2)
lubsort(array, indices=True)
.[a, b, c] = func(some, thing)
) i odpowiednio działać. Jest to przydatne na przykład, jeśli obliczenie pierwszego argumentu wyjściowego jest tanie, ale obliczenie drugiego jest drogie. Nie znam żadnego innego języka, w którym odpowiednik Matlabsnargout
jest dostępny w czasie wykonywania.sorted, _ = sort(array)
.sort
funkcja może stwierdzić, że nie musi obliczać wskaźników? Fajnie, nie wiedziałem o tym.Prawdziwym powodem, dla którego jedna wartość zwracana jest tak popularna, są wyrażenia używane w tak wielu językach. W dowolnym języku, w którym możesz mieć wyrażenie, tak jak
x + 1
myślisz w kategoriach pojedynczych wartości zwrotnych, ponieważ oceniasz wyrażenie w głowie, dzieląc je na kawałki i decydując o wartości każdego elementu. Patrzyszx
i decydujesz, że jego wartość wynosi 3 (na przykład), patrzysz na 1, a potem patrzyszx + 1
i zebranie wszystkiego razem, aby zdecydować, że wartość całości wynosi 4. Każda część składniowa wyrażenia ma jedną wartość, a nie inną liczbę wartości; taka jest naturalna semantyka wyrażeń, której wszyscy się spodziewają. Nawet gdy funkcja zwraca parę wartości, nadal naprawdę zwraca jedną wartość, która wykonuje zadanie dwóch wartości, ponieważ pomysł funkcji, która zwraca dwie wartości, które nie są w jakiś sposób opakowane w jedną kolekcję, jest zbyt dziwny.Ludzie nie chcą zajmować się alternatywną semantyką, która byłaby wymagana, aby funkcje zwracały więcej niż jedną wartość. Na przykład, w języku opartym na stosie, takim jak Forth , możesz mieć dowolną liczbę zwracanych wartości, ponieważ każda funkcja po prostu modyfikuje górę stosu, popping wejściami i wypychaniem danych do woli. Dlatego Forth nie ma takich wyrażeń, jakie mają normalne języki.
Perl jest innym językiem, który może czasem zachowywać się tak, jakby funkcje zwracały wiele wartości, nawet jeśli zwykle rozważa się zwrócenie listy. Sposób, w jaki listy interpolują w Perlu, daje nam listy,
(1, foo(), 3)
które mogą mieć 3 elementy, jak większość ludzi, którzy nie wiedzą, że Perl mógłby się spodziewać, ale równie łatwo mogą mieć tylko 2 elementy, 4 elementy lub dowolną większą liczbę elementów w zależności odfoo()
. Listy w Perlu są spłaszczone, tak że lista składniowa nie zawsze ma semantykę listy; może to być tylko fragment większej listy.Innym sposobem na to, aby funkcje zwracały wiele wartości, byłoby zastosowanie alternatywnej semantyki wyrażeń, w której każde wyrażenie może mieć wiele wartości, a każda wartość reprezentuje możliwość. Weźmy
x + 1
jeszcze raz, ale tym razem wyobraź sobie, żex
ma dwie wartości {3, 4}, wówczas wartościx + 1
to {4, 5}, a wartościx + x
to {6, 8}, a może {6, 7, 8} , w zależności od tego, czy jedna ocena może używać wielu wartości dlax
. Taki język może zostać zaimplementowany przy użyciu śledzenia wstecznego, podobnie jak Prolog, aby dać wiele odpowiedzi na zapytanie.Krótko mówiąc, wywołanie funkcji jest pojedynczą jednostką syntaktyczną, a pojedyncza jednostka syntaktyczna ma jedną wartość w semantyce wyrażeń, którą wszyscy znamy i kochamy. Każda inna semantyka zmusiłaby cię do dziwnych sposobów robienia rzeczy, takich jak Perl, Prolog lub Forth.
źródło
Jak zasugerowano w tej odpowiedzi , jest to kwestia wsparcia sprzętowego, chociaż tradycja projektowania języka również odgrywa pewną rolę.
Spośród trzech pierwszych języków, Fortran, Lisp i COBOL, pierwszy używał pojedynczej wartości zwracanej, tak jak modelowano na matematyce. Drugi zwrócił dowolną liczbę parametrów w taki sam sposób, w jaki je otrzymał: jako listę (można również argumentować, że przekazał i zwrócił tylko jeden parametr: adres listy). Trzeci zwraca zero lub jedną wartość.
Te pierwsze języki miały duży wpływ na projektowanie następujących po nich języków, chociaż jedyny, który zwrócił wiele wartości, Lisp, nigdy nie zyskał dużej popularności.
Kiedy pojawiło się C, pod wpływem wcześniejszych języków, położyło duży nacisk na wydajne wykorzystanie zasobów sprzętowych, zachowując ścisły związek między tym, co zrobił język C, a kodem maszynowym, który go zaimplementował. Niektóre z jego najstarszych funkcji, takie jak zmienne „auto” vs. „register”, są wynikiem tej filozofii projektowania.
Należy również zauważyć, że język asemblerowy cieszył się dużą popularnością aż do lat 80., kiedy w końcu zaczął stopniowo wycofywać się z głównego nurtu rozwoju. Ludzie, którzy pisali kompilatory i tworzyli języki, znali się na asemblerze i w większości trzymali się tego, co działało najlepiej.
Większość języków, które odbiegały od tej normy, nigdy nie cieszyła się dużą popularnością, a zatem nigdy nie odgrywała istotnej roli wpływającej na decyzje projektantów języków (którzy oczywiście byli zainspirowani tym, co wiedzieli).
Sprawdźmy teraz język asemblera. Spójrzmy najpierw na 6502 , mikroprocesor z 1975 r., Który był powszechnie używany przez mikrokomputery Apple II i VIC-20. Był bardzo słaby w porównaniu z ówczesnymi komputerami mainframe i minikomputerami, choć potężny w porównaniu z pierwszymi komputerami sprzed 20, 30 lat wcześniej, u zarania języków programowania.
Jeśli spojrzysz na opis techniczny, ma on 5 rejestrów plus kilka flag jednobitowych. Jedynym „pełnym” rejestrem był Licznik Programów (PC) - rejestr ten wskazuje na następną instrukcję do wykonania. W pozostałych rejestrach znajduje się akumulator (A), dwa rejestry „indeksowe” (X i Y) oraz wskaźnik stosu (SP).
Wywołanie podprogramu umieszcza komputer w pamięci wskazywanej przez SP, a następnie zmniejsza SP. Powrót z podprogramu działa odwrotnie. Można przesuwać i wyciągać inne wartości ze stosu, ale trudno jest odwoływać się do pamięci w stosunku do SP, więc pisanie podprogramów ponownie było trudne. To, co bierzemy za pewnik, nazywając podprogram w dowolnym momencie, nie jest tak powszechne w tej architekturze. Często tworzy się osobny „stos”, aby parametry i adres zwrotny podprogramu były oddzielone.
Jeśli spojrzysz na procesor, który zainspirował 6502, 6800 , miał on dodatkowy rejestr, Rejestr Indeksów (IX), tak szeroki jak SP, który mógłby otrzymać wartość z SP.
Na komputerze wywołanie podprogramu ponownej rejestracji składało się z wypychania parametrów na stosie, wypychania komputera PC, zmiany komputera na nowy adres, a następnie podprogram podrzucałby lokalne zmienne na stosie . Ponieważ liczba lokalnych zmiennych i parametrów jest znana, ich adresowanie można wykonać w stosunku do stosu. Na przykład funkcja otrzymująca dwa parametry i posiadająca dwie zmienne lokalne wyglądałaby tak:
Można go wywołać dowolną liczbę razy, ponieważ cała przestrzeń tymczasowa znajduje się na stosie.
Model 8080 , zastosowany w TRS-80 i wiele mikrokomputerów opartych na CP / M, może zrobić coś podobnego do 6800, wciskając SP na stos, a następnie umieszczając go w rejestrze pośrednim, HL.
Jest to bardzo powszechny sposób implementacji rzeczy i uzyskał jeszcze większe wsparcie na bardziej nowoczesnych procesorach, a wskaźnik podstawowy sprawia, że zrzucanie wszystkich zmiennych lokalnych przed powrotem jest łatwe.
Problem polega na tym, jak coś zwrócić ? Rejestry procesorów na początku nie były bardzo liczne i często trzeba było użyć niektórych z nich, aby nawet dowiedzieć się, do której pamięci należy się odnieść. Zwracanie rzeczy na stosie byłoby skomplikowane: trzeba by było wszystko pop, zapisać komputer, wcisnąć zwracane parametry (które w międzyczasie byłyby przechowywane?), A następnie ponownie wcisnąć komputer i wrócić.
Tak więc zwykle robiono rezerwowanie jednego rejestru na wartość zwracaną. Kod wywoławczy wiedział, że wartość zwracana będzie w określonym rejestrze, który będzie musiał zostać zachowany, aż będzie można go zapisać lub wykorzystać.
Spójrzmy na język, który pozwala na wiele zwracanych wartości: Dalej. To, co robi Forth, to utrzymywanie osobnego stosu zwrotnego (RP) i stosu danych (SP), tak aby wszystko, co musiała zrobić, to usunąć wszystkie parametry i pozostawić zwracane wartości na stosie. Ponieważ stos zwrotny był osobny, nie przeszkadzał.
Jako osoba, która nauczyła się języka asemblera i Fortha w ciągu pierwszych sześciu miesięcy pracy z komputerami, wiele zwracanych wartości wygląda dla mnie zupełnie normalnie. Operatory takie jak Forth
/mod
, które zwracają podział na liczby całkowite i resztę, wydają się oczywiste. Z drugiej strony mogę łatwo zobaczyć, jak ktoś, kogo wczesne doświadczenie miał umysł C, uznał tę koncepcję za dziwną: jest to sprzeczne z ich zakorzenionymi oczekiwaniami co do „funkcji”.Co do matematyki ... cóż, programowałem komputery na wiele sposobów, zanim jeszcze dostałem się do funkcji na lekcjach matematyki. Tam jest cała sekcja CS i języków programowania, który jest pod wpływem matematyki, ale potem znowu jest cała sekcja, która nie jest.
Mamy więc zbieżność czynników, w których matematyka wpływała na wczesne projektowanie języka, gdzie ograniczenia sprzętowe decydowały o tym, co można łatwo wdrożyć, i gdzie popularne języki miały wpływ na ewolucję sprzętu (maszyny Lisp i procesory maszyn Forth były przeszkodami w tym procesie).
źródło
Znane mi języki funkcjonalne mogą łatwo zwracać wiele wartości dzięki krotkom (w dynamicznie pisanych językach można nawet używać list). Krotki są również obsługiwane w innych językach:
W powyższym przykładzie
f
funkcja zwraca 2 wartości int.Podobnie ML, Haskell, F # itp. Mogą również zwracać struktury danych (wskaźniki są zbyt niskie dla większości języków). Nie słyszałem o nowoczesnym języku GP z takimi ograniczeniami:
Wreszcie
out
parametry mogą być emulowane nawet w językach funkcjonalnych przezIORef
. Istnieje kilka powodów, dla których nie ma natywnego wsparcia dla naszych zmiennych w większości języków:Niejasna semantyka : Czy następująca funkcja wypisuje 0 lub 1? Znam języki, które wypisałyby 0 i te, które wypisałyby 1. Oba mają zalety (zarówno pod względem wydajności, jak i dopasowania modelu mentalnego programisty):
Nie zlokalizowane efekty : Jak w powyższym przykładzie, możesz stwierdzić, że możesz mieć długi łańcuch, a najbardziej wewnętrzna funkcja wpływa na stan globalny. Zasadniczo trudniej jest uzasadnić, jakie są wymagania funkcji i czy zmiana jest legalna. Biorąc pod uwagę, że większość współczesnych paradygmatów próbuje albo zlokalizować efekty (enkapsulacja w OOP), albo wyeliminować skutki uboczne (programowanie funkcjonalne), jest w konflikcie z tymi paradygmatami.
Nadmiarowość : jeśli masz krotki, masz 99% funkcjonalności
out
parametrów i 100% idiomatycznego użytkowania. Jeśli dodasz wskaźniki do miksu, pokryjesz pozostały 1%.Mam problem z nazwaniem jednego języka, który nie może zwrócić wielu wartości za pomocą krotki, klasy lub
out
parametru (w większości przypadków dozwolone są 2 lub więcej z tych metod).źródło
Myślę, że to z powodu wyrażeń , takich jak
(a + b[i]) * c
.Wyrażenia składają się z „pojedynczych” wartości. Funkcja zwracająca wartość w liczbie pojedynczej może więc zostać bezpośrednio użyta w wyrażeniu zamiast dowolnej z czterech zmiennych pokazanych powyżej. Funkcja wielu wyjść jest co najmniej nieco niezdarna w wyrażeniu.
Osobiście uważam, że jest to cecha szczególna w wyjątkowej wartości zwrotu. Można obejść ten problem, dodając składnię określającą, która z wielu zwracanych wartości ma zostać użyta w wyrażeniu, ale z pewnością będzie bardziej niezręczna niż stara, dobra notacja matematyczna, która jest zwięzła i znana wszystkim.
źródło
To trochę komplikuje składnię, ale na poziomie implementacji nie ma dobrego powodu, aby na to nie zezwalać. W przeciwieństwie do niektórych innych odpowiedzi, zwracanie wielu wartości, jeśli są dostępne, prowadzi do wyraźniejszego i wydajniejszego kodu. Nie mogę liczyć, jak często żałowałem, że nie mogę zwrócić X i Y, lub logicznego „sukcesu” i użytecznej wartości.
źródło
[out]
parametr, ale praktycznie wszystkie zwracająHRESULT
(kod błędu). Byłoby całkiem praktyczne po prostu znaleźć tam parę. W językach, które mają dobre wsparcie dla krotek, takich jak Python, jest to używane w wielu kodach, które widziałem.sort
funkcja zwykle układa macierz:sorted_array = sort(array)
. Czasami również potrzebują odpowiednich indeksów:[sorted_array, indices] = sort(array)
. Czasami chcę tylko indeksy:[~, indices]
= sort (array). The function
sort` może faktycznie powiedzieć, ile argumentów wyjściowych jest potrzebnych, więc jeśli potrzebna jest dodatkowa praca dla 2 wyników w porównaniu do 1, może obliczyć te wyniki tylko w razie potrzeby.W większości języków, w których obsługiwane są funkcje, można wywołać funkcję w dowolnym miejscu, w którym można użyć zmiennej tego typu: -
Jeśli funkcja zwraca więcej niż jedną wartość, nie zadziała. Umożliwiają to dynamicznie wpisywane języki, takie jak python, ale w większości przypadków spowoduje to błąd w czasie wykonywania, chyba że uda się wypracować coś sensownego z krotką w środku równania.
źródło
foo()[0]
Chcę tylko skorzystać z odpowiedzi Harveya. Pierwotnie znalazłem to pytanie na stronie z wiadomościami (arstechnica) i znalazłem niesamowite wytłumaczenie, że naprawdę odpowiadam na sedno tego pytania i brakuje mi wszystkich innych odpowiedzi (oprócz Harveya):
Pochodzenie pojedynczego powrotu z funkcji leży w kodzie maszynowym. Na poziomie kodu maszynowego funkcja może zwrócić wartość z rejestru A (akumulatora). Wszelkie inne zwracane wartości będą na stosie.
Język, który obsługuje dwie zwracane wartości, skompiluje go jako kod maszynowy, który zwraca jeden, a drugi umieszcza na stosie. Innymi słowy, druga wartość zwracana i tak byłaby jako parametr wyjściowy.
To tak, jakby pytać, dlaczego przypisanie jest jedną zmienną na raz. Możesz mieć na przykład język, który zezwala na a, b = 1, 2. Ale skończyłoby to na poziomie kodu maszynowego jako a = 1, po którym następuje b = 2.
Istnieje uzasadnienie, że konstrukcje języka programowania są nieco pozorne w stosunku do tego, co faktycznie stanie się, gdy kod zostanie skompilowany i uruchomiony.
źródło
Zaczęło się od matematyki. FORTRAN, nazwany od „Formula Translation” był pierwszym kompilatorem. FORTRAN był i jest zorientowany na fizykę / matematykę / inżynierię.
COBOL, prawie tak samo stary, nie miał wyraźnej wartości zwracanej; Ledwie miał podprogramy. Od tego czasu była to głównie bezwładność.
Na przykład Go ma wiele zwracanych wartości, a wynik jest czystszy i mniej niejednoznaczny niż przy użyciu parametrów „out”. Po niewielkim użyciu jest bardzo naturalny i wydajny. Zalecam rozważenie wielu zwracanych wartości dla wszystkich nowych języków. Może też dla starych języków.
źródło
Prawdopodobnie ma to więcej wspólnego ze spuścizną tworzenia wywołań funkcji w instrukcjach maszyn procesorów oraz z faktem, że wszystkie języki programowania pochodzą od kodu maszynowego: na przykład C -> Montaż -> Maszyna.
Jak procesory wykonują wywołania funkcji
Pierwsze programy zostały napisane w kodzie maszynowym, a następnie w asemblerze. Procesory obsługiwały wywołania funkcji poprzez wypychanie kopii wszystkich bieżących rejestrów na stos. Powrót z funkcji spowoduje usunięcie zapisanego zestawu rejestrów ze stosu. Zwykle jeden rejestr pozostawiono nietknięty, aby funkcja zwracająca mogła zwrócić wartość.
Teraz, dlaczego procesory zostały zaprojektowane w ten sposób ... prawdopodobnie chodziło o ograniczenia zasobów.
źródło