Valori del "ricordare" nella programmazione funzionale


20

Ho deciso di assumermi il compito di apprendere la programmazione funzionale. Finora è stato un vero spasso, e ho "visto la luce" per così dire. Sfortunatamente, in realtà non conosco nessun programmatore funzionale da cui possa rimandare le domande. Presentazione di Stack Exchange.

Sto seguendo un corso di sviluppo web / software, ma il mio istruttore non ha familiarità con la programmazione funzionale. Sta bene con me che lo uso e mi ha appena chiesto di aiutarlo a capire come funziona in modo che possa leggere meglio il mio codice.

Ho deciso che il modo migliore per farlo sarebbe quello di illustrare una semplice funzione matematica, come aumentare un valore a una potenza. In teoria, potrei facilmente farlo con una funzione predefinita, ma ciò vanificherebbe lo scopo di un esempio.

Comunque, ho qualche difficoltà a capire come mantenere un valore. Dato che si tratta di una programmazione funzionale, non posso cambiare variabile. Se dovessi scrivere questo codice in modo imperativo, sarebbe simile a questo:

(Quanto segue è tutto pseudocodice)

f(x,y) {
  int z = x;
  for(int i = 0, i < y; i++){
    x = x * z;
  }
  return x;
}

Nella programmazione funzionale, non ero sicuro. Questo è quello che mi è venuto in mente:

f(x,y,z){
  if z == 'null',
    f(x,y,x);
  else if y > 1,
    f(x*z,y-1,z);
  else
    return x;
}

È giusto? Devo mantenere un valore, zin entrambi i casi, ma non ero sicuro di come farlo nella programmazione delle funzioni. In teoria, il modo in cui l'ho fatto funziona, ma non ero sicuro che fosse "giusto". C'è un modo migliore per farlo?


32
Se vuoi che il tuo esempio sia preso sul serio, falla risolvere un problema pratico piuttosto che un problema di matematica. È una specie di cliché tra gli sviluppatori che "tutto FP è utile per risolvere problemi matematici" e se il tuo esempio è Yet Another Mathematical Function stai solo rinforzando lo stereotipo, invece di rendere utile ciò che stai facendo.
Mason Wheeler,

12
Il tuo tentativo è in realtà abbastanza buono quando si tiene conto delle considerazioni del mondo reale. Tutte le tue chiamate ricorsive sono chiamate di coda , cioè la funzione non fa nient'altro dopo averle chiamate. Ciò significa che un compilatore o un interprete che lo supporta può ottimizzarli in modo che la funzione ricorsiva utilizzi una quantità fissa di memoria dello stack, anziché una quantità proporzionale a y.
8

1
Grazie mille per il supporto! Sono ancora molto nuovo in questo, quindi il mio pseudocodice non è perfetto. @MasonWheeler Immagino, in questo caso il mio codice non è pensato per essere preso sul serio. Sto ancora imparando, e la ragione per cui amo FP è perché è matematica-y. Il punto del mio esempio è spiegare al mio insegnante perché sto usando FP. Non capisce davvero di cosa si tratta, quindi questo sembrava un buon modo per mostrargli i vantaggi.
Ucenna,

5
In quale lingua prevedi di scrivere il codice? Non provare a usare uno stile non adatto alla lingua che stai utilizzando.
Carsten S,

Risposte:


37

Innanzitutto, congratulazioni per "vedere la luce". Hai reso il mondo del software un posto migliore ampliando i tuoi orizzonti.

In secondo luogo, onestamente non c'è modo che un professore che non capisca la programmazione funzionale sia in grado di dire qualcosa di utile sul tuo codice, se non i commenti banali come "il rientro sembra off". Ciò non è sorprendente in un corso di sviluppo Web, poiché la maggior parte dello sviluppo Web viene eseguita utilizzando HTML / CSS / JavaScript. A seconda di quanto effettivamente ti interessa apprendere lo sviluppo web, potresti voler impegnarti per apprendere gli strumenti che il tuo professore sta insegnando (per quanto doloroso possa essere - lo so per esperienza).

Per rispondere alla domanda posta: se il codice imperativo utilizza un ciclo, è probabile che il codice funzionale sia ricorsivo.

(* raises x to the power of y *)
fun pow (x: real) (y: int) : real = 
    if y = 1 then x else x * (pow x (y-1))

