Perché le funzioni in Ocaml / F # non sono ricorsive per impostazione predefinita?


104

Perché le funzioni in F # e Ocaml (e possibilmente in altri linguaggi) non sono ricorsive per impostazione predefinita?

In altre parole, perché i progettisti del linguaggio hanno deciso che era una buona idea farti digitare esplicitamente recuna dichiarazione come:

let rec foo ... = ...

e non dare alla funzione capacità ricorsiva di default? Perché la necessità di un reccostrutto esplicito ?


Risposte:


87

I discendenti francesi e britannici della ML originale fecero scelte diverse e le loro scelte sono state ereditate nel corso dei decenni dalle varianti moderne. Quindi questa è solo eredità, ma influisce sugli idiomi in queste lingue.

Le funzioni non sono ricorsive per impostazione predefinita nella famiglia di linguaggi CAML francese (incluso OCaml). Questa scelta semplifica la sostituzione delle definizioni di funzioni (e variabili) utilizzate letin quei linguaggi perché è possibile fare riferimento alla definizione precedente all'interno del corpo di una nuova definizione. F # ha ereditato questa sintassi da OCaml.

Ad esempio, sostituendo la funzione pquando si calcola l'entropia di Shannon di una sequenza in OCaml:

let shannon fold p =
  let p x = p x *. log(p x) /. log 2.0 in
  let p t x = t +. p x in
  -. fold p 0.0

Notate come l'argomento pdella shannonfunzione di ordine superiore viene sostituito da un altro pnella prima riga del corpo e poi da un altro pnella seconda riga del corpo.

Al contrario, il ramo SML britannico della famiglia di linguaggi ML ha preso l'altra scelta e le funfunzioni associate a SML sono ricorsive per impostazione predefinita. Quando la maggior parte delle definizioni di funzione non necessita dell'accesso alle associazioni precedenti del loro nome di funzione, ciò risulta in un codice più semplice. Tuttavia, le funzioni sostituite sono fatte per usare nomi diversi ( f1, f2ecc.) Che inquinano l'ambito e rendono possibile invocare accidentalmente la "versione" sbagliata di una funzione. E ora c'è una discrepanza tra le funfunzioni legate implicitamente ricorsive e le funzioni legate non ricorsive val.

Haskell rende possibile inferire le dipendenze tra le definizioni limitandole a essere pure. Ciò rende i campioni di giocattoli più semplici, ma ha un costo elevato altrove.

Nota che le risposte fornite da Ganesh e Eddie sono falsi. Hanno spiegato perché i gruppi di funzioni non possono essere collocati all'interno di un gigante let rec ... and ...perché influisce sulla generalizzazione delle variabili di tipo. Questo non ha nulla a che fare con recl'impostazione predefinita in SML ma non in OCaml.


3
Non credo che siano false piste rosse: se non fosse per le restrizioni sull'inferenza, è probabile che interi programmi o moduli sarebbero automaticamente trattati come reciprocamente ricorsivi come fanno la maggior parte degli altri linguaggi. Ciò renderebbe discutibile la specifica decisione progettuale di richiedere o meno "rec".
GS - Chiedo scusa a Monica il

"... automaticamente trattate come reciprocamente ricorsive come fanno la maggior parte delle altre lingue". BASIC, C, C ++, Clojure, Erlang, F #, Factor, Forth, Fortran, Groovy, OCaml, Pascal, Smalltalk e Standard ML non lo fanno.
JD

3
C / C ++ richiede solo prototipi per le definizioni in avanti, che non riguarda in realtà il contrassegno esplicito della ricorsione. Java, C # e Perl hanno certamente la ricorsione implicita. Potremmo entrare in un dibattito senza fine sul significato di "più" e sull'importanza di ciascuna lingua, quindi accontentiamoci di "moltissime" altre lingue.
GS - Chiedo scusa a Monica

