Come suggerito in questa risposta , è una questione di supporto hardware, sebbene anche la tradizione nella progettazione del linguaggio abbia un ruolo.
quando una funzione ritorna lascia un puntatore all'oggetto di ritorno in un registro specifico
Delle tre prime lingue, Fortran, Lisp e COBOL, la prima utilizzava un singolo valore di ritorno come modellato sulla matematica. Il secondo ha restituito un numero arbitrario di parametri nello stesso modo in cui li ha ricevuti: come elenco (si potrebbe anche sostenere che ha solo passato e restituito un singolo parametro: l'indirizzo dell'elenco). Il terzo restituisce zero o un valore.
Queste prime lingue influenzarono molto il design delle lingue che le seguirono, sebbene l'unica che avesse restituito più valori, Lisp, non guadagnò mai molta popolarità.
Quando arrivò il C, mentre era influenzato dai linguaggi precedenti, si concentrava molto sull'uso efficiente delle risorse hardware, mantenendo una stretta associazione tra ciò che faceva il linguaggio C e il codice macchina che lo implementava. Alcune delle sue caratteristiche più antiche, come le variabili "auto" vs "register", sono il risultato di quella filosofia progettuale.
Va anche sottolineato che il linguaggio dell'assemblaggio era ampiamente diffuso fino agli anni '80, quando alla fine iniziò a essere gradualmente eliminato dallo sviluppo tradizionale. Le persone che scrivevano compilatori e creavano lingue conoscevano l' assemblaggio e, per la maggior parte, si attenevano a ciò che funzionava meglio lì.
La maggior parte delle lingue che si sono discostate da questa norma non ha mai trovato molta popolarità e, quindi, non ha mai avuto un ruolo importante influenzando le decisioni dei progettisti del linguaggio (che, ovviamente, sono stati ispirati da ciò che sapevano).
Quindi esaminiamo il linguaggio dell'assemblaggio. Diamo prima un'occhiata al 6502 , un microprocessore del 1975 che è stato notoriamente utilizzato dai microcomputer Apple II e VIC-20. Era molto debole rispetto a quello che era usato nei mainframe e nei minicomputer dell'epoca, sebbene potente rispetto ai primi computer di 20, 30 anni prima, agli albori dei linguaggi di programmazione.
Se guardi la descrizione tecnica, ha 5 registri più alcuni flag da un bit. L'unico registro "completo" era il Program Counter (PC), che indica la successiva istruzione da eseguire. Gli altri registri in cui l'accumulatore (A), due registri "indice" (X e Y) e un puntatore di stack (SP).
Chiamare una subroutine mette il PC nella memoria indicato da SP, quindi diminuisce SP. Il ritorno da una subroutine funziona al contrario. Si può spingere e tirare altri valori nello stack, ma è difficile fare riferimento alla memoria relativa all'SP, quindi è stato difficile scrivere subroutine rientranti . Questa cosa che diamo per scontato, chiamando una subroutine in qualsiasi momento, non era così comune su questa architettura. Spesso viene creato uno "stack" separato in modo che i parametri e l'indirizzo di ritorno della subroutine vengano mantenuti separati.
Se guardi il processore che ha ispirato il 6502, il 6800 , aveva un registro aggiuntivo, il Index Index (IX), largo quanto l'SP, che poteva ricevere il valore dall'SP.
Sulla macchina, chiamare una subroutine rientrante consisteva nello spingere i parametri nello stack, spingere il PC, cambiare il PC nel nuovo indirizzo e quindi la subroutine spingeva le sue variabili locali nello stack . Poiché è noto il numero di variabili e parametri locali, è possibile indirizzarli in relazione allo stack. Ad esempio, una funzione che riceve due parametri e che ha due variabili locali sarebbe simile a questa:
SP + 8: param 2
SP + 6: param 1
SP + 4: return address
SP + 2: local 2
SP + 0: local 1
Può essere chiamato un numero qualsiasi di volte perché tutto lo spazio temporaneo è nello stack.
L' 8080 , utilizzato su TRS-80 e una serie di microcomputer basati su CP / M potrebbe fare qualcosa di simile al 6800, spingendo SP sullo stack e poi facendolo apparire sul suo registro indiretto, HL.
Questo è un modo molto comune di implementare le cose e ha ottenuto un supporto ancora maggiore su processori più moderni, con il puntatore di base che semplifica il dumping di tutte le variabili locali prima di tornare.
Il problema, il, è come si restituisce qualcosa ? I registri dei processori non erano molto numerosi all'inizio, e spesso era necessario usarne alcuni anche per scoprire quale memoria occuparsi. Restituire le cose nello stack sarebbe complicato: dovresti far scoppiare tutto, salvare il PC, spingere i parametri di ritorno (che sarebbero memorizzati nel frattempo?), Quindi spingere di nuovo il PC e tornare indietro.
Quindi, ciò che veniva fatto di solito era riservare un registro per il valore restituito. Il codice chiamante sapeva che il valore di ritorno sarebbe stato in un registro particolare, che avrebbe dovuto essere conservato fino a quando non potesse essere salvato o usato.
Diamo un'occhiata a una lingua che consente più valori di ritorno: Forth. Ciò che Forth fa è mantenere uno stack di ritorno (RP) e uno stack di dati (SP) separati, in modo che tutto ciò che una funzione doveva fare fosse far apparire tutti i suoi parametri e lasciare i valori di ritorno nello stack. Dato che lo stack di ritorno era separato, non si metteva in mezzo.
Come qualcuno che ha imparato il linguaggio assembly e Forth nei primi sei mesi di esperienza con i computer, i valori di ritorno multipli mi sembrano del tutto normali. Operatori come Forth's /mod
, che restituiscono la divisione intera e il resto, sembrano ovvi. D'altra parte, posso facilmente vedere come qualcuno la cui esperienza iniziale sia stata la mente di C trovi strano quel concetto: va contro le loro radicate aspettative di cosa sia una "funzione".
Per quanto riguarda la matematica ... beh, stavo programmando i computer molto prima che avessi mai avuto funzioni nelle lezioni di matematica. V'è una sezione intera di CS e linguaggi di programmazione che è influenzata dalla matematica, ma, poi di nuovo, c'è una intera sezione che non è.
Quindi abbiamo una confluenza di fattori in cui la matematica ha influenzato la progettazione del linguaggio precoce, in cui i vincoli hardware dettavano ciò che era facilmente implementabile e dove i linguaggi popolari influenzavano l'evoluzione dell'hardware (la macchina Lisp e i processori della macchina Forth sono stati i protagonisti di questo processo).