Si noti che questo algoritmo è in realtà più o meno identico al codice imperativo. In effetti, si potrebbe considerare il ciclo sopra come zucchero sintattico per processi iterativi ricorsivi.

Come nota za margine, in realtà non è necessario un valore nel codice imperativo o funzionale. Avresti dovuto scrivere la tua funzione imperativa in questo modo:

def pow(x, y):
    var ret = 1
    for (i = 0; i < y; i++)
         ret = ret * x
    return ret

piuttosto che cambiare il significato della variabile x.


Il tuo ricorsivo pownon è del tutto corretto. Così com'è, pow 3 3ritorna 81, invece di 27. Dovrebbe essereelse x * pow x (y-1).
8

3
Spiacenti, scrivere il codice corretto è difficile :) Risolto, e ho anche aggiunto annotazioni di tipo. @Ucenna Dovrebbe essere SML, ma non lo uso da un po ', quindi potrei avere la sintassi leggermente sbagliata. Ci sono troppi modi dannati per dichiarare una funzione, non riesco mai a ricordare la parola chiave giusta. Oltre alle modifiche alla sintassi, il codice è identico in JavaScript.
gardenhead

2
@jwg Javascript ha alcuni aspetti funzionali: le funzioni possono definire funzioni nidificate, funzioni di ritorno e accettare funzioni come parametri; supporta chiusure con ambito lessicale (nessun ambito dinamico lisp però). Sta alla disciplina del programmatore astenersi dal cambiare stato e mutare i dati.
Kasper van den Berg,

1
@jwg Non esiste una definizione concordata di linguaggio "funzionale" (né di "imperativo", "orientato agli oggetti" o "dichiarativo"). Cerco di astenermi dall'utilizzare questi termini ogni volta che è possibile. Ci sono troppe lingue sotto il sole per essere classificate in quattro gruppi ordinati.
gardenhead,

1
La popolarità è una terribile metrica, motivo per cui ogni volta che qualcuno menziona quel linguaggio o lo strumento X deve essere migliore perché è così ampiamente usato, so che continuare l'argomento sarebbe inutile. Conosco personalmente più la famiglia di lingue ML che Haskell. Ma non sono nemmeno sicuro che sia vero; suppongo che la stragrande maggioranza degli sviluppatori non abbia provato Haskell in primo luogo.
gardenhead,

33

Questo è davvero solo un addendum alla risposta di gardenhead, ma vorrei sottolineare che c'è un nome per lo schema che stai vedendo: pieghevole.

Nella programmazione funzionale, una piega è un modo per combinare una serie di valori che "ricorda" un valore tra ogni operazione. Valuta di aggiungere un elenco di numeri in modo imperativo:

def sum_all(xs):
  total = 0
  for x in xs:
    total = total + x
  return total

Prendiamo un elenco di valori xse uno stato iniziale di 0(rappresentato da totalin questo caso). Quindi, per ogni xin xs, combiniamo quel valore con lo stato corrente in base ad alcune operazioni di combinazione (in questo caso aggiunta) e utilizziamo il risultato come nuovo stato. In sostanza, sum_all([1, 2, 3])è equivalente a (3 + (2 + (1 + 0))). Questo modello può essere estratto in una funzione di ordine superiore , una funzione che accetta funzioni come argomenti:

def fold(items, initial_state, combiner_func):
  state = initial_state
  for item in items:
    state = combiner_func(item, state)
  return state

def sum_all(xs):
  return fold(xs, 0, lambda x y: x + y)

Questa implementazione di foldè ancora imperativa, ma può essere fatta anche in modo ricorsivo:

def fold_recursive(items, initial_state, combiner_func):
  if not is_empty(items):
    state = combiner_func(initial_state, first_item(items))
    return fold_recursive(rest_items(items), state, combiner_func)
  else:
    return initial_state

Espressa in termini di piega, la tua funzione è semplicemente:

def exponent(base, power):
  return fold(repeat(base, power), 1, lambda x y: x * y))

... dove repeat(x, n)restituisce un elenco di ncopie di x.

Molte lingue, in particolare quelle orientate alla programmazione funzionale, offrono la piegatura nella loro libreria standard. Anche Javascript lo fornisce sotto il nome reduce. In generale, se ti ritrovi ad usare la ricorsione per "ricordare" un valore attraverso un ciclo di qualche tipo, probabilmente vorrai una piega.


