Perché la maggior parte dei linguaggi di programmazione supporta solo la restituzione di un singolo valore da una funzione? [chiuso]


118

C'è un motivo per cui le funzioni nella maggior parte dei (?) Linguaggi di programmazione sono progettate per supportare un numero qualsiasi di parametri di input ma solo un valore di ritorno?

Nella maggior parte delle lingue, è possibile "aggirare" tale limitazione, ad esempio utilizzando parametri fuori, restituendo puntatori o definendo / restituendo strutture / classi. Ma sembra strano che i linguaggi di programmazione non siano stati progettati per supportare più valori di ritorno in modo più "naturale".

C'è una spiegazione per questo?


40
perché puoi restituire un array ...
Nathan Hayfield

6
Quindi perché non consentire solo un argomento? Ho il sospetto che sia legato al discorso. Prendi alcune idee, trasformale in una cosa che ritorni a chiunque sia abbastanza fortunato / sfortunato da ascoltare. Il ritorno è quasi come un'opinione. "questo" è ciò che fai con "questi".
Erik Reppen,

30
Credo che l'approccio di pitone è abbastanza semplice ed elegante: quando si deve restituire più valori semplicemente restituire una tupla: def f(): return (1,2,3)e quindi è possibile utilizzare tuple-dall'imballaggio "split" la tupla: a,b,c = f() #a=1,b=2,c=3. Non è necessario creare un array ed estrarre manualmente gli elementi, non è necessario definire alcuna nuova classe.
Bakuriu,

7
Posso informarti che Matlab ha un numero variabile di valori di ritorno. Il numero di argomenti di output è determinato dalla firma chiamante (ad es. [a, b] = f()Vs. [a, b, c] = f()) e ottenuto al suo interno fda nargout. Non sono un grande fan di Matlab, ma a volte questo è molto utile.
Gerrit,

5
Penso che se la maggior parte dei linguaggi di programmazione sono progettati in questo modo è discutibile. Nella storia dei linguaggi di programmazione, c'erano alcuni linguaggi molto popolari creati in quel modo (come Pascal, C, C ++, Java, VB classico), ma oggi ci sono anche un sacco di altri linguaggi che ottengono sempre più fan che consentono il ritorno multiplo valori.
Doc Brown,

Risposte:


58

Alcuni linguaggi, come Python , supportano nativamente più valori di ritorno, mentre alcuni linguaggi come C # li supportano tramite le loro librerie di base.

Ma in generale, anche nelle lingue che li supportano, più valori di ritorno non vengono utilizzati spesso perché sono sciatti:

  • Le funzioni che restituiscono più valori sono difficili da nominare chiaramente .
  • È facile confondere l'ordine dei valori di ritorno

    (password, username) = GetUsernameAndPassword()  
    

    (Per questo stesso motivo, molte persone evitano di avere troppi parametri in una funzione; alcuni addirittura la spingono fino a dire che una funzione non dovrebbe mai avere due parametri dello stesso tipo!)

  • Le lingue OOP hanno già un'alternativa migliore a più valori di ritorno: le classi.
    Sono più fortemente tipizzati, mantengono i valori restituiti raggruppati come un'unica unità logica e mantengono i nomi delle (proprietà di) valori restituiti coerenti per tutti gli usi.

Il posto in cui sono piuttosto convenienti è nelle lingue (come Python) in cui più valori di ritorno da una funzione possono essere usati come parametri di input multipli in un'altra. Ma i casi d'uso in cui questo è un design migliore rispetto all'utilizzo di una classe sono piuttosto sottili.


50
È difficile dire che restituire una tupla sta restituendo più cose. Sta tornando una tupla . Il codice che hai scritto semplicemente lo decomprime in modo pulito usando dello zucchero sintattico.

10
@Lego: non vedo distinzioni: una tupla è per definizione valori multipli. Cosa considereresti "valori di ritorno multipli", se non questo?
BlueRaja - Danny Pflughoeft

19
È una distinzione davvero confusa, ma considera una Tupla vuota (). È una cosa o zero cose? Personalmente, direi che è una cosa sola. Posso assegnare x = ()bene, proprio come posso assegnare x = randomTuple(). In quest'ultimo caso se la tupla restituita è vuota o no, posso ancora assegnare quella tupla restituita x.

