Aiutami a capire il software "stateless" nella programmazione funzionale

3

Sono un ingegnere software esperto e ho esperienza in una gamma di linguaggi da PHP, Ruby, C #, Java - negli ultimi anni ho lavorato principalmente su Java.

Ora sto imparando Scala, ed è la mia prima vera incursione nella programmazione funzionale.

Capisco che i miei metodi non dovrebbero avere effetti collaterali e che dovrei favorire l'immutabilità.

Sto cercando di costruire un piccolo progetto che ho precedentemente realizzato in Java e Python, come esercizio. L'applicazione effettua le seguenti operazioni:

  • Parla con un servizio REST per recuperare una serie di configurazioni.
  • Mantiene una copia / cache di tali configurazioni, in modo che possano essere riferimenti in altre aree del software.
  • Ascolta l'input dell'utente e chiama di nuovo i servizi RESTful usando l'input dell'utente come comando e le configurazioni che abbiamo richiamato all'avvio.

Mi sembra che poiché la mia classe che si occupa dell'API RESTful non sarà trasparente referenzialmente (l'output potrebbe differire per un numero qualsiasi di ragioni, è un'API esterna su cui non ho alcun controllo), devo avere un qualche tipo di stato (le configurazioni), che forse questo software non è adatto a un paradigma FP.

Sto pensando a questo nel modo sbagliato?

Grazie

    
posta Richter 05.10.2014 - 11:19
fonte

3 risposte

4

La programmazione funzionale non riguarda il non avere stato. Riguarda il mantenimento delle funzioni trasparenza referenziale e senza effetti collaterali. Di solito questo significa non avere uno stato, ma non deve essere così.
La trasparenza referenziale significa che per un dato parametro di input la funzione restituirà sempre lo stesso risultato.
Nessun effetto collaterale significa che la funzione non cambia il mondo esterno, ad esempio non modifica alcuni parametri globali. Ciò significa anche che non chiamare la funzione non influirà su altre funzioni.

Un esempio in cui esiste uno stato ma mantiene comunque la trasparenza referenziale e nessun effetto collaterale è il caching.
class StatefullCache {
  val remoteRepository = ???
  var map: Map[String,Data] = Map()
  private def fetchFromRemote(id: String) = {
    val data = remoteRepository.get(id)
    map = map + (id -> data)
    data
  }
  def get(id: String) = map.getOrElse(id, fetchFromRemote(id))
}