3
"C / C ++ richiede solo prototipi per le definizioni in avanti, che non riguarda in realtà il contrassegno esplicito della ricorsione". Solo nel caso speciale di auto-ricorsione. Nel caso generale di ricorsione reciproca, le dichiarazioni anticipate sono obbligatorie sia in C che in C ++.
JD

2
In realtà le dichiarazioni in avanti non sono richieste in C ++ negli ambiti di classe, cioè i metodi statici vanno bene per chiamarsi l'un l'altro senza alcuna dichiarazione.
polkovnikov.ph

52

Una ragione cruciale per l'uso esplicito di ha reca che fare con l'inferenza di tipo Hindley-Milner, che è alla base di tutti i linguaggi di programmazione funzionale di tipo statico (sebbene modificati ed estesi in vari modi).

Se hai una definizione let f x = x, ti aspetteresti che abbia un tipo 'a -> 'ae che sia applicabile a 'atipi diversi in punti diversi. Ma allo stesso modo, se scrivi let g x = (x + 1) + ..., ti aspetteresti xdi essere trattato come un intnel resto del corpo di g.

Il modo in cui l'inferenza Hindley-Milner affronta questa distinzione è attraverso un passaggio di generalizzazione esplicito . In alcuni punti durante l'elaborazione del programma, il sistema dei tipi si interrompe e dice "ok, i tipi di queste definizioni saranno generalizzati a questo punto, in modo che quando qualcuno li usa, tutte le variabili di tipo libere nel loro tipo saranno appena istanziate, e quindi non interferirà con altri usi di questa definizione. "

Risulta che il posto sensato per fare questa generalizzazione è dopo aver verificato un insieme di funzioni reciprocamente ricorsive. Prima, e generalizzerai troppo, portando a situazioni in cui i tipi potrebbero effettivamente scontrarsi. In seguito, e generalizzerai troppo poco, creando definizioni che non possono essere utilizzate con istanze di più tipi.

Quindi, dato che il controllo di tipo ha bisogno di sapere quali insiemi di definizioni sono reciprocamente ricorsivi, cosa può fare? Una possibilità è semplicemente fare un'analisi delle dipendenze su tutte le definizioni in un ambito e riordinarle nei gruppi più piccoli possibili. Haskell lo fa in realtà, ma in linguaggi come F # (e OCaml e SML) che hanno effetti collaterali illimitati, questa è una cattiva idea perché potrebbe riordinare anche gli effetti collaterali. Quindi, invece, chiede all'utente di contrassegnare esplicitamente quali definizioni sono reciprocamente ricorsive e quindi, per estensione, dove dovrebbe verificarsi la generalizzazione.