19
... Non ho mai sostenuto che le tuple non potessero essere usate per altre cose. Ma sostenere che "Python non supporta più valori di ritorno, supporta le tuple" è semplicemente estremamente pedante. Questa è ancora una risposta corretta.
BlueRaja - Danny Pflughoeft,

14
Né le tuple né le classi sono "valori multipli".
Andres F.

54

Perché le funzioni sono costrutti matematici che eseguono un calcolo e restituiscono un risultato. In effetti, molto che è "sotto il cofano" di non pochi linguaggi di programmazione si concentra esclusivamente su un input e un output, con più input che sono solo un involucro sottile attorno all'input - e quando un singolo valore non funziona, usando un singolo struttura coesiva (o tupla, o Maybe) essere l'output (sebbene quel valore di ritorno "singolo" sia composto da molti valori).

Questo non è cambiato perché i programmatori hanno scoperto che i outparametri sono costrutti scomodi che sono utili solo in una serie limitata di scenari. Come in molte altre cose, il supporto non è presente perché la necessità / domanda non è presente.


5
@FrustratedWithFormsDesigner - questo è emerso un po 'in una domanda recente . Posso contare da una parte il numero di volte in cui ho desiderato più uscite in 20 anni.
Telastyn,

61
Le funzioni in matematica e le funzioni nella maggior parte dei linguaggi di programmazione sono due bestie molto diverse.
martedì

17
All'inizio @tdammers, erano molto simili nei pensieri. Fortran, pascal e simili sono stati fortemente influenzati dalla matematica più che dall'architettura informatica.

9
@tdammers - come mai? Voglio dire per la maggior parte delle lingue si riduce al calcolo lambda alla fine: un input, un output, nessun effetto collaterale. Tutto il resto è una simulazione / hack per di più. Le funzioni di programmazione potrebbero non essere pure nel senso che più input possono produrre lo stesso output ma lo spirito è lì.
Telastyn,

16
@SteveEvers: è un peccato che il nome "funzione" abbia preso il posto della programmazione imperativa, anziché la "procedura" o la "routine" più appropriate. Nella programmazione funzionale, una funzione è molto più simile alle funzioni matematiche.
martedì

35

In matematica, una funzione "ben definita" è quella in cui esiste un solo output per un dato input (come nota a margine, puoi avere solo funzioni di input singole e ottenere semanticamente più input usando il curry ).

Per funzioni multivalore (ad es. Radice quadrata di un numero intero positivo, ad esempio), è sufficiente restituire una raccolta o una sequenza di valori.

Per i tipi di funzioni di cui stai parlando (ad es. Funzioni che restituiscono più valori, di tipi diversi ) lo vedo in modo leggermente diverso da quello che sembri: vedo la necessità / l'uso di out params come soluzione per un design migliore o un struttura dei dati più utile. Ad esempio, preferirei se i *.TryParse(...)metodi restituissero una Maybe<T>monade invece di usare un parametro out. Pensa a questo codice in F #:

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

Il supporto del compilatore / IDE / analisi è ottimo per questi costrutti. Ciò risolverebbe gran parte del "bisogno" di parametri. Ad essere sincero, non riesco a pensare a nessun altro metodo fuori mano dove questa non sarebbe la soluzione.

Per altri scenari - quelli che non ricordo - è sufficiente una semplice Tupla.


1
Inoltre, mi piacerebbe davvero poter scrivere in C #: var (value, success) = ParseInt("foo");che sarebbe un tipo di tempo di compilazione controllato perché è (int, bool) ParseInt(string s) { }stato dichiarato. Io so questo può essere fatto con i generici, ma ancora vorrei fare una bella aggiunta lingua.
Smorfia di disperazione,

10
@GrimaceofDespair quello che vuoi veramente è la destrutturazione della sintassi, non più valori di ritorno.
Domenic,

2
@warren: Sì. Vedi qui e nota che una tale soluzione non sarebbe continua: en.wikipedia.org/wiki/Well-definition
Steven Evers

4
La nozione matematica di ben definita non ha nulla a che fare con il numero di output restituiti da una funzione. Significa che le uscite saranno sempre le stesse se l'ingresso è lo stesso. A rigor di termini, le funzioni matematiche restituiscono un valore, ma quel valore è spesso una tupla. Per un matematico, non c'è sostanzialmente alcuna differenza tra questo e restituire più valori. Le argomentazioni secondo cui le funzioni di programmazione dovrebbero restituire un solo valore perché è quello che fanno le funzioni matematiche non sono molto convincenti.
Michael Siler,