8
Sicuramente impari a individuare quando un problema può essere risolto da una piega o una mappa. In FP, quasi tutti i loop possono essere espressi come piega o mappa; quindi la ricorsione esplicita di solito non è necessaria.
Carcigenicato,

1
In alcune lingue, puoi semplicemente scriverefold(repeat(base, power), 1, *)
user253751

4
Rico Kahler: scanè essenzialmente un punto in foldcui invece di combinare l'elenco di valori in un solo valore, viene combinato e ogni valore intermedio viene sputato indietro lungo la strada, producendo un elenco di tutti gli stati intermedi che la piega ha creato invece di produrre semplicemente il stato finale. È implementabile in termini di fold(ogni operazione di looping è).
Jack,

4
@RicoKahler E, per quanto ne so, riduzioni e pieghe sono la stessa cosa. Haskell usa il termine "piega", mentre Clojure preferisce "ridurre". Il loro comportamento mi sembra lo stesso.
Carcigenicate,

2
@Ucenna: è sia una variabile che una funzione. Nella programmazione funzionale, le funzioni sono valori come i numeri e le stringhe: è possibile memorizzarle in variabili, passarle come argomenti ad altre funzioni, restituirle da funzioni e generalmente trattarle come altri valori. Quindi combiner_funcè un argomento e sum_allsta trasmettendo una funzione anonima (questo è il lambdabit - crea un valore di funzione senza nominarlo) che definisce come vuole combinare due elementi insieme.
Jack,

8

Questa è una risposta supplementare per aiutare a spiegare mappe e pieghe. Per gli esempi seguenti, userò questo elenco. Ricorda, questo elenco è immutabile, quindi non cambierà mai:

var numbers = [1, 2, 3, 4, 5]

Userò i numeri nei miei esempi perché portano a un codice di facile lettura. Ricorda, tuttavia, le pieghe possono essere utilizzate per qualsiasi cosa per cui si possa usare un ciclo imperativo tradizionale.

Una mappa prende un elenco di qualcosa e una funzione e restituisce un elenco che è stato modificato utilizzando la funzione. Ogni elemento viene passato alla funzione e diventa qualunque cosa restituisca la funzione.

L'esempio più semplice di ciò è semplicemente l'aggiunta di un numero a ciascun numero in un elenco. Userò lo pseudocodice per rendere agnostico il linguaggio:

function add-two(n):
    return n + 2

var numbers2 =
    map(add-two, numbers) 

Se stampassi numbers2, vedresti [3, 4, 5, 6, 7]quale è il primo elenco con 2 aggiunti a ciascun elemento. Si noti che è add-twostata assegnata la funzione mapda utilizzare.

Le piegature sono simili, ad eccezione della funzione richiesta per assegnarle che deve accettare 2 argomenti. Il primo argomento è di solito l'accumulatore (in una piega a sinistra, che sono i più comuni). L'accumulatore sono i dati passati durante il ciclo. Il secondo argomento è l'elemento corrente dell'elenco; proprio come sopra per la mapfunzione.

function add-together(n1, n2):
    return n1 + n2

var sum =
    fold(add-together, 0, numbers)

Se stampassi sumvedresti la somma dell'elenco dei numeri: 15.

Ecco cosa devono foldfare gli argomenti :

  1. Questa è la funzione che stiamo offrendo. La piega passerà la funzione dell'accumulatore corrente e l'elemento corrente dell'elenco. Qualunque sia la funzione restituita diventerà il nuovo accumulatore, che verrà passato alla funzione la volta successiva. Ecco come "ricordi" i valori quando esegui il looping in stile FP. Gli ho dato una funzione che accetta 2 numeri e li aggiunge.

  2. Questo è l'accumulatore iniziale; come inizia l'accumulatore prima che vengano elaborati tutti gli elementi nell'elenco. Quando sommi i numeri, qual è il totale prima di aver aggiunto tutti i numeri? 0, che ho passato come secondo argomento.

  3. Infine, come per la mappa, passiamo anche nell'elenco dei numeri per l'elaborazione.

Se le pieghe non hanno ancora senso, considera questo. Quando scrivi:

# Notice I passed the plus operator directly this time, 
#  instead of wrapping it in another function. 
fold(+, 0, numbers)

In pratica stai mettendo la funzione passata tra ciascun elemento nell'elenco e aggiungi l'accumulatore iniziale a sinistra oa destra (a seconda che si tratti di una piega sinistra o destra), quindi:

[1, 2, 3, 4, 5]