Questa classe ha lo stato ma è la trasparenza referenziale, poiché restituisce sempre lo stesso risultato per ogni dato ID. Inoltre non ha effetti collaterali (beh ... ha una memoria in crescita, ma una limitazione che non influisce sul flusso dell'applicazione).
In pratica nessuna applicazione è un "puro funzionale". stampa sullo schermo, file e amp; I / O di rete hanno tutti gli effetti di dimensione (e arrivano all'estremo, anche il calore della CPU è un effetto collaterale). L'idea nella programmazione funzionale è di mantenere l'effetto collaterale al minimo e isolato. Alcune lingue ti costringono a farlo. Scala non lo forza, ma ti incoraggia a farlo.

A seconda del tuo caso d'uso che chiama API / REST di terze parti / qualsiasi cosa che non sia immutabile o abbia effetti collaterali, può essere considerata utile o meno dalla tua applicazione. Ad esempio, se l'esecuzione dell'applicazione è "nuovo mondo" in cui i dati potrebbero cambiare, la classe sopra funzionerà correttamente (poiché accederà al servizio esterno solo una volta per ID). Per riassumere, se minimizzi la mutevolezza e l'effetto collaterale e tieni tutto sotto controllo, puoi avere i benefici per la programmazione funzionale nel resto dell'app.     
risposta data 05.10.2014 - 14:35
fonte
1

Considera la frase

I understand that my methods should not have side affects, and that I should favour immutability.

Favorire l'immutabilità significa che non dovresti usare la mutabilità quando non ne hai bisogno e che dovresti gestirla in un modo speciale quando ne hai bisogno.

Ci sono molti casi in cui puoi usare la mutazione per calcolare qualcosa, mentre puoi anche usare una soluzione senza mutazioni usando la ricorsione o le funzioni di alto ordine come map , fold , filter , e così via . Ad esempio, se vuoi calcolare la somma dei numeri da 1 a n, puoi scrivere

def sumMut(n: Int): Int = {
    var s = 0
    var i = 1
    while (i < n) {
        s = s + i
        i = i + 1
    }

    return s
}

usando la mutazione, ma puoi anche scrivere

def sumImmut(n: Int): Int = {
    def loop(i: Int, acc: Int): Int = if (i < n) loop(i + 1, acc + i) else acc

    loop(1, 0)
}

senza mutazione. Il primo è una soluzione imperativa / di stato, quest'ultima è una soluzione funzionale / senza stato.

Nel caso in cui sia necessaria la mutazione per memorizzare lo stato (ad esempio tra chiamate intermedie a un oggetto), l'approccio funzionale è

  1. Mantieni il codice che utilizza la mutazione al minimo e
  2. Isolare chiaramente quel codice dal resto del codice, che dovrebbe essere privo di modifiche.

Alcuni linguaggi funzionali offrono costrutti espliciti per indirizzare il punto 2 (vedi ad esempio monadi in Haskell).

    
risposta data 05.10.2014 - 12:09
fonte
1

La programmazione funzionale non riguarda l'assenza di stato. Invece, tutta quella roba di immutabilità riguarda il rendere esplicito lo stato. Un semplice esempio è l'aggiunta di un elenco di numeri. In Python, faremmo qualcosa del genere:

xs = [1, 2, 3, 4, 5]
sum = 0
for x in xs:
  sum += x

(In realtà, utilizzeremmo sum incorporato, ma questo è solo un esempio). Questo ciclo ha lo stato, poiché mutiamo la variabile sum a ogni iterazione. La variabile sum non è referenzialmente trasparente, perché punta a valori diversi in diversi punti di tempo. Lo stato è codificato nel flusso di controllo. Scala non è dogmatico sulla programmazione funzionale e ti permette di scrivere codice in modo statico. Tuttavia, sei incoraggiato a limitare lo stato implicito.

Come possiamo rendere esplicito lo stato, o sbarazzarci di esso quando si sommano i numeri? Possiamo definire la somma di una lista come zero se la lista è vuota, e il primo elemento più la somma se gli altri elementi sono altrimenti. Si noti che questa è una definizione ricorsiva. In Scala, lo scriveremo come

val xs = Seq(1, 2, 3, 4, 5)

def summer(xs: Seq[Int]): Int = xs match {
  case Seq() => 0
  case x :: rest => x + summer(rest)
}

val sum = summer(xs)

Da notare che mentre abbiamo fatto ricorsivamente il ciclo for … in … , non c'è ancora uno stato ovvio. Questo perché la funzione ricorsiva utilizza lo stack di chiamate per memorizzare valori intermedi. Lo stato del loop diventa esplicito se lo scriviamo come funzione ricorsiva di coda:

import scala.annotation.tailrec

@tailrec
def summer(xs: Seq[Int], acc: Int = 0): Int = xs match {
    case Seq() => acc
    case x :: rest => summer(rest, x + acc)
}

L'argomento acc (accumulator) rappresenta qui lo stato. Non è mutato, ma creiamo un nuovo stato per ogni iterazione.

In effetti, eseguire un'operazione del genere è così comune che di solito viene eseguito tramite fold (o foldLeft , che consente al risultato di essere di un tipo diverso):

def summer (xs: Seq[Int]): Int = (xs fold 0) (_ + _)

Come si configura lo stato esplicito con la programmazione orientata agli oggetti? Abbastanza bene, in realtà: l'oggetto è lo stato. Pensa all'attuale oggetto come a un parametro che viene passato invisibilmente in qualsiasi chiamata di metodo. Tuttavia, non si modifica mai lo stato e ne si crea uno nuovo. Attenzione però: questo può rendere orribilmente inefficiente alcune strutture dati se non progettate pensando all'immutabilità.

Che cosa significa per implementare un'API REST? Niente. È compito del programmatore decidere quali paradigmi e modelli sono adatti in ogni situazione specifica. Usare lo stato esplicito può avere dei benefici, ma potrebbe anche essere l'offuscamento di ciò che sta realmente accadendo. In ogni caso, diverse parti del programma possono utilizzare diverse quantità di programmazione funzionale. L'utilizzo dei pattern FP è possibile anche su una scala piuttosto ridotta e non richiede che l'intero programma sia scritto in assoluta "purezza".

    
risposta data 05.10.2014 - 12:15
fonte

Leggi altre domande sui tag