1
@MichaelSiler (Sono d'accordo con il tuo commento) Ma tieni presente che l'argomento è reversibile: "gli argomenti secondo cui le funzioni del programma possono restituire più valori perché possono restituire un singolo valore di tupla non sono neanche molto convincenti" :)
Andres F.

23

Oltre a quanto già detto quando si osservano i paradigmi utilizzati nell'assembly quando una funzione restituisce, lascia un puntatore all'oggetto di ritorno in un registro specifico. Se usassero registri variabili / multipli la funzione chiamante non saprebbe dove ottenere i valori restituiti se quella funzione fosse in una libreria. Quindi ciò renderebbe difficile il collegamento alle librerie e invece di impostare un numero arbitrario di puntatori restituibili, ne hanno scelto uno. Le lingue di livello superiore non hanno esattamente la stessa scusa.


Ah! Punto di dettaglio molto interessante. +1!
Steven Evers,

Questa dovrebbe essere la risposta accettata. Di solito le persone pensano alle macchine target quando costruiscono compilatori. Un'altra analogia è il motivo per cui abbiamo int, float, char / string, ecc. Perché è ciò che è supportato dalla macchina target. Anche se il target non è bare metal (ad esempio jvm), si desidera comunque ottenere prestazioni decenti non emulando troppo.
imel96,

26
... È possibile definire facilmente una convenzione di chiamata per restituire più valori da una funzione, quasi allo stesso modo è possibile definire una convenzione di chiamata per passare più valori a una funzione. Questa non è una risposta -1
BlueRaja - Danny Pflughoeft

2
Sarebbe interessante sapere se i motori di esecuzione basati su stack (JVM, CLR) hanno mai preso in considerazione / permesso valori multipli di ritorno .. dovrebbe essere abbastanza semplice; il chiamante deve solo far apparire il giusto numero di valori, proprio come spinge il giusto numero di argomenti!
Lorenzo Dematté,

1
@David no, cdecl consente (teoricamente) un numero illimitato di parametri (ecco perché sono possibili le funzioni varargs ) . Sebbene alcuni compilatori C possano limitarti a diverse dozzine o centinaia di argomenti per funzione, che penso sia ancora più che ragionevole -_-
BlueRaja - Danny Pflughoeft

18

Molti casi d'uso in cui avresti usato più valori di ritorno in passato semplicemente non sono più necessari con le funzionalità del linguaggio moderno. Vuoi restituire un codice di errore? Generare un'eccezione o restituire un Either<T, Throwable>. Vuoi restituire un risultato opzionale? Restituisce un Option<T>. Vuoi restituire uno di diversi tipi? Restituisce un Either<T1, T2>sindacato o un tag.

E anche nei casi in cui è veramente necessario restituire più valori, i linguaggi moderni di solito supportano tuple o qualche tipo di struttura di dati (elenco, matrice, dizionario) o oggetti, nonché una qualche forma di associazione destrutturante o di corrispondenza dei modelli, che rende il confezionamento i tuoi valori multipli in un unico valore e poi distruggendolo nuovamente in più valori banali.

Ecco alcuni esempi di lingue che non supportano la restituzione di più valori. Non vedo davvero come l'aggiunta del supporto per più valori di ritorno li renderebbe significativamente più espressivi per compensare il costo di una nuova funzionalità linguistica.

Rubino

def foo; return 1, 2, 3 end

one, two, three = foo

one
# => 1

three
# => 3

Pitone

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

