Introduzione ai concetti di programmazione funzionale in F#

La programmazione funzionale è uno stile di programmazione che enfatizza l'uso di funzioni e dati non modificabili. La programmazione funzionale tipizzata è quando la programmazione funzionale viene combinata con tipi statici, ad esempio con F#. In generale, i concetti seguenti vengono sottolineati nella programmazione funzionale:

  • Funzioni come costrutti primari usati
  • Espressioni anziché istruzioni
  • Valori non modificabili sulle variabili
  • Programmazione dichiarativa sulla programmazione imperativa

In questa serie verranno esaminati i concetti e i modelli nella programmazione funzionale usando F#. Lungo la strada si apprenderà anche un po' di F#.

Terminologia

La programmazione funzionale, come altri paradigmi di programmazione, include un vocabolario che alla fine sarà necessario apprendere. Ecco alcuni termini comuni che verranno visualizzati tutti i tempi:

  • Funzione : una funzione è un costrutto che produrrà un output quando viene specificato un input. Più formalmente, esegue il mapping di un elemento da un set a un altro set. Questo formalismo viene sollevato in concreto in molti modi, soprattutto quando si usano funzioni che operano su raccolte di dati. È il concetto di base (e importante) nella programmazione funzionale.
  • Espressione : un'espressione è un costrutto nel codice che produce un valore. In F# questo valore deve essere associato o ignorato in modo esplicito. Un'espressione può essere facilmente sostituita da una chiamata di funzione.
  • Purezza : la purezza è una proprietà di una funzione in modo che il valore restituito sia sempre lo stesso per gli stessi argomenti e che la sua valutazione non abbia effetti collaterali. Una funzione pura dipende interamente dai relativi argomenti.
  • Trasparenza referenziale - Trasparenza referenziale è una proprietà di espressioni in modo che possano essere sostituite con il relativo output senza influire sul comportamento di un programma.
  • Immutabilità : l'immutabilità indica che non è possibile modificare un valore sul posto. Ciò è in contrasto con le variabili, che possono cambiare sul posto.

Esempi

Gli esempi seguenti illustrano questi concetti di base.

Funzioni

Il costrutto più comune e fondamentale nella programmazione funzionale è la funzione . Ecco una funzione semplice che aggiunge 1 a un numero intero:

let addOne x = x + 1

La firma del tipo è la seguente:

val addOne: x:int -> int

La firma può essere letta come , "addOne accetta un int denominato x e produrrà un ".int Più formalmente, addOne esegue il mapping di un valore dal set di interi al set di interi. Il -> token indica questo mapping. In F#, in genere è possibile esaminare la firma della funzione per ottenere un'idea di ciò che fa.

Quindi, perché la firma è importante? Nella programmazione funzionale tipizzata, l'implementazione di una funzione è spesso meno importante della firma del tipo effettivo. Il fatto che addOne aggiunge il valore 1 a un numero intero è interessante in fase di esecuzione, ma quando si costruisce un programma, il fatto che accetta e restituisce un int è ciò che informa come si userà effettivamente questa funzione. Inoltre, una volta usata correttamente questa funzione (rispetto alla firma del tipo), la diagnosi di eventuali problemi può essere eseguita solo all'interno del corpo della addOne funzione. Questo è l'impulso alla programmazione funzionale tipizzata.

Espressioni

Le espressioni sono costrutti che restituiscono un valore. A differenza delle istruzioni, che eseguono un'azione, le espressioni possono essere considerate come eseguire un'azione che restituisce un valore. Le espressioni vengono quasi sempre usate nella programmazione funzionale anziché nelle istruzioni .

Si consideri la funzione precedente, addOne. Il corpo di addOne è un'espressione:

// 'x + 1' is an expression!
let addOne x = x + 1

È il risultato di questa espressione che definisce il tipo di risultato della addOne funzione. Ad esempio, l'espressione che costituisce questa funzione può essere modificata in modo che sia un tipo diverso, ad esempio :string

let addOne x = x.ToString() + "1"

La firma della funzione è ora:

val addOne: x:'a -> string

Poiché qualsiasi tipo in F# può ToString() chiamare su di esso, il tipo di x è stato reso generico (denominato Generalizzazione automatica) e il tipo risultante è un .string

Le espressioni non sono solo i corpi delle funzioni. È possibile avere espressioni che producono un valore usato altrove. Uno comune è if:

// Checks if 'x' is odd by using the mod operator
let isOdd x = x % 2 <> 0

let addOneIfOdd input =
    let result =
        if isOdd input then
            input + 1
        else
            input

    result

L'espressione if produce un valore denominato result. Si noti che è possibile omettere result completamente, rendendo l'espressione il if corpo della addOneIfOdd funzione. La cosa chiave da ricordare sulle espressioni è che producono un valore.

Esiste un tipo speciale, unit, che viene usato quando non è presente alcun elemento da restituire. Si consideri ad esempio questa funzione semplice:

let printString (str: string) =
    printfn $"String is: {str}"

La firma è simile alla seguente:

val printString: str:string -> unit

Il unit tipo indica che non viene restituito alcun valore effettivo. Ciò è utile quando si dispone di una routine che deve "svolgere il lavoro" nonostante non abbia alcun valore da restituire in seguito a tale lavoro.