3
Ehm, no. Il tuo primo paragrafo è sbagliato (stai parlando dell'uso esplicito di "and" e non di "rec") e, di conseguenza, il resto è irrilevante.
JD

5
Non sono mai stato soddisfatto di questo requisito. Grazie per la spiegazione. Un altro motivo per cui Haskell è superiore nel design.
Bent Rasmussen

9
NO!!!! COME È POTUTO ACCADERE?! Questa risposta è chiaramente sbagliata! Si prega di leggere la risposta di Harrop di seguito o di controllare The Definition of Standard ML (Milner, Tofte, Harper, MacQueen - 1997) [p.24]
lambdapower

9
Come ho detto nella mia risposta, il problema dell'inferenza del tipo è uno dei motivi della necessità di rec, anziché essere l'unico motivo. La risposta di Jon è anche una risposta molto valida (a parte il solito commento sprezzante su Haskell); Non credo che i due siano in opposizione.
GS - Chiedo scusa a Monica

16
"il problema dell'inferenza del tipo è uno dei motivi della necessità di rec". Il fatto che OCaml richieda recma SML no è un ovvio contro esempio. Se l'inferenza del tipo fosse il problema per i motivi che descrivi, OCaml e SML non avrebbero potuto scegliere soluzioni diverse come hanno fatto. Il motivo è, ovviamente, quello di cui parli andper rendere Haskell rilevante.
JD

10

Ci sono due ragioni principali per cui questa è una buona idea:

Innanzitutto, se abiliti le definizioni ricorsive, non puoi fare riferimento a un'associazione precedente di un valore con lo stesso nome. Questo è spesso un idioma utile quando stai facendo qualcosa come estendere un modulo esistente.

In secondo luogo, i valori ricorsivi, e in particolare gli insiemi di valori reciprocamente ricorsivi, sono molto più difficili da ragionare, quindi sono definizioni che procedono in ordine, ogni nuova definizione si basa su ciò che è già stato definito. È bello quando si legge un codice di questo tipo avere la garanzia che, ad eccezione delle definizioni esplicitamente contrassegnate come ricorsive, le nuove definizioni possono fare riferimento solo a definizioni precedenti.


4

Alcune ipotesi:

  • letnon viene utilizzato solo per associare funzioni, ma anche altri valori regolari. La maggior parte delle forme di valori non possono essere ricorsive. Sono consentite alcune forme di valori ricorsivi (ad esempio funzioni, espressioni pigre, ecc.), Quindi è necessaria una sintassi esplicita per indicarlo.
  • Potrebbe essere più semplice ottimizzare le funzioni non ricorsive
  • La chiusura creata quando si crea una funzione ricorsiva deve includere una voce che punti alla funzione stessa (quindi la funzione può chiamare se stessa ricorsivamente), il che rende le chiusure ricorsive più complicate delle chiusure non ricorsive. Quindi potrebbe essere bello essere in grado di creare chiusure non ricorsive più semplici quando non è necessaria la ricorsione
  • Consente di definire una funzione in termini di una funzione definita in precedenza o di un valore con lo stesso nome; anche se penso che questa sia una cattiva pratica
  • Maggiore sicurezza? Si assicura che tu stia facendo quello che volevi. es.Se non intendi che sia ricorsivo ma hai accidentalmente usato un nome all'interno della funzione con lo stesso nome della funzione stessa, molto probabilmente si lamenterà (a meno che il nome non sia stato definito prima)
  • Il letcostrutto è simile al letcostrutto in Lisp e Scheme; che non sono ricorsivi. C'è un letreccostrutto separato in Scheme per Let's ricorsivi

"La maggior parte delle forme di valori non possono essere ricorsive. Alcune forme di valori ricorsivi sono consentite (ad esempio funzioni, espressioni pigre, ecc.), Quindi è necessaria una sintassi esplicita per indicarlo". Questo è vero per F # ma non sono sicuro di quanto sia vero per OCaml dove puoi farlo let rec xs = 0::ys and ys = 1::xs.
JD

4

Dato ciò:

let f x = ... and g y = ...;;

Confrontare:

let f a = f (g a)

Con questo:

let rec f a = f (g a)

Il primo ridefinisce fper applicare il precedentemente definito fal risultato dell'applicazione ga a. Quest'ultimo Ridefinisce fa ciclo per sempre applicando ga a, che non è di solito quello che si desidera in ML varianti.

Detto questo, è una cosa in stile designer del linguaggio. Basta andare con esso.


1

Una parte importante è che offre al programmatore un maggiore controllo sulla complessità dei loro ambiti locali. Lo spettro di let, let*e let recoffrono un crescente livello di potenza e costo. let*e let recsono essenzialmente versioni annidate del semplice let, quindi usare uno dei due è più costoso. Questa classificazione ti consente di microgestire l'ottimizzazione del tuo programma in quanto puoi scegliere il livello di let di cui hai bisogno per l'attività da svolgere. Se non hai bisogno della ricorsione o della capacità di fare riferimento a binding precedenti, puoi ripiegare su un semplice let per risparmiare un po 'di prestazioni.

È simile ai predicati di uguaglianza graduata in Scheme. (cioè eq?, eqv?e equal?)

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.