Dlaczego większość języków programowania obsługuje tylko zwracanie jednej wartości z funkcji? [Zamknięte]

118

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?

M4N
źródło
40
bo możesz zwrócić tablicę ...
Nathan Hayfield
6
Dlaczego więc nie pozwolić tylko na jeden argument? Podejrzewam, że ma to związek z mową. Weź kilka pomysłów, połóż je w jednej rzeczy, którą powrócisz do tego, kto ma szczęście / nieszczęście, by słuchać. Powrót jest prawie jak opinia. „to” jest tym, co robisz z „tymi”.
Erik Reppen
30
Wierzę podejście Pythona jest dość prosty i elegancki: kiedy trzeba zwracać wiele wartości prostu wrócić krotki: 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.
Bakuriu
7
Mogę poinformować, że Matlab ma zmienną liczbę zwracanych wartości. Liczba argumentów wyjściowych jest określana przez sygnaturę wywołującą (np. [a, b] = f()Vs. [a, b, c] = f()) i uzyskiwana wewnątrz fprzez nargout. Nie jestem wielkim fanem Matlaba, ale czasami jest to całkiem przydatne.
gerrit
5
Sądzę, że jeśli większość języków programowania jest zaprojektowana w ten sposób, jest to możliwe. W historii języków programowania powstało kilka bardzo popularnych języków (takich jak Pascal, C, C ++, Java, classic VB), ale dziś jest też dużo innych języków, które mają coraz więcej fanów, które umożliwiają wielokrotny powrót wartości.
Doc Brown

Odpowiedzi:

58

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:

  • Funkcje zwracające wiele wartości trudno jest jednoznacznie nazwać .
  • Łatwo jest pomylić kolejność zwracanych wartości

    (password, username) = GetUsernameAndPassword()  
    

    (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!)

  • Języki OOP mają już lepszą alternatywę dla wielu zwracanych wartości: klas.
    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 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.

BlueRaja - Danny Pflughoeft
źródło
50
Trudno powiedzieć, że zwrócenie krotki zwraca wiele rzeczy. Zwraca jedną krotkę . Kod, który napisałeś, po prostu rozpakowuje go czysto, używając cukru syntaktycznego.
10
@ Lego: Nie widzę różnicy - krotka jest z definicji wieloma wartościami. Co uważasz za „wielokrotne zwracane wartości”, jeśli nie to?
BlueRaja - Danny Pflughoeft
19
To naprawdę mgliste rozróżnienie, ale weź pod uwagę pustą Tuple (). 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 krotki x.
19
... Nigdy nie twierdziłem, że krotek nie można używać do innych rzeczy. Ale argumentowanie, że „Python nie obsługuje wielu zwracanych wartości, obsługuje krotki” jest po prostu wyjątkowo bezcelowe pedantyczne. To wciąż poprawna odpowiedź.
BlueRaja - Danny Pflughoeft
14
Ani krotki ani klasy nie są „wieloma wartościami”.
Andres F.,
54

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 Maybewyjś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 outparametry 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.

Telastyn
źródło
5
@FrustratedWithFormsDesigner - pojawiło się to trochę w ostatnim pytaniu . Mogę liczyć z jednej strony, ile razy chciałem wielu wyników w ciągu 20 lat.
Telastyn
61
Funkcje matematyki i funkcje w większości języków programowania to dwie bardzo różne bestie.
tdammers
17
@tdammers na początku byli bardzo podobni w myślach. Fortran, pascal i tym podobne były pod silnym wpływem matematyki bardziej niż architektury komputerowej.
9
@tdammers - jak to możliwe? Mam na myśli, że w większości języków sprowadza się to do rachunku lambda na końcu - jedno wejście, jedno wyjście, brak efektów ubocznych. Wszystko inne to symulacja / hack na dodatek. Funkcje programowania mogą nie być czyste w tym sensie, że wiele wejść może dawać taki sam wynik, ale dusza tam jest.
Telastyn
16
@ SteveEvers: niefortunnie jest, że nazwa „funkcja” przejęła programowanie imperatywne, zamiast bardziej odpowiedniej „procedury” lub „procedury”. W programowaniu funkcjonalnym funkcja bardziej przypomina funkcje matematyczne.
tdammers
35

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(...)metody Maybe<T>zwracały monadę zamiast używania parametru out. Pomyśl o tym kodzie w F #:

let s = "1"
match tryParse s with
| Some(i) -> // do whatever with i
| None -> // failed to parse

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.

Steven Evers
źródło
1
Ponadto naprawdę chciałbym móc pisać w języku C #: 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.
Grimace of Despair
10
@GrimaceofDespair tak naprawdę chcesz składni destrukcyjnej, a nie wielu zwracanych wartości.
Domenic
2
@warren: Tak. Zobacz tutaj i zauważ, że takie rozwiązanie nie byłoby ciągłe: en.wikipedia.org/wiki/Well-definition
Steven Evers
4
Matematyczne pojęcie dobrze zdefiniowanej nie ma nic wspólnego z liczbą wyników zwracanych przez funkcję. Oznacza to, że wyjścia zawsze będą takie same, jeśli dane wejściowe są takie same. Ściśle mówiąc, funkcje matematyczne zwracają jedną wartość, ale ta wartość jest często krotką. Dla matematyka zasadniczo nie ma różnicy między tym a zwracaniem wielu wartości. Argumenty, że funkcje programowania powinny zwracać tylko jedną wartość, ponieważ to, co robią funkcje matematyczne, nie są zbyt przekonujące.
Michael Siler,
1
@MichaelSiler (zgadzam się z twoim komentarzem) Należy jednak pamiętać, że argument jest odwracalny: „argumenty, że funkcje programu mogą zwrócić wiele wartości, ponieważ mogą zwrócić jedną wartość krotki, również nie są zbyt przekonujące” :)
Andres F.
23

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.

David
źródło
Ach! Bardzo interesujący szczegół. +1!
Steven Evers
To powinna być zaakceptowana odpowiedź. Zwykle ludzie myślą o komputerach docelowych, gdy budują kompilatory. Inną analogią jest to, dlaczego mamy int, float, char / string itp., Ponieważ to jest obsługiwane przez maszynę docelową. Nawet jeśli celem nie jest czysty metal (np. Jvm), nadal chcesz uzyskać przyzwoitą wydajność, nie emulując zbyt wiele.
imel96
26
... Możesz łatwo zdefiniować konwencję wywoływania dla zwracania wielu wartości z funkcji, w prawie taki sam sposób, jak możesz zdefiniować konwencję wywoływania dla przekazywania wielu wartości do funkcji. To nie jest odpowiedź. -1
BlueRaja - Danny Pflughoeft
2
Interesujące byłoby wiedzieć, czy oparte na stosie silniki wykonawcze (JVM, CLR) kiedykolwiek rozważały / dopuszczały wielokrotne zwracane wartości. Powinno to być dość łatwe; dzwoniący musi po prostu podać odpowiednią liczbę wartości, tak jak popycha odpowiednią liczbę argumentów!
Lorenzo Dematté
1
@ David nie, cdecl pozwala na (teoretycznie) nieograniczoną liczbę parametrów (dlatego możliwe są funkcje varargs ) . Chociaż niektóre kompilatory C mogą ograniczać cię do kilkudziesięciu lub stu argumentów na funkcję, co moim zdaniem jest więcej niż rozsądne -_-
BlueRaja - Danny Pflughoeft
18

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óć an Option<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

def foo; return 1, 2, 3 end

one, two, three = foo

one
# => 1

three
# => 3

Pyton

def foo(): return 1, 2, 3

one, two, three = foo()

one
# >>> 1

three
# >>> 3

