"Memorizzare" i valori nella programmazione funzionale

20

Ho deciso di assumermi il compito di apprendere la programmazione funzionale. Finora è stata una grande esplosione, e ho "visto la luce" per così dire. Sfortunatamente, in realtà non conosco nessun programmatore funzionale con cui possa rimbalzare 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 usa 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 illustrare una semplice funzione matematica, come elevare un valore a un potere. In teoria, avrei potuto farlo facilmente con una funzione precostruita, ma ciò avrebbe vanificato lo scopo di un esempio.

Ad ogni modo, sto avendo qualche difficoltà a capire come mantenere un valore. Poiché questa è una programmazione funzionale, non posso cambiare la variabile. Se dovessi codificare questo imperativo, sarebbe simile a questo:

(Quello che 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? Ho bisogno di mantenere un valore, z in 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 se fosse "giusto". C'è un modo migliore per farlo?

    
posta Ucenna 20.09.2016 - 20:42
fonte

4 risposte

37

Prima di tutto, congratulazioni per "vedere la luce". Hai reso il mondo del software un posto migliore espandendo i tuoi orizzonti.

In secondo luogo, onestamente non c'è modo che un professore che non capisce la programmazione funzionale possa dire qualcosa di utile sul tuo codice, a parte commenti banali come "l'indentazione guarda fuori". Questo non è sorprendente in un corso di sviluppo web, poiché la maggior parte dello sviluppo web viene fatto usando HTML / CSS / JavaScript. A seconda di quanto ti preoccupi veramente dell'apprendimento dello sviluppo web, potresti voler mettere in atto gli strumenti per insegnare il tuo professore (per quanto doloroso possa essere - lo so per esperienza).

Per rispondere alla domanda dichiarata: se il tuo codice imperativo utilizza un ciclo, è probabile che il tuo 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 uno zucchero sintattico per processi iterativi ricorsivi.

Come nota a margine, non c'è bisogno di un valore di z nel tuo codice imperativo o funzionale, in effetti. 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

anziché modificare il significato della variabile x .

    
risposta data 20.09.2016 - 20:57
fonte
33

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

Nella programmazione funzionale, un ripiegare è un modo per combinare una serie di valori che "ricorda" un valore tra ciascuna operazione. Prendi in considerazione l'aggiunta di una lista di numeri imperativamente:

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

Prendiamo un elenco di valori xs e uno stato iniziale di 0 (rappresentato da total in questo caso). Quindi, per ogni x in xs , combiniamo quel valore con lo stato corrente secondo alcune operazioni combinate (in questo caso l'aggiunta), e usiamo il risultato come lo stato nuovo . In sostanza, sum_all([1, 2, 3]) equivale 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 eseguita 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 una lista di n copie 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 trovi a ricorrere per "ricordare" un valore su un ciclo di qualche tipo, probabilmente vuoi una piega.

    
risposta data 20.09.2016 - 22:16
fonte
8

Questa è una risposta supplementare per aiutare a spiegare mappe e pieghe. Per gli esempi di seguito, userò questa lista. Ricorda, questa lista è 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 però che le pieghe possono essere usate per qualsiasi cosa possa essere usata per un ciclo imperativo tradizionale.

Una mappa prende una lista di qualcosa, e una funzione, e restituisce una lista che è stata modificata usando la funzione. Ogni elemento viene passato alla funzione e diventa qualsiasi cosa restituisca la funzione.

L'esempio più semplice di questo è semplicemente l'aggiunta di un numero a ciascun numero in un elenco. Userò pseudocodice per renderlo indipendente dal linguaggio:

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

var numbers2 =
    map(add-two, numbers) 

Se hai stampato numbers2 , vedresti [3, 4, 5, 6, 7] che è la prima lista con 2 aggiunti a ciascun elemento. Si noti che la funzione add-two è stata data a map da utilizzare.

Fold sono simili, tranne per il fatto che la funzione che ti viene richiesta deve prendere 2 argomenti. Il primo argomento è solitamente l'accumulatore (in una piega a sinistra, che è il più comune). L'accumulatore è il dato che viene passato durante il ciclo. Il secondo argomento è l'elemento corrente dell'elenco; proprio come sopra per la funzione map .

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

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

Se hai stampato sum vedresti la somma dell'elenco di numeri: 15.

Ecco quali sono gli argomenti a fold do:

  1. Questa è la funzione che stiamo dando la piega. La piega passerà la funzione l'accumulatore corrente e l'elemento corrente della lista. Qualunque funzione restituisca la funzione diventerà il nuovo accumulatore, che verrà passato alla funzione la volta successiva. Questo è il modo in cui "ricordi" i valori quando fai un ciclo in stile FP. Gli ho dato una funzione che prende 2 numeri e li aggiunge.

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

  3. Infine, come per la mappa, passiamo anche all'elenco dei numeri da elaborare.

Se le pieghe non hanno ancora senso, prendi in considerazione 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 ogni elemento della lista e aggiungi l'accumulatore iniziale a sinistra oa destra (a seconda che si tratti di una piega a sinistra oa 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).

Quale è uguale a 15.

Usa un map quando vuoi trasformare una lista in un'altra lista, della stessa lunghezza.

Usa un fold quando vuoi trasformare una lista in un singolo valore, come sommare un elenco di numeri.

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

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 capito, ti renderai conto che quasi tutti i cicli possono essere sostituiti da una piega o da una mappa.

    
risposta data 21.09.2016 - 18:17
fonte
1

È difficile trovare buoni problemi che non possono essere risolti con funzionalità incorporate. E se è incorporato, dovrebbe essere usato per essere un esempio di buon stile nel linguaggio x.

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

O se vuoi farlo più programmaticamente 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à che fornisce. Comunque potrebbe essere un buon passo per mostrare come funziona dietro le quinte, ma penso che dovresti codificare nel modo migliore in qualunque lingua tu stia usando e quindi aiutare la persona da lì a capire cosa sta succedendo se necessario.

    
risposta data 21.09.2016 - 13:06
fonte

Leggi altre domande sui tag