1
Penso che un aspetto sia che, in alcune lingue (come Matlab), una funzione può essere flessibile in quanti valori restituisce; vedi il mio commento sopra . Non ci sono molti aspetti di Matlab che non mi piacciono, ma questa è una delle poche (forse l'unica) caratteristica che mi manca quando eseguo il porting da Matlab, ad esempio Python.
Gerrit,

1
Ma che dire di linguaggi dinamici come Python o Ruby? Supponiamo di scrivere qualcosa come la sortfunzione Matlab : sorted = sort(array)restituisce solo l'array ordinato, mentre [sorted, indices] = sort(array)restituisce entrambi. L'unico modo in cui riesco a pensare in Python sarebbe quello di passare una bandiera sortlungo le linee di sort(array, nout=2)o sort(array, indices=True).
Gerrit,

2
@MikeCellini Non la penso così. Una funzione può dire con quanti argomenti di output viene chiamata la funzione ( [a, b, c] = func(some, thing)) e agire di conseguenza. Ciò è utile, ad esempio, se il calcolo del primo argomento di output è economico ma il calcolo del secondo argomento è costoso. Non ho familiarità con nessun'altra lingua in cui l'equivalente di Matlabs nargoutè disponibile in fase di esecuzione.
Gerrit,

1
@gerrit la soluzione corretta in Python è quello di scrivere questo: sorted, _ = sort(array).
Miglia in rotta il

1
@MilesRout: E la sortfunzione può dire che non è necessario calcolare gli indici? Fantastico, non lo sapevo.
Jörg W Mittag,

12

La vera ragione per cui un singolo valore restituito è così popolare è l'espressione utilizzata in così tante lingue. In qualsiasi lingua in cui puoi avere un'espressione come x + 1stai già pensando in termini di valori di ritorno singoli perché valuta un'espressione nella tua testa spezzandola in pezzi e decidendo il valore di ogni pezzo. Guardi xe decidi che il suo valore è 3 (per esempio) e guardi 1 e poi guardix + 1e metti tutto insieme per decidere che il valore dell'insieme è 4. Ogni parte sintattica dell'espressione ha un valore, non un altro numero di valori; questa è la semantica naturale delle espressioni che tutti si aspettano. Anche quando una funzione restituisce una coppia di valori, restituisce comunque un valore che fa il lavoro di due valori, perché l'idea di una funzione che restituisce due valori che non sono in qualche modo racchiusi in una singola raccolta è troppo strana.

Le persone non vogliono avere a che fare con la semantica alternativa che sarebbe richiesta per avere funzioni che restituiscono più di un valore. Ad esempio, in un linguaggio basato su stack come Forth puoi avere un numero qualsiasi di valori di ritorno perché ogni funzione modifica semplicemente la parte superiore dello stack, facendo scattare gli input e spingendo gli output a piacimento. Ecco perché Forth non ha il tipo di espressioni che hanno le lingue normali.

Perl è un altro linguaggio che a volte può comportarsi come se le funzioni restituissero più valori, anche se di solito è solo considerato il ritorno di un elenco. Il modo in cui le liste "interpolano" in Perl ci fornisce elenchi come quelli (1, foo(), 3)che potrebbero avere 3 elementi come la maggior parte delle persone che non sanno che Perl si aspetterebbe, ma potrebbero altrettanto facilmente avere solo 2 elementi, 4 elementi o un numero maggiore di elementi a seconda di foo(). Gli elenchi in Perl sono appiattiti in modo che un elenco sintattico non abbia sempre la semantica di un elenco; può essere semplicemente un pezzo di un elenco più ampio.

Un altro modo per ottenere funzioni che restituiscono più valori sarebbe quello di avere una semantica di espressione alternativa in cui qualsiasi espressione può avere più valori e ogni valore rappresenta una possibilità. Riprendilo x + 1, ma questa volta immagina che xabbia due valori {3, 4}, quindi i valori di x + 1sarebbero {4, 5} e i valori di x + xsarebbero {6, 8} o forse {6, 7, 8} , a seconda che a una valutazione sia consentito utilizzare più valori per x. Un linguaggio del genere potrebbe essere implementato usando il backtracking come Prolog usa per dare risposte multiple a una query.

In breve, una chiamata di funzione è una singola unità sintattica e una singola unità sintattica ha un singolo valore nell'espressione semantica che tutti conosciamo e amiamo. Qualsiasi altra semantica ti costringerebbe a strani modi di fare le cose, come Perl, Prolog o Forth.


9

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


@gnat L'uso di "informare" come in "per fornire la qualità essenziale" era intenzionale.
Daniel C. Sobral,

sentiti libero di tornare indietro se ti senti fortemente al riguardo; secondo me l' influenza della lettura si adatta leggermente meglio qui: "colpisce ... in modo importante"
moscerino

1
+1 Come sottolineato, il conteggio dei registri sparsi delle prime CPU rispetto a un set di registri abbondante di CPU contemporanee (utilizzato anche in molte ABI, ad esempio x64 abi) potrebbe essere un punto di svolta e le ragioni precedentemente convincenti di restituire solo 1 valore oggi potrebbe essere solo una ragione storica.
BitTickler,

Non sono convinto che le prime micro CPU a 8 bit abbiano molta influenza sulla progettazione del linguaggio e su quali cose si presume siano necessarie nelle convenzioni di chiamata C o Fortran in qualsiasi architettura. Fortran presume che tu possa passare argomenti di matrice (essenzialmente puntatori). Quindi già hai grossi problemi di implementazione per il normale Fortran su macchine come 6502, a causa della mancanza di modalità di indirizzamento per pointer + index, come discusso nella tua risposta e in Perché i compilatori da C a Z80 producono codice scadente? su retrocomputing.SE.
Peter Cordes,

Fortran, come C, presume anche che tu possa passare un numero arbitrario di argomenti, e avere un accesso casuale a loro e una quantità arbitraria di gente del posto, giusto? Come hai appena spiegato, non puoi farlo facilmente su 6502 perché l'indirizzamento relativo allo stack non è una cosa, a meno che tu non abbandoni il rientro. Puoi inserirli nella memoria statica. Se riesci a passare un elenco arg arbitrario, puoi aggiungere parametri nascosti extra per valori di ritorno (ad es. Oltre il primo) che non rientrano nei registri.
Peter Cordes,

7

I linguaggi funzionali che conosco possono restituire facilmente più valori attraverso l'uso delle tuple (in linguaggi tipicamente dinamici, puoi persino usare gli elenchi). Le tuple sono supportate anche in altre lingue:

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);
}