Questo è in contrasto con la programmazione imperativa, in cui il costrutto equivalente if è un'istruzione e la produzione di valori viene spesso eseguita con variabili mutevoli. Ad esempio, in C#, il codice potrebbe essere scritto come segue:

bool IsOdd(int x) => x % 2 != 0;

int AddOneIfOdd(int input)
{
    var result = input;

    if (IsOdd(input))
    {
        result = input + 1;
    }

    return result;
}

Vale la pena notare che C# e altri linguaggi in stile C supportano l'espressione ternaria, che consente la programmazione condizionale basata su espressioni.

Nella programmazione funzionale, è raro modificare i valori con istruzioni . Anche se alcuni linguaggi funzionali supportano istruzioni e mutazioni, non è comune usare questi concetti nella programmazione funzionale.

Funzioni pure

Come accennato in precedenza, le funzioni pure sono funzioni che:

  • Restituisce sempre lo stesso valore per lo stesso input.
  • Non hanno effetti collaterali.

È utile pensare alle funzioni matematiche in questo contesto. In matematica, le funzioni dipendono solo dai loro argomenti e non hanno effetti collaterali. Nella funzione f(x) = x + 1matematica il valore di f(x) dipende solo dal valore di x. Le funzioni pure nella programmazione funzionale sono uguali.

Quando si scrive una funzione pura, la funzione deve dipendere solo dai relativi argomenti e non eseguire alcuna azione che produce un effetto collaterale.

Di seguito è riportato un esempio di funzione non pura perché dipende dallo stato globale modificabile:

let mutable value = 1

let addOneToValue x = x + value

La addOneToValue funzione è chiaramente impura, perché value può essere modificata in qualsiasi momento per avere un valore diverso da 1. Questo modello di a seconda di un valore globale deve essere evitato nella programmazione funzionale.

Ecco un altro esempio di una funzione non pura, perché esegue un effetto collaterale:

let addOneToValue x =
    printfn $"x is %d{x}"
    x + 1

Anche se questa funzione non dipende da un valore globale, scrive il valore di x nell'output del programma. Anche se non c'è nulla di intrinsecamente sbagliato con questa operazione, significa che la funzione non è pura. Se un'altra parte del programma dipende da un elemento esterno al programma, ad esempio il buffer di output, la chiamata a questa funzione può influire sull'altra parte del programma.

La rimozione dell'istruzione printfn rende pura la funzione:

let addOneToValue x = x + 1

Anche se questa funzione non è intrinsecamente migliore della versione precedente con l'istruzione printfn , garantisce che tutta questa funzione restituisca un valore. La chiamata di questa funzione a un numero qualsiasi di volte produce lo stesso risultato: produce semplicemente un valore. La prevedibilità data dalla purezza è qualcosa che molti programmatori funzionali cercano.

Immutabilità

Infine, uno dei concetti più fondamentali della programmazione funzionale tipizzata è l'immutabilità. In F# tutti i valori non sono modificabili per impostazione predefinita. Ciò significa che non possono essere mutati sul posto, a meno che non vengano contrassegnati in modo esplicito come modificabili.

In pratica, lavorare con valori non modificabili significa che si cambia l'approccio alla programmazione da , "Devo cambiare qualcosa", in "Devo produrre un nuovo valore".

Ad esempio, l'aggiunta di 1 a un valore significa produrre un nuovo valore, senza modificare quello esistente:

let value = 1
let secondValue = value + 1

In F# il codice seguente non modifica la value funzione; esegue invece un controllo di uguaglianza:

let value = 1
value = value + 1 // Produces a 'bool' value!

Alcuni linguaggi di programmazione funzionale non supportano affatto la mutazione. In F# è supportato, ma non è il comportamento predefinito per i valori.

Questo concetto si estende ulteriormente alle strutture di dati. Nella programmazione funzionale, le strutture di dati non modificabili, ad esempio set (e molte altre) hanno un'implementazione diversa da quella prevista inizialmente. Concettualmente, un elemento come l'aggiunta di un elemento a un set non modifica il set, produce un nuovo set con il valore aggiunto. Questa operazione viene spesso eseguita da una struttura di dati diversa che consente di tenere traccia in modo efficiente di un valore in modo che la rappresentazione appropriata dei dati possa essere fornita di conseguenza.

Questo stile di utilizzo di valori e strutture di dati è fondamentale, perché impone di gestire qualsiasi operazione che modifichi qualcosa come se creasse una nuova versione di tale elemento. Ciò consente a cose come l'uguaglianza e la confrontabilità di essere coerenti nei programmi.

Passaggi successivi

La sezione successiva illustra in modo approfondito le funzioni, esplorando diversi modi in cui è possibile usarle nella programmazione funzionale.

L'uso delle funzioni in F# esplora le funzioni in modo approfondito, mostrando come usarle in vari contesti.

Altre risorse

La serie Thinking Functionally è un'altra grande risorsa per apprendere la programmazione funzionale con F#. Illustra i concetti fondamentali della programmazione funzionale in modo pragmatico e facile da leggere, usando le funzionalità F# per illustrare i concetti.