Perché la maggior parte dei linguaggi di programmazione ha una parola chiave o una sintassi speciale per la dichiarazione delle funzioni? [chiuso]

39

La maggior parte dei linguaggi di programmazione (sia linguaggi tipizzati dinamicamente che staticamente) ha una parola chiave e / o una sintassi speciali che sembrano molto diverse rispetto alla dichiarazione delle variabili per la dichiarazione delle funzioni. Vedo le funzioni proprio come dichiarare un'altra entità con nome:

Ad esempio in Python:

x = 2
y = addOne(x)
def addOne(number): 
  return number + 1

Perché no:

x = 2
y = addOne(x)
addOne = (number) => 
  return number + 1

Allo stesso modo, in una lingua come Java:

int x = 2;
int y = addOne(x);

int addOne(int x) {
  return x + 1;
}

Perché no:

int x = 2;
int y = addOne(x);
(int => int) addOne = (x) => {
  return x + 1;
}

Questa sintassi sembra un modo più naturale di dichiarare qualcosa (sia una funzione o una variabile) che una parola chiave in meno come def o function in alcune lingue. E, IMO, è più coerente (guardo nello stesso posto per capire il tipo di una variabile o funzione) e probabilmente rende il parser / grammatica un po 'più semplice da scrivere.

So che pochissime lingue usano questa idea (CoffeeScript, Haskell) ma i linguaggi più comuni hanno una sintassi speciale per le funzioni (Java, C ++, Python, JavaScript, C #, PHP, Ruby).

Anche in Scala, che supporta entrambi i modi (e ha un'inferenza di tipo), è più comune scrivere:

def addOne(x: Int) = x + 1

Piuttosto che:

val addOne = (x: Int) => x + 1

IMO, almeno in Scala, questa è probabilmente la versione più facilmente comprensibile ma questo idioma viene raramente seguito:

val x: Int = 1
val y: Int = addOne(x)
val addOne: (Int => Int) = x => x + 1

Sto lavorando sul mio linguaggio giocattolo e mi sto chiedendo se ci sono delle insidie se disegno la mia lingua in questo modo e se ci sono motivi storici o tecnici questo schema non è ampiamente seguito?

    
posta pathikrit 26.09.2014 - 20:50
fonte

12 risposte

48

Penso che la ragione sia che le lingue più popolari derivano o sono influenzate dalla famiglia di lingue C rispetto alle lingue funzionali e dalla loro radice, il calcolo lambda.

E in queste lingue, le funzioni sono non solo un altro valore:

  • In C ++, C # e Java, puoi sovraccaricare le funzioni: puoi avere due funzioni con lo stesso nome, ma firma diversa.
  • In C, C ++, C # e Java, puoi avere valori che rappresentano funzioni, ma i puntatori di funzioni, i funtori, i delegati e le interfacce funzionali sono tutti distinti dalle funzioni stesse. Parte della ragione è che la maggior parte di queste non sono in realtà solo funzioni, sono una funzione insieme a uno stato (mutevole).
  • Le variabili sono modificabili per impostazione predefinita (devi usare const , readonly o final per vietare la mutazione), ma le funzioni non possono essere riassegnate.
  • Da un punto di vista più tecnico, il codice (che è composto da funzioni) e i dati sono separati. In genere occupano diverse parti della memoria e sono accessibili in modo diverso: il codice viene caricato una volta e poi eseguito (ma non letto o scritto), mentre i dati vengono spesso allocati e deallocati costantemente e scritti e letti, ma mai eseguiti.

    E poiché C doveva essere "vicino al metallo", ha senso rispecchiare questa distinzione anche nella sintassi del linguaggio.

  • L'approccio "la funzione è solo un valore" che costituisce la base della programmazione funzionale ha acquisito trazione nei linguaggi comuni solo relativamente di recente, come evidenziato dalla recente introduzione di lambda in C ++, C # e Java (2011, 2007, 2014).

risposta data 26.09.2014 - 22:48
fonte
59

È perché è importante che gli esseri umani riconoscano che le funzioni non sono solo "un'altra entità con nome". A volte ha senso manipolarli come tali, ma sono ancora in grado di essere riconosciuti a colpo d'occhio.

Non importa ciò che il computer pensa della sintassi, poiché una chiazza di caratteri incomprensibile va bene per una macchina da interpretare, ma sarebbe praticamente impossibile per gli esseri umani capire e mantenere.

È davvero la stessa ragione del perché abbiamo while e for loops, switch e if else, ecc, anche se alla fine tutti si riducono a un'istruzione comparativa e di salto. Il motivo è perché è lì a beneficio degli umani che mantengono e comprendono il codice.

Avere le tue funzioni come "un'altra entità con nome" nel modo in cui stai proponendo renderà il tuo codice più difficile da vedere e quindi più difficile da capire.

    
risposta data 26.09.2014 - 21:04
fonte
10

Potresti essere interessato a sapere che, in tempi preistorici, un linguaggio chiamato ALGOL 68 usava una sintassi simile a quella che proponi. Riconoscendo che gli identificatori di funzione sono legati ai valori proprio come gli altri identificatori, in quella lingua potresti dichiarare una funzione (costante) usando la sintassi

function-type name = (parameter-list) result-type : body ;

Concretamente, il tuo esempio dovrebbe leggere

PROC (INT)INT add one = (INT n) INT: n+1;

Riconoscendo la ridondanza in quanto il tipo iniziale può essere letto dal RHS della dichiarazione, e essendo un tipo di funzione inizia sempre con PROC , questo potrebbe (e di solito dovrebbe) essere contratto in

PROC add one = (INT n) INT: n+1;

ma nota che = arriva ancora prima nell'elenco dei parametri. Si noti inoltre che se si desidera una funzione variabile (a cui potrebbe essere assegnato in seguito un altro valore dello stesso tipo di funzione), = deve essere sostituito da := , dando uno dei due

PROC (INT)INT func var := (INT n) INT: n+1;
PROC func var := (INT n) INT: n+1;

Tuttavia in questo caso entrambe le forme sono in realtà delle abbreviazioni; poiché l'identificatore func var designa un riferimento a una funzione generata localmente, la forma completamente espansa sarebbe

REF PROC (INT)INT func var = LOC PROC (INT)INT := (INT n) INT: n+1;

Questa particolare forma sintattica è facile da abituare, ma chiaramente non ha avuto un ampio seguito in altri linguaggi di programmazione. Anche i linguaggi di programmazione funzionali come Haskell preferiscono lo stile f n = n+1 con = dopo l'elenco dei parametri. Immagino che la ragione sia principalmente psicologica; dopotutto anche i matematici non preferiscono spesso, come faccio io, f = n n + 1 su f ( n ) = n + 1.

A proposito, la discussione sopra evidenzia una differenza importante tra variabili e funzioni: le definizioni di funzione di solito associano un nome a uno specifico valore di funzione, che non può essere modificato in seguito, mentre le definizioni di variabile di solito introducono un identificatore con un valore iniziale , ma che può cambiare in seguito. (Non è una regola assoluta, le variabili di funzione e le costanti non funzionali si verificano nella maggior parte dei linguaggi.) Inoltre, nei linguaggi compilati il valore associato in una definizione di funzione è in genere una costante in fase di compilazione, in modo che le chiamate alla funzione possano essere compilato utilizzando un indirizzo fisso nel codice. In C / C ++ questo è addirittura un requisito; l'equivalente di ALGOL 68

PROC (REAL) REAL f = IF mood=sunny THEN sin ELSE cos FI;

non può essere scritto in C ++ senza introdurre un puntatore a funzione. Questo tipo di limitazioni specifiche giustificano l'utilizzo di una sintassi diversa per le definizioni di funzione. Ma dipendono dalla semantica della lingua e la giustificazione non si applica a tutte le lingue.

    
risposta data 27.09.2014 - 12:20
fonte
8

Hai citato Java e Scala come esempi. Tuttavia, hai trascurato un fatto importante: quelle non sono funzioni, quelle sono metodi. I metodi e le funzioni sono fondamentalmente diversi. Le funzioni sono oggetti, i metodi appartengono agli oggetti.

In Scala, che ha sia funzioni che metodi, ci sono le seguenti differenze tra metodi e funzioni:

    I metodi
  • possono essere generici, le funzioni non possono
  • I metodi
  • possono avere no, uno o più elenchi di parametri, le funzioni hanno sempre esattamente un elenco di parametri
  • I metodi
  • possono avere un elenco di parametri impliciti, le funzioni non possono
  • I metodi
  • possono avere parametri opzionali con argomenti predefiniti, le funzioni non possono
  • I metodi
  • possono avere parametri ripetuti, le funzioni non possono
  • I metodi
  • possono avere parametri di tipo nome, le funzioni non possono
  • I metodi
  • possono essere chiamati con argomenti con nome, le funzioni non possono
  • Le funzioni
  • sono oggetti, i metodi non sono

Quindi, la tua sostituzione proposta semplicemente non funziona, almeno per quei casi.

    
risposta data 27.09.2014 - 02:13
fonte
3

Girando la domanda, se non si è interessati a provare a modificare il codice sorgente su una macchina che è estremamente RAM-constrained, o minimizzare il tempo di leggerlo da un floppy disk, cosa c'è di sbagliato nell'usare le parole chiave?

Certamente è più carino leggere x=y+z di store the value of y plus z into x , ma ciò non significa che i caratteri di punteggiatura siano intrinsecamente "migliori" delle parole chiave. Se le variabili i , j e k sono Integer e x è Real , considera le seguenti righe in Pascal:

k := i div j;
x := i/j;

La prima riga eseguirà una divisione intera troncante, mentre la seconda eseguirà una divisione numero reale. La distinzione può essere fatta bene perché Pascal usa div come operatore troncante-intero-divisione, piuttosto che provare a usare un segno di punteggiatura che ha già un altro scopo (divisione in numeri reali).

Anche se ci sono alcuni contesti in cui può essere utile rendere concisa una definizione di funzione (ad esempio un lambda che viene usato come parte di un'altra espressione), le funzioni dovrebbero generalmente distinguersi ed essere visivamente riconoscibili come funzioni. Mentre potrebbe essere possibile rendere la distinzione molto più sottile e utilizzare solo caratteri di punteggiatura, quale sarebbe il punto? Dire Function Foo(A,B: Integer; C: Real): String rende chiaro quale sia il nome della funzione, quali parametri si aspetta e cosa restituisce. Forse si potrebbe accorciare di sei o sette caratteri sostituendo Function con alcuni caratteri di punteggiatura, ma cosa si otterrebbe?

Un'altra cosa da notare è che esiste un gran numero di framework una differenza fondamentale tra una dichiarazione che associa sempre un nome a un metodo particolare oa un particolare binding virtuale e uno che crea una variabile che identifica inizialmente un particolare metodo o vincolante, ma potrebbe essere cambiato in fase di runtime per identificarne un altro. Dato che questi sono concetti semanticamente molto diversi nella maggior parte dei framework procedurali, ha senso che abbiano una sintassi diversa.

    
risposta data 27.09.2014 - 00:13
fonte
3

I motivi a cui posso pensare sono:

  • È più facile per il compilatore sapere cosa dichiariamo.
  • È importante per noi sapere (in modo triviale) se si tratta di una funzione o di una variabile. Le funzioni sono solitamente scatole nere e non ci interessa la loro implementazione interna. Non mi piace l'inferenza di tipo sui tipi restituiti in Scala, perché credo che sia più facile usare una funzione che abbia un tipo restituito: spesso è l'unica documentazione fornita.
  • E la più importante è la strategia following the crowd utilizzata nella progettazione dei linguaggi di programmazione. C ++ è stato creato per rubare i programmatori C e Java è stato progettato in modo da non spaventare i programmatori C ++ e C # per attirare i programmatori Java. Anche C #, che ritengo sia un linguaggio molto moderno con un team straordinario dietro, ha copiato alcuni errori da Java o anche da C.
risposta data 26.09.2014 - 22:14
fonte
2

Bene, la ragione potrebbe essere, che quelle lingue non sono abbastanza funzionali, per così dire. In altre parole, definite piuttosto di rado le funzioni. Pertanto, l'uso di una parola chiave extra è accettabile.

Nelle lingue dell'eredità ML o Miranda, OTOH, definisci le funzioni il più delle volte. Guarda un po 'di codice Haskell, per esempio. È letteralmente per lo più una sequenza di definizioni di funzioni, molte di quelle hanno funzioni locali e funzioni locali di quelle funzioni locali. Quindi, una parola chiave divertente in Haskell sarebbe un errore e richiedere una dichiarazione di asserzione in un linguaggio imperativo per iniziare con assign . L'assegnazione della causa è probabilmente di gran lunga l'affermazione più frequente.

    
risposta data 27.09.2014 - 18:48
fonte
1

Questo potrebbe essere utile su linguaggi dinamici in cui il tipo non è così importante, ma non è leggibile in linguaggi tipizzati statici in cui si desidera sempre conoscere il tipo di variabile. Inoltre, nei linguaggi orientati agli oggetti è molto importante conoscere il tipo di variabile, per sapere quali operazioni supporta.

Nel tuo caso, una funzione con 4 variabili sarebbe:

(int, long, double, String => int) addOne = (x, y, z, s) => {
  return x + 1;
}

Quando guardo l'intestazione della funzione e vedo (x, y, z, s) ma non conosco i tipi di queste variabili. Se voglio conoscere il tipo di z che è il terzo parametro, dovrò guardare l'inizio della funzione e iniziare a contare 1, 2, 3 e poi vedere che il tipo è double . Nel primo modo guardo direttamente e vedo double z .

    
risposta data 26.09.2014 - 21:24
fonte
1

Personalmente, non vedo difetti fatali nella tua idea; potresti scoprire che è più complicato di quanto ti aspettassi per esprimere certe cose usando la tua nuova sintassi, e / o potresti trovare che devi rivederlo (aggiungendo vari casi speciali e altre funzionalità, ecc.), ma dubito che ti troverai bisogno di abbandonare completamente l'idea.

La sintassi che hai proposto sembra più o meno come una variante di alcuni degli stili di notazione a volte usati per esprimere funzioni o tipi di funzioni in matematica. Ciò significa che, come tutte le grammatiche, probabilmente si rivolge più ad alcuni programmatori che ad altri. (Da matematico, mi piace piacermi.)

Tuttavia, dovresti notare che nella maggior parte delle lingue, la sintassi def -style (ad esempio la sintassi tradizionale) si comporta diversamente da un'assegnazione di variabile standard.

  • Nella famiglia C e C++ , le funzioni non sono generalmente trattate come "oggetti", cioè blocchi di dati digitati da copiare e mettere nello stack e quant'altro. (Sì, puoi avere dei puntatori di funzione, ma quelli puntano ancora al codice eseguibile, non ai "dati" nel senso tipico.)
  • Nella maggior parte dei linguaggi OO, esiste una gestione speciale dei metodi (ad esempio funzioni membro); cioè, non sono solo funzioni dichiarate nell'ambito di una definizione di classe. La differenza più importante è che l'oggetto su cui viene chiamato il metodo viene in genere passato come primo parametro implicito al metodo. Python lo rende esplicito con self (che, a proposito, non è in realtà una parola chiave, puoi rendere qualsiasi identificatore valido il primo argomento di un metodo).

Devi considerare se la tua nuova sintassi precisa (e, si spera, intuitivamente) rappresenta ciò che il compilatore o l'interprete sta effettivamente facendo. Può essere utile leggere, diciamo, la differenza tra lambda e metodi in Ruby; questo ti darà un'idea di come il tuo paradigma funzioni-sono-solo-dati differisce dal tipico paradigma OO / procedurale.

    
risposta data 26.09.2014 - 23:46
fonte
1

Per alcune lingue le funzioni non sono valori. In un tale linguaggio per dire che

val f = << i : int  ... >> ;

è una definizione di funzione, mentre

val a = 1 ;

dichiara una costante, confonde perché stai usando una sintassi per indicare due cose.

Altre lingue, come ML, Haskell e Scheme trattano le funzioni come valori di 1a classe, ma forniscono all'utente una sintassi speciale per la dichiarazione delle costanti con valori di funzione. * Si applica la regola che "l'utilizzo riduce il modulo". Cioè se un costrutto è sia comune che dettagliato, dovresti dare una stenografia all'utente. È inelegante dare all'utente due diverse sintassi che significano esattamente la stessa cosa; a volte l'eleganza dovrebbe essere sacrificata per l'utilità.

Se, nella tua lingua, le funzioni sono di 1a classe, allora perché non provare a trovare una sintassi abbastanza concisa da non essere tentato di trovare uno zucchero sintattico?

- Modifica -

Un altro problema che nessuno ha sollevato (ancora) è la ricorsione. Se permetti

{ 
    val f = << i : int ... g(i-1) ... >> ;
    val g = << j : int ... f(i-1) ... >> ;
    f(x)
}

e permetti

{
    val a = 42 ;
    val b = a + 1 ;
    a
} ,

segue che dovresti consentire

{
    val a = b + 1 ; 
    val b = a - 1 ;
    a
} ?

In un linguaggio pigro (come Haskell), non vi è alcun problema qui. In una lingua con essenzialmente nessun controllo statico (come LISP), non vi è alcun problema qui. Ma in un linguaggio bisognoso controllato staticamente, devi stare attento a come sono definite le regole del controllo statico, se vuoi consentire le prime due e proibire l'ultima.

- Fine modifica -

* Si potrebbe sostenere che Haskell non appartiene a questa lista. Fornisce due modi per dichiarare una funzione, ma entrambi sono, in un certo senso, generalizzazioni della sintassi per la dichiarazione di costanti di altri tipi

    
risposta data 27.09.2014 - 23:06
fonte
0

C'è una ragione molto semplice per avere una tale distinzione nella maggior parte delle lingue: c'è bisogno di distinguere valutazione e dichiarazione . Il tuo esempio è buono: perché non mi piacciono le variabili? Bene, le espressioni delle variabili vengono immediatamente valutate.

Haskell ha un modello speciale in cui non vi è alcuna distinzione tra valutazione e dichiarazione, motivo per cui non c'è bisogno di una parola chiave speciale.

    
risposta data 26.09.2014 - 22:48
fonte
0

Le funzioni sono dichiarate in modo diverso da letterali, oggetti, ecc. nella maggior parte delle lingue perché vengono utilizzate in modo diverso, vengono eseguite il debug in modo diverso e pongono diverse potenziali fonti di errore.

Se un riferimento all'oggetto dinamico o un oggetto mutevole viene passato a una funzione, la funzione può cambiare il valore dell'oggetto mentre viene eseguito. Questo tipo di effetto collaterale può rendere difficile seguire ciò che una funzione farà se è annidata all'interno di un'espressione complessa, e questo è un problema comune in linguaggi come C ++ e Java.

Considera il debug di una sorta di modulo del kernel in Java, dove ogni oggetto ha un'operazione toString (). Mentre è probabile che il metodo toString () ripristini l'oggetto, potrebbe essere necessario disassemblare e riassemblare l'oggetto per tradurre il suo valore in un oggetto String. Se si sta tentando di eseguire il debug dei metodi che toString () chiamerà (in uno scenario hook-and-template) per fare il suo lavoro, e accidentalmente evidenzierà l'oggetto nella finestra delle variabili della maggior parte degli IDE, si può bloccare il debugger. Questo perché l'IDE proverà a toString () l'oggetto che chiama il codice stesso nel processo di debug. Nessun valore primitivo fa mai schifo come questo perché il significato semantico dei valori primitivi è definito dal linguaggio, non dal programmatore.

    
risposta data 27.09.2014 - 02:10
fonte

Leggi altre domande sui tag