diventa:

0 + 1 + 2 + 3 + 4 + 5
^ Note the initial accumulator being added onto the left (for a left fold).

Che equivale a 15.

Utilizzare a mapquando si desidera trasformare un elenco in un altro elenco, della stessa lunghezza.

Usa a foldquando vuoi trasformare un elenco in un singolo valore, come sommare un elenco di numeri.

Come ha sottolineato @Jorg nei commenti, il "valore singolo" non deve necessariamente essere qualcosa di semplice come un numero; potrebbe essere un singolo oggetto, incluso un elenco o una tupla! Il modo in cui ho effettivamente fatto clic sulle pieghe per me era definire una mappa in termini di piega. Nota come l'accumulatore è un elenco:

function map(f, list):
    fold(
        function(xs, x): # xs is the list that has been processed so far
            xs.add( f(x) ) # Add returns the list instead of mutating it
        , [] # Before any of the list has been processed, we have an empty list
        , list) 

Onestamente, una volta capiti tutti, ti renderai conto che quasi tutti i loop possono essere sostituiti da una piega o una mappa.


1
@Ucenna @Ucenna Ci sono un paio di difetti nel tuo codice (come inon essere mai definiti), ma penso che tu abbia la giusta idea. Un problema con il tuo esempio è: la funzione ( x), viene passato solo un elemento dell'elenco alla volta, non l'intero elenco. La prima volta che xviene chiamato, viene passato l'accumulatore iniziale ( y) come primo argomento e il primo elemento come secondo argomento. Alla successiva esecuzione, xverrà passato il nuovo accumulatore a sinistra (qualunque cosa sia stata xrestituita la prima volta) e il secondo elemento dell'elenco come secondo argomento.
Carcigenicate,

1
@Ucenna Ora che hai l'idea di base, guarda di nuovo l'implementazione di Jack.
Carcigenicate,

1
@Ucenna: diversi lang hanno preferenze diverse sul fatto che la funzione data a fold prenda l'accumulatore come primo o secondo argomento, sfortunatamente. Uno dei motivi per cui è bello usare un'operazione commutativa come un'aggiunta per insegnare le pieghe.
Jack,

3
"Usa a foldquando vuoi trasformare un elenco in un singolo valore (come sommare un elenco di numeri)." - Voglio solo notare che questo "valore singolo" può essere arbitrariamente complesso ... incluso un elenco! In realtà, foldè un metodo generale di iterazione, può fare tutto ciò che l' iterazione può fare. Ad esempio, mappuò essere banalmente espresso come func map(f, l) = fold((xs, x) => append(xs, f(x)), [], l)Qui, il "valore singolo" calcolato da foldè in realtà un elenco.
Jörg W Mittag,

2
... Forse vuoi fare con un elenco, può essere fatto con fold. E non deve essere un elenco, ogni raccolta che può essere espressa come vuota / non vuota lo farà. Il che significa sostanzialmente che qualsiasi iteratore lo farà. (Immagino che inserire la parola "catamorfismo" sarebbe troppo per l'introduzione di un principiante, però :-D)
Jörg W Mittag

1

È difficile trovare buoni problemi che non possono essere risolti con la funzionalità integrata. E se è integrato, dovrebbe essere usato come esempio di buon stile nel linguaggio x.

In haskell, ad esempio, hai già la funzione (^)in Prelude.

O se vuoi farlo in modo più programmatico product (replicate y x)

Quello che sto dicendo è che è difficile mostrare i punti di forza di uno stile / linguaggio se non si utilizzano le funzionalità fornite. Tuttavia, potrebbe essere un buon passo per mostrare come funziona dietro le quinte, ma penso che dovresti programmare il modo migliore in qualsiasi lingua tu stia usando e quindi aiutare la persona da lì a capire cosa sta succedendo se necessario.


1
Al fine di collegare logicamente questa risposta agli altri, va notato che productè solo una funzione di collegamento foldcon la moltiplicazione come funzione e 1 come argomento iniziale e che replicateè una funzione che produce un iteratore (o elenco; come ho notato sopra i due sono essenzialmente indistinguibili in haskell) che fornisce un determinato numero di output identici. Dovrebbe essere facile capire ora come questa implementazione faccia la stessa cosa della risposta di @ Jack sopra, semplicemente usando versioni predefinite di casi speciali delle stesse funzioni per renderlo più conciso.
Periata Breatta,
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.