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