Nell'esempio sopra, fè una funzione che restituisce 2 ints.

Allo stesso modo, ML, Haskell, F #, ecc., Possono anche restituire strutture di dati (i puntatori sono di livello troppo basso per la maggior parte delle lingue). Non ho sentito parlare di un moderno linguaggio GP con una tale limitazione:

data MyValue = MyValue Int Int

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

Infine, i outparametri possono essere emulati anche in linguaggi funzionali tramite IORef. Esistono diversi motivi per cui non esiste un supporto nativo per le variabili out nella maggior parte delle lingue:

  • Semantica poco chiara : la seguente funzione stampa 0 o 1? Conosco i linguaggi che stamperebbero 0 e quelli che stamperebbero 1. Ci sono vantaggi per entrambi (sia in termini di prestazioni, sia in abbinamento al modello mentale del programmatore):

    int x;
    
    int f(out int y) {
      x = 0;
      y = 1;
      printf("%d\n", x);
    }
    f(out x);
    
  • Effetti non localizzati : come nell'esempio sopra, puoi scoprire che puoi avere una lunga catena e la funzione più interna influisce sullo stato globale. In generale, rende più difficile ragionare su quali siano i requisiti della funzione e se la modifica è legale. Dato che la maggior parte dei paradigmi moderni tenta di localizzare gli effetti (incapsulamento in OOP) o di eliminare gli effetti collaterali (programmazione funzionale), è in conflitto con quei paradigmi.

  • Essere ridondanti : se hai tuple, hai il 99% della funzionalità dei outparametri e il 100% dell'uso idiomatico. Se aggiungi puntatori al mix, copri il restante 1%.

Ho difficoltà a nominare una lingua che non può restituire più valori usando una tupla, una classe o un outparametro (e nella maggior parte dei casi sono consentiti 2 o più di questi metodi).


+1 Per menzionare come i linguaggi funzionali gestiscono questo in modo elegante e indolore.
Andres F.

1
Tecnicamente stai ancora restituendo un singolo valore: D (È solo che questo singolo valore è banale da scomporre in più valori).
Thomas Eding,

1
Direi che un parametro con una semantica "out" reale dovrebbe comportarsi come un temporaneo del compilatore che viene copiato nella destinazione quando un metodo esce normalmente; uno con la variabile semantica "inout" dovrebbe comportarsi come un compilatore temporaneo che viene caricato dalla variabile pass-in all'entrata e riscritto all'uscita; uno con la semantica "ref" dovrebbe comportarsi come un alias. I cosiddetti parametri "out" di C # sono in realtà parametri "ref" e si comportano come tali.
supercat

1
Anche la tupla "soluzione alternativa" non è gratuita. Blocca le opportunità di ottimizzazione. Se esisteva un ABI che consentiva di restituire N valori di ritorno nei registri della CPU, il compilatore poteva effettivamente ottimizzare, invece di creare un'istanza tupla e costruirla.
BitTickler