Scala

def foo = (1, 2, 3)

val (one, two, three) = foo
// => one:   Int = 1
// => two:   Int = 2
// => three: Int = 3

Haskell

let foo = (1, 2, 3)

let (one, two, three) = foo

one
-- > 1

three
-- > 3

Perl6

sub foo { 1, 2, 3 }

my ($one, $two, $three) = foo

$one
# > 1

$three
# > 3
Jörg W Mittag
źródło
1
Myślę, że jednym z aspektów jest to, że w niektórych językach (takich jak Matlab) funkcja może być elastyczna pod względem liczby zwracanych wartości; patrz mój komentarz powyżej . W Matlabie jest wiele aspektów, których nie lubię, ale jest to jedna z niewielu (być może jedynych) funkcji, za którymi tęsknię podczas przesyłania z Matlaba do np. Pythona.
gerrit
1
A co z dynamicznymi językami, takimi jak Python lub Ruby? Załóżmy, że piszę coś takiego jak sortfunkcja 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 flagi sortwzdłuż linii sort(array, nout=2)lub sort(array, indices=True).
gerrit
2
@MikeCellini Nie sądzę. Funkcja może powiedzieć, z iloma argumentami wyjściowymi jest wywoływana ( [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 Matlabs nargoutjest dostępny w czasie wykonywania.
gerrit
1
@gerrit poprawnym rozwiązaniem w Pythonie jest to napisać: sorted, _ = sort(array).
Miles Rout
1
@MilesRout: A sortfunkcja może stwierdzić, że nie musi obliczać wskaźników? Fajnie, nie wiedziałem o tym.
Jörg W Mittag
12

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 + 1myś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. Patrzysz xi decydujesz, że jego wartość wynosi 3 (na przykład), patrzysz na 1, a potem patrzyszx + 1i 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 od foo(). 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 + 1jeszcze raz, ale tym razem wyobraź sobie, że xma dwie wartości {3, 4}, wówczas wartości x + 1to {4, 5}, a wartości x + xto {6, 8}, a może {6, 7, 8} , w zależności od tego, czy jedna ocena może używać wielu wartości dla x. 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.

Geo
źródło
9

Jak zasugerowano w tej odpowiedzi , jest to kwestia wsparcia sprzętowego, chociaż tradycja projektowania języka również odgrywa pewną rolę.

gdy funkcja zwraca, pozostawia wskaźnik do zwracanego obiektu w określonym rejestrze

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:

SP + 8: param 2
SP + 6: param 1
SP + 4: return address
SP + 2: local 2
SP + 0: local 1

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

Daniel C. Sobral
źródło
@gnat Użycie „informować” jak w „w celu zapewnienia niezbędnej jakości” było zamierzone.
Daniel C. Sobral,
nie wahaj się wycofać, jeśli mocno o tym myślisz; na mój odczyt wpływ wpasowuje się tu nieco lepiej: „wpływa na ... w ważny sposób”
komik
1
+1 Jak zaznaczono, rzadka liczba rejestrów wczesnych procesorów w porównaniu z licznym zestawem rejestrów współczesnych procesorów (wykorzystywanych również w wielu ABI, np. X64 abi) może być przełomem w grze i dawniej przekonującymi powodami, aby zwrócić tylko 1 wartość może w dzisiejszych czasach być po prostu historycznym powodem.
BitTickler,
Nie jestem przekonany, że wczesne 8-bitowe mikroprocesory mają duży wpływ na projektowanie języka i na to, co jest potrzebne w konwencjach wywoływania C lub Fortran w dowolnej architekturze. Fortran zakłada, że ​​możesz przekazywać argumenty tablicowe (zasadniczo wskaźniki). Masz już poważne problemy z implementacją normalnego Fortrana na maszynach takich jak 6502, z powodu braku trybów adresowania wskaźnika + indeksu, jak omówiono w odpowiedzi i Dlaczego kompilatory C do Z80 produkują słaby kod? na retrocomputing.SE.
Peter Cordes,
Fortran, podobnie jak C, zakłada również, że możesz przekazać dowolną liczbę argumentów i mieć do nich losowy dostęp oraz dowolną liczbę mieszkańców, prawda? Jak właśnie wyjaśniłeś, nie możesz tego łatwo zrobić na 6502, ponieważ adresowanie względne stosu nie jest niczym, chyba że porzucisz ponowne ustalanie. Możesz umieścić je w pamięci statycznej. Jeśli możesz przekazać dowolną listę argumentów, możesz dodać dodatkowe ukryte parametry dla zwracanych wartości (np. Poza pierwszą), które nie mieszczą się w rejestrach.
Peter Cordes,
7

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:

f :: Int -> (Int, Int)
f x = (x - 1, x + 1)

// Even C++ have tuples - see Boost.Graph for use
std::pair<int, int> f(int x) {
  return std::make_pair(x - 1, x + 1);
}

W powyższym przykładzie ffunkcja 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:

data MyValue = MyValue Int Int

g :: Int -> MyValue
g x = MyValue (x - 1, x + 1)

Wreszcie outparametry mogą być emulowane nawet w językach funkcjonalnych przez IORef. 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):

    int x;
    
    int f(out int y) {
      x = 0;
      y = 1;
      printf("%d\n", x);
    }
    f(out x);
    
  • 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 outparametró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 outparametru (w większości przypadków dozwolone są 2 lub więcej z tych metod).

Maciej Piechotka
źródło
+1 Za wspomnienie o tym, jak funkcjonalne języki sobie z tym radzą w elegancki i bezbolesny sposób.
Andres F.,
1
Technicznie nadal zwracasz jedną wartość: D (Tyle, że pojedyncza wartość rozkłada się na wiele wartości w sposób trywialny).
Thomas Eding,
1
Powiedziałbym, że parametr z prawdziwą semantyką „out” powinien zachowywać się jak kompilator tymczasowy, który jest kopiowany do miejsca docelowego, gdy metoda kończy się normalnie; jeden ze zmienną semantyki „inout” powinien zachowywać się jak kompilator tymczasowy, który jest ładowany ze zmiennej przekazywanej przy wejściu i zapisywany z powrotem przy wyjściu; jeden z semantyką „ref” powinien zachowywać się jak alias. Tak zwane parametry „out” C # są naprawdę parametrami „ref” i zachowują się jako takie.
supercat
1
„Obejście” krotki również nie jest bezpłatne. Blokuje możliwości optymalizacji. Jeśli istniał ABI, który pozwalał na zwracanie N rejestrów zwracanych w rejestrach CPU, kompilator mógłby faktycznie zoptymalizować, zamiast tworzyć instancję krotki i ją konstruować.
BitTickler,
1
@ BitTickler nic nie stoi na przeszkodzie, aby zwrócić n pierwszych pól struktury do przekazania przez rejestry, jeśli kontrolujesz ABI.
Maciej Piechotka,
6

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.

Roman Starkov
źródło
4

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.

ddyer
źródło
3
Czy możesz podać przykład, w którym wiele zwrotów zapewnia wyraźniejszy i / lub bardziej wydajny kod?
Steven Evers,
3
Na przykład w programowaniu C ++ COM wiele funkcji ma jeden [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.
Felix Dombek
W niektórych językach zwracany byłby wektor ze współrzędnymi X i Y, a zwracanie dowolnej użytecznej wartości liczyłoby się jako „sukces” z wyjątkami, być może zawierającymi tę użyteczną wartość, używaną do niepowodzenia.
doppelgreener
3
Dużo czasu kończysz na kodowaniu informacji do wartości zwracanej w nieoczywisty sposób - tj. wartości ujemne to kody błędów, wartości dodatnie to wyniki. Fuj Uzyskując dostęp do tabeli skrótów, zawsze jest bałagan, aby wskazać, czy element został znaleziony, a także zwrócić przedmiot.
ddyer
@SteveEvers Matlab sortfunkcja 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.
gerrit
2

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

x = n + sqrt(y);

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.

James Anderson
źródło
5
Po prostu nie używaj niewłaściwych funkcji. Nie różni się to od „problemu” stawianego przez funkcje, które nie zwracają żadnych wartości lub zwracają wartości nienumeryczne.
ddyer
3
W używanych przeze mnie językach, które oferują wiele zwracanych wartości (na przykład SciLab), pierwsza zwracana wartość jest uprzywilejowana i będzie używana w przypadkach, gdy potrzebna jest tylko jedna wartość. Więc nie ma tam prawdziwego problemu.
Photon
A nawet jeśli nie są, tak jak w przypadku rozpakowywania krotki Pythona, możesz wybrać, który chcesz:foo()[0]
Izkata,
Dokładnie, jeśli funkcja zwraca 2 wartości, to zwracanym typem są 2 wartości, a nie pojedyncza wartość. Język programowania nie powinien czytać w twoich myślach.
Mark E. Haase,
1

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.

użytkownik2202911
źródło
Jeśli języki niskiego poziomu, takie jak C, obsługują wiele zwracanych wartości jako funkcję pierwszej klasy, konwencje wywoływania C obejmują wiele rejestrów wartości zwracanych, w taki sam sposób, w jaki do przekazywania argumentów funkcji całkowitych / wskaźnikowych w x86- używa się maksymalnie 6 rejestrów 64 System V ABI. (W rzeczywistości x86-64 SysV zwraca struktury do 16 bajtów spakowane do pary rejestrów RDX: RAX. Jest to dobre, jeśli struktura została właśnie załadowana i będzie przechowywana, ale kosztuje dodatkowe rozpakowanie w porównaniu do posiadania osobnych elementów w osobnych rejestrach nawet jeśli są węższe niż 64 bity.)
Peter Cordes
Oczywistą konwencją będzie RAX, a następnie reg przechodzące arg. (RDI, RSI, RDX, RCX, R8, R9). Lub w konwencji Windows x64 RCX, RDX, R8, R9. Ale ponieważ C nie ma natywnie wielu wartości zwracanych, konwencje C ABI / wywołujące określają tylko wiele rejestrów zwrotnych dla szerokich liczb całkowitych i niektórych struktur. Zobacz Procedurę wywoływania standardu dla architektury ARM®: 2 oddzielne, ale powiązane wartości zwracane, na przykład użycia szerokiego int, aby uzyskać kompilator w celu wydajnego asm do odbierania 2 wartości zwracanych na ARM.
Peter Cordes,
-1

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.

RickyS
źródło
4
to nie odpowiada na zadane pytanie
komara
@gnat jak dla mnie odpowiada. OTOH trzeba już mieć trochę doświadczenia, aby to zrozumieć, a ta osoba prawdopodobnie nie zadaje pytania ...
Netch
@Netch prawie nie potrzebuje dużego tła, aby stwierdzić, że stwierdzenia takie jak „FORTRAN ... był pierwszym kompilatorem” są totalnym bałaganem. To nawet nie jest złe. Podobnie jak reszta tej „odpowiedzi”
komara
link mówi, że były wcześniejsze próby kompilacji, ale „Zespół FORTRAN kierowany przez Johna Backusa z IBM jest ogólnie uznawany za osobę, która wprowadziła pierwszy kompletny kompilator w 1957 roku”. Pytanie dotyczyło tylko jednego? Tak jak powiedziałem. Była to głównie matematyka i bezwładność. Matematyczna definicja terminu „funkcja” wymaga dokładnie jednej wartości wyniku. To była znajoma forma.
RickyS
-2

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.

Harvey
źródło