Solo il concetto di funzioni matematiche giustifica il motivo per cui i metodi devono solo restituire un valore

4

La maggior parte dei linguaggi di programmazione moderni non consente più tipi di ritorno (esclusi scenari come i parametri out di C # o il più recente utilizzo di Tuples ). Questo perché tutte le lingue implementano il concetto di funzioni matematiche, che per un dato numero di parametri di input producono solo un risultato one (per uno non intendo necessariamente un singolo intero ma un singolo output, anche se è un set, un double o qualsiasi altro).

Possiamo superare questo problema con l'uso di objects o in lingue più vecchie (come C) con un struct di più valori. Ho visto anche array utilizzati in determinati casi.

Ma intuitivamente, i programmatori sono stati scoraggiati nel creare un metodo che finirà con che richiede di restituire più di un valore; questa è considerata una cattiva pratica .

Ma con tutti gli aggiornamenti nei linguaggi moderni, solo il concetto sopra menzionato in matematica, giustifica perché i metodi dovrebbero restituire un solo valore ? È solo per la leggibilità e per evitare comuni equivoci tra i programmatori, così come le ottimizzazioni del compilatore?

In altre parole, si tratta solo di una limitazione tecnica o no?

EDIT: non ho esempi da mostrare. Personalmente, credo fermamente che le funzioni dovrebbero restituire solo il valore UNO . Ma come spiegate le nuove funzionalità di C # 7.0 che consentono questo concetto? Come ho detto a @DFord nella sezione commenti, se ragioniamo semplicemente come "la cosa giusta" senza alcuna spiegazione, stiamo effettivamente scomunicando queste caratteristiche e chiamando quelli che le hanno create, idioti.

EDIT 2: mi sono perso nella sezione dei commenti. Ci sono quelli che sono per il concetto di un valore di ritorno e quelli che sono contro esso. Se, secondo alcuni di voi, le funzioni matematiche possono restituire più valori , allora perché le lingue sono state progettate per restituire un valore one e il concetto di cui stiamo discutendo è arrivato più tardi? Posso suddividere i miei pensieri in due categorie:

  • I metodi dovrebbero restituire solo un valore , quindi l'uso di soluzioni intermedie come objects o structs risolve solo un problema che non dovrebbe essere presente in primo luogo
  • Gli sviluppatori devono restituire tutti i valori desiderati , quindi sia il sopra che i parametri out di C # e Tuples sono completamente giustificati e non solo zucchero sintattico.

Nota: È completamente diverso per gli sviluppatori trovare alternative per implementare qualcosa che non è supportato (es. restituire un oggetto) e per i linguaggi di programmazione supportarlo ufficialmente fornendo modi per farlo tramite la loro sintassi - rendendolo così "legittimo".

    
posta Lefteris008 13.12.2017 - 15:23
fonte

5 risposte

5

Il motivo per cui le funzioni restituiscono solo un valore è tecnico. Ha a che fare con il funzionamento dello stack nei processori (e nelle lingue degli assemblatori).

In primo luogo, una (piccola) lezione su come funzionano i processori. Il processore nel tuo computer ha registri in esso. I registri sono piccoli bit di memoria presenti sul chip del processore stesso. Qualsiasi cosa il tuo processore interagisca con qualsiasi cosa sia memorizzata in quei registri. Ma ci sono solo tanti registri nel tuo processore (fisicamente, il circuito può contenere solo così tanti circuiti di registrazione, quindi il tuo processore ne ha solo una manciata a disposizione). Con un numero limitato di registri, il tuo processore dovrà scambiare i valori dentro e fuori da questi registri da e verso la memoria (RAM, cache, ecc. Lo chiameremo semplicemente memoria per il gusto di questa discussione, come tutto ciò che funziona è irrilevante a questo.)

Il tuo programma ha qualcosa in esso chiamato stack. Lo stack è solo uno spazio di memoria in cui il programma può memorizzare i valori che deve essere eseguito durante la chiamata di funzioni (come le variabili locali). Ogni volta che chiami una funzione, uno stack frame viene aggiunto allo stack. Qui è dove si memorizzano cose come argomenti per la funzione successiva, quale istruzione viene dopo quando la funzione ritorna, ecc. Una cosa che succede spesso è che devi salvare lo stato della tua funzione attuale prima di chiamare un'altra funzione. E questo significa andare fino in fondo alla memoria per salvare quelle informazioni.

Dato che usiamo un sacco di funzioni, si attiva e si esce molto dagli stack frame. Parte di questo passaggio consiste nel prendere i valori che dovevamo memorizzare nello stack e riportarli indietro nei registri. E, a livello di processore, questo switch è costoso (l'accesso a materiale nei registri è quasi istantaneo, come misurato in nanosecondi istantanei. Ottenere materiale dalla memoria è più lento di molti ordini di grandezza. Se devi andare su disco, è praticamente un eternità). Quindi vogliamo scambiare il materiale dentro e fuori dai registri il meno possibile.

Quindi, quando una funzione finisce e stiamo cambiando dal suo stack frame, dobbiamo memorizzare quel valore di ritorno da qualche parte. Le nostre scelte sono o un registro o lo stack. La maggior parte delle volte, qualsiasi valore che si ottiene da una funzione verrà utilizzato immediatamente (memorizzato in una variabile, utilizzato in un calcolo, passato a un'altra funzione, ecc.). Quindi le persone brillanti che hanno progettato quei processori hanno deciso di ottimizzare per quel caso e mantenere il valore di ritorno in un registro e ne hanno assegnato uno esplicitamente per quello scopo. Ciò significa che puoi avere solo un valore di ritorno. (Questo semplifica enormemente il modo in cui funziona la lingua degli assemblatori, ma questo è un altro argomento per un'altra volta.)

Il "utilizzare un oggetto per restituire più cose" è una soluzione alternativa a quel problema hardware. Quando restituisci un oggetto, stai semplicemente infilando un puntatore su quell'oggetto in quel registro magico (anche senza capire come funzionano i puntatori, perché questo è già abbastanza lungo). In pratica, il puntatore ti permette di dire "tutte le cose sono memorizzate in questo posto nella memoria, vai a prenderle lì". E il codice compilato sa che la cosa nel registro magico è un puntatore e può gestirlo appropriatamente.

Quindi le lingue sono state progettate per far fronte a questo problema hardware. E dal momento che c'era una soluzione adeguata, probabilmente non valeva la pena di creare la caratteristica della lingua extra. (Questo probabilmente deve anche fare un po 'con il modo in cui le lingue si sono evolute dall'assemblatore ai linguaggi di livello superiore.Essi stavano semplicemente costruendo in cima a quello fornito dall'assemblatore, così che il paradigma continuava. di essi, come C - > C ++ - > Java, C #, ecc.)

Quindi quando vedi una lingua come C # che aggiunge tuple per valori di ritorno multipli, è davvero solo zucchero sintattico coprire l'intera cosa di puntatore a un oggetto nel registro magico. Perché altri linguaggi non lo fanno è puramente una decisione progettuale dei creatori linguistici. Forse non ne vedono il bisogno. Forse pensano che poiché i valori sono probabilmente correlati, un oggetto descriverà meglio questa relazione e ti costringerà a farlo. Forse non vale il tempo e lo sforzo quando ci sono altre funzionalità più utili richieste. E questo sarà diverso per ogni lingua.

    
risposta data 13.12.2017 - 19:16
fonte
3

Sei confuso riguardo al concetto matematico di funzioni che restituiscono un solo valore. Ciò non significa che un input non possa restituire un output aggregato molto grande. Significa che per lo stesso input , otterrai sempre lo stesso output .

In altre parole, se f(1) == Set(1,2,3,...,1000000) , f(1) dovrebbe sempre restituire lo stesso set. Non ottieni un milione di articoli la prima volta che chiami f con un argomento di 1 , ma un set vuoto la seconda volta che lo chiami con lo stesso argomento. Se vuoi un output diverso, devi dare un input diverso.

La maggior parte dei linguaggi di programmazione non ti limita alle vere funzioni matematiche. A causa degli effetti collaterali, puoi scrivere qualcosa del tipo:

var adder = 0

def f(x: Int): Int = {
  adder += 1
  return x + adder
}

Questo valore restituisce 1 la prima volta che chiami f(1) , ma 2 la seconda volta che chiami f(1) , ecc. e quindi non è una vera funzione matematica. Non è la dimensione del valore di ritorno che conta. Può essere una lista infinita. Ciò che importa è che è lo stesso output dato lo stesso input.

Le persone non raccomandano di non restituire valori aggregati dalle funzioni, raccomandano contro le funzioni fare più di una cosa, avendo più di una responsabilità. Questa è una cosa molto diversa.

    
risposta data 13.12.2017 - 18:59
fonte
1

Bene, i processori del mondo reale hanno una quantità più o meno generosa di registri, denominati luoghi in cui memorizzare i valori. E quelli non sono necessariamente uguali: l' ISA generalmente limita alcune (gruppi di) istruzioni a un sottoinsieme di registri per salvare le istruzioni codici e transistor, ma forniscono comunque istruzioni specializzate dove sembra appropriato per la velocità e la densità del codice.

Oltre ai registri, un ISA, o almeno l' ABI su di esso, spesso implementa uno stack, una semplice struttura dati LIFO che può essere abbastanza considerevole.

Infine, c'è sempre la possibilità di passare un puntatore a un'area di uscita, sia manualmente che nascosta.

Tutti questi modi possono e sono usati per restituire valori semplici adattando un registro, valori complessi che riempiono un'intera struttura o anche più risultati. L'ABI definisce i dettagli.

Quindi, se non ci sono restrizioni tecniche che ci limitino al massimo a un valore di ritorno, allora perché poche lingue supportano facilmente più?

Bene, il problema sta nel modo in cui rappresenti ciò che dovrebbe accadere con i singoli risultati, ad esempio in f(g(x, y), z) , è f chiamato con 2 argomenti, o quanti?
E come il modo più semplice, solo disabilitare i ritorni multipli non limita affatto la generalità, perché le relativamente poche volte che sono necessari più risultati, i work-around funzionano bene e non sono necessariamente più prolissi, perché preoccuparsi?

    
risposta data 13.12.2017 - 23:08
fonte
0

Una risposta da un linguaggio di programmazione che supporta in modo nativo più valori di ritorno (Common LISP):

Il concetto LISP

In LISP, una funzione può restituire un elenco di valori arbitrariamente lungo. Quando si utilizza tale chiamata di funzione in un'espressione "normale", viene utilizzato solo il primo valore e gli altri vengono persi. Esistono costrutti speciali che, ad es. consenti a più variabili di ricevere i valori di ritorno multipli.

Un esempio utile è la divisione. La funzione floor quando viene chiamata con 2 argomenti, restituisce la parte intera del quoziente come primo risultato e il resto come il secondo. Questo secondo risultato solitamente viene liberato dal processo di divisione, non è utile molto spesso, ma se lo è, non è necessario eseguire la divisione due volte, è sufficiente prendere entrambi i risultati. E la maggior parte delle volte, quando non ti interessa il resto, puoi utilizzare la funzione floor "normalmente", senza nemmeno pensare al suo secondo valore di risultato.

Implicazioni

La maggior parte delle lingue (incluso LISP) si basa sul concetto di un'espressione che ha un valore. Una chiamata di funzione che dà più valori non rientra in tale concetto, e per me questo è il motivo per cui non è ampiamente supportato.

Alcune lingue usano speciali parametri "out" che ricevono i valori al di fuori del valore dell'espressione.

LISP può essere classificato come restituendo un valore prominente da una funzione, normalmente andando nel valore dell'espressione e offrendo valori aggiuntivi che possono essere catturati utilizzando costrutti speciali. Penso che un concetto come questo potrebbe non essere impossibile da introdurre ad es. Java, consentendo di utilizzare la stessa funzione sia in espressioni a valore singolo che in situazioni a valori multipli, magari con una sintassi come questa:

int quotient1 = Math.floor(3.14, 2);
{ int quotient2, double remainder2 } = Math.floor(3.14, 2);
    
risposta data 13.12.2017 - 18:15
fonte
-1

La realtà è che è una semplificazione, tutto qui.

Al livello più basso, se restituisci zero o un valore, quel valore può essere lasciato in un posto speciale (ad esempio, in un registro utilizzato per i valori restituiti) e quel valore può essere usato o meno, a seconda del desiderio del callee.

Ad un livello superiore, non è necessario ingombrare la sintassi con la sintassi opzionale per ricevere più valori e non è necessario preoccuparsi delle complicazioni delle discrepanze tra il numero fornito e il numero desiderato.

    
risposta data 13.12.2017 - 19:41
fonte

Leggi altre domande sui tag