1
@BitTickler non c'è nulla che impedisca di restituire i primi n campi di struct che devono essere passati dai registri se controlli l'ABI.
Maciej Piechotka,

6

Penso sia a causa di espressioni come (a + b[i]) * c.

Le espressioni sono composte da valori "singolari". Una funzione che restituisce un valore singolare può quindi essere utilizzata direttamente in un'espressione, al posto di una delle quattro variabili mostrate sopra. Una funzione multi-output è almeno un po ' goffa in un'espressione.

Personalmente ritengo che questa sia la cosa speciale di un valore di ritorno singolare. Potresti aggirare questo aggiungendo la sintassi per specificare quale dei multipli valori di ritorno vuoi usare in un'espressione, ma è destinato ad essere più goffo della buona vecchia notazione matematica, che è concisa e familiare a tutti.


4

Ciò complica un po 'la sintassi, ma a livello di implementazione non ci sono buoni motivi per non permetterlo. Contrariamente ad alcune delle altre risposte, la restituzione di più valori, ove disponibili, porta a un codice più chiaro ed efficiente. Non posso contare con quale frequenza avrei desiderato poter restituire una X e una Y, oppure un valore booleano "successo" e un valore utile.


3
Potresti fornire un esempio di dove più resi forniscono codice più chiaro e / o più efficiente?
Steven Evers,

3
Ad esempio, nella programmazione COM C ++, molte funzioni hanno un [out]parametro, ma praticamente tutte restituiscono un HRESULT(codice di errore). Sarebbe abbastanza pratico prenderne un paio lì. In lingue che hanno un buon supporto per le tuple, come Python, questo viene usato in un sacco di codice che ho visto.
Felix Dombek,

In alcune lingue, restituiresti un vettore con le coordinate X e Y e restituire qualsiasi valore utile conterrebbe come "successo" con le eccezioni, possibilmente portando quel valore utile, essendo utilizzato per il fallimento.
doppelgreener,

3
Molte volte finisci per codificare le informazioni nel valore di ritorno in modi non ovvi - vale a dire; i valori negativi sono codici di errore, i valori positivi sono risultati. Che schiffo. Accedendo a una tabella hash, è sempre disordinato indicare se l'oggetto è stato trovato e restituire anche l'oggetto.
Ddyer

@SteveEvers La Matlab sortfunzione ordina un array normalmente: sorted_array = sort(array). A volte ho anche bisogno gli indici corrispondenti: [sorted_array, indices] = sort(array). A volte voglio solo gli indici: [~, indices]= sort (array) . The function sort` in realtà può dire quanti argomenti di output sono necessari, quindi se è necessario un lavoro aggiuntivo per 2 output rispetto a 1, può calcolare tali output solo se necessario.
Gerrit,

2

Nella maggior parte delle lingue in cui sono supportate le funzioni è possibile utilizzare una chiamata di funzione ovunque sia possibile utilizzare una variabile di quel tipo:

x = n + sqrt(y);

Se la funzione restituisce più di un valore, questa non funzionerà. Linguaggi tipicamente dinamici come python ti permetteranno di farlo, ma, nella maggior parte dei casi, genererà un errore di runtime a meno che non riesca a capire qualcosa di sensato da fare con una tupla nel mezzo di un'equazione.


5
Basta non usare funzioni inappropriate. Questo non è diverso dal "problema" posto dalle funzioni che non restituiscono valori o restituiscono valori non numerici.
lugubre il

3
Nelle lingue che ho usato che offrono più valori di ritorno (SciLab, ad esempio), il primo valore di ritorno è privilegiato e verrà utilizzato nei casi in cui è necessario un solo valore. Quindi nessun problema reale lì.
The Photon,

E anche quando non lo sono, come con la tupla disimballata di Python, puoi selezionare quale vuoi:foo()[0]
Izkata,

Esatto, se una funzione restituisce 2 valori, il suo tipo restituito è 2 valori, non un singolo valore. Il linguaggio di programmazione non dovrebbe leggerti nella mente.
Mark E. Haase,

1

Voglio solo basarmi sulla risposta di Harvey. Inizialmente ho trovato questa domanda su un sito di notizie (arstechnica) e ho trovato una spiegazione sorprendente che ritengo davvero risponda al nocciolo di questa domanda e manchi da tutte le altre risposte (tranne Harvey):

L'origine del ritorno singolo dalle funzioni risiede nel codice macchina. A livello di codice macchina, una funzione può restituire un valore nel registro A (accumulatore). Qualsiasi altro valore restituito sarà nello stack.

Un linguaggio che supporta due valori di ritorno lo compilerà come codice macchina che restituisce uno e inserisce il secondo nello stack. In altre parole, il secondo valore restituito finirebbe comunque come parametro out.

È come chiedere perché l'assegnazione è una variabile alla volta. Potresti avere una lingua che ha permesso a, b = 1, 2 per esempio. Ma finirebbe a livello di codice macchina essendo a = 1 seguito da b = 2.

C'è qualche logica nell'avere costrutti del linguaggio di programmazione che assomigliano a ciò che accadrà effettivamente quando il codice viene compilato ed eseguito.


Se linguaggi di basso livello come C supportano più valori di ritorno come funzionalità di prima classe, le convenzioni di chiamata C includeranno più registri di valore di ritorno, allo stesso modo in cui vengono usati fino a 6 registri per passare argomenti arg / integer / pointer in x86- 64 System V ABI. (In effetti x86-64 SysV restituisce le strutture fino a 16 byte impacchettati nella coppia di registri RDX: RAX. Questo è utile se lo struct è stato appena caricato e verrà archiviato, ma costa un ulteriore disimballaggio rispetto al fatto di avere membri separati in registri separati anche se sono più stretti di 64 bit.)
Peter Cordes,

La convenzione ovvia sarebbe RAX, quindi i reg di passaggio arg. (RDI, RSI, RDX, RCX, R8, R9). O nella convenzione di Windows x64, RCX, RDX, R8, R9. Ma poiché C non ha nativamente più valori di ritorno, le convenzioni di chiamata / ABI C specificano solo più registri di ritorno per numeri interi ampi e alcune strutture. Vedere la procedura standard di chiamata per l'architettura ARM®: 2 valori di ritorno separati ma correlati per un esempio di utilizzo di un int ampio per fare in modo che il compilatore diventi efficiente per ricevere 2 valori di ritorno su ARM.
Peter Cordes,

-1

È iniziato con la matematica. FORTRAN, chiamato per "Formula Translation" è stato il primo compilatore. FORTRAN era ed è orientato alla fisica / matematica / ingegneria.

COBOL, quasi altrettanto vecchio, non aveva un valore di ritorno esplicito; Aveva a malapena subroutine. Da allora è stata per lo più inerzia.

Go , ad esempio, ha più valori di ritorno e il risultato è più pulito e meno ambiguo rispetto all'utilizzo dei parametri "out". Dopo un po 'di utilizzo, è molto naturale ed efficiente. Raccomando di considerare più valori di ritorno per tutte le nuove lingue. Forse anche per le vecchie lingue.


4
questo non risponde alla domanda posta
moscerino il

@gnat come per me risponde. OTOH uno deve già aver bisogno di un po 'di background per capirlo, e quella persona probabilmente non farà la domanda ...
Netch

@Netch uno difficilmente ha bisogno di molto background per capire che affermazioni come "FORTRAN ... was the first compilator" sono un casino totale. Non è nemmeno sbagliato. Proprio come il resto di questa "risposta"
moscerino il

link dice che ci sono stati tentativi precedenti di compilatori, ma che "il team FORTRAN guidato da John Backus presso IBM è generalmente accreditato per aver introdotto il primo compilatore completo nel 1957". La domanda posta era perché solo uno? Come ho detto. Era principalmente matematica e inerzia. La definizione matematica del termine "Funzione" richiede esattamente un valore di risultato. Quindi era una forma familiare.
RickyS,

-2

Probabilmente ha più a che fare con l'eredità di come vengono fatte le chiamate di funzione nelle istruzioni della macchina del processore e il fatto che tutti i linguaggi di programmazione derivano dal codice della macchina: ad esempio, C -> Assembly -> Machine.

In che modo i processori eseguono le chiamate di funzione

I primi programmi sono stati scritti in codice macchina e successivamente assemblati. I processori supportano le chiamate di funzione spingendo una copia di tutti i registri correnti nello stack. Ritornando dalla funzione, il gruppo di registri salvato verrà estratto dallo stack. Di solito un registro è rimasto invariato per consentire alla funzione di ritorno di restituire un valore.

Ora, sul motivo per cui i processori sono stati progettati in questo modo ... era probabilmente una questione di vincoli di risorse.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.