Inleiding tot functionele programmeerconcepten in F#

Functioneel programmeren is een programmeerstijl die het gebruik van functies en onveranderbare gegevens benadrukt. Getypt functioneel programmeren is wanneer functioneel programmeren wordt gecombineerd met statische typen, zoals met F#. In het algemeen worden de volgende concepten benadrukt in functionele programmering:

  • Functies als de primaire constructies die u gebruikt
  • Expressies in plaats van instructies
  • Onveranderbare waarden boven variabelen
  • Declaratieve programmering ten opzichte van imperatieve programmering

In deze reeks verkent u concepten en patronen in functionele programmering met behulp van F#. Onderweg leert u ook wat F#.

Terminologie

Functionele programmering, zoals andere programmeerparadigma's, wordt geleverd met een woordenlijst die u uiteindelijk moet leren. Hier volgen enkele algemene termen die u de hele tijd ziet:

  • Functie : een functie is een constructie die een uitvoer produceert wanneer een invoer wordt gegeven. Formeel gezien wijst het een item van de ene set toe aan een andere set. Dit formalisme wordt op veel manieren in het beton gebracht, met name bij het gebruik van functies die werken op verzamelingen gegevens. Het is het meest elementaire (en belangrijke) concept in functionele programmering.
  • Expressie : een expressie is een constructie in code die een waarde produceert. In F# moet deze waarde worden gebonden of expliciet genegeerd. Een expressie kan triviaal worden vervangen door een functieaanroep.
  • Zuiverheid: zuiverheid is een eigenschap van een functie, zodat de geretourneerde waarde altijd hetzelfde is voor dezelfde argumenten en dat de evaluatie geen bijwerkingen heeft. Een zuivere functie is volledig afhankelijk van de argumenten.
  • Referentiële transparantie : referentiële transparantie is een eigenschap van expressies, zodat ze kunnen worden vervangen door hun uitvoer zonder dat dit van invloed is op het gedrag van een programma.
  • Onveranderbaarheid: onveranderbaarheid betekent dat een waarde niet ter plaatse kan worden gewijzigd. Dit is in tegenstelling tot variabelen, die kunnen worden gewijzigd.

Voorbeelden

In de volgende voorbeelden worden deze kernconcepten gedemonstreert.

Functions

De meest voorkomende en fundamentele constructie in functionele programmering is de functie. Hier volgt een eenvoudige functie waarmee 1 wordt toegevoegd aan een geheel getal:

let addOne x = x + 1

De typehandtekening is als volgt:

val addOne: x:int -> int

De handtekening kan worden gelezen als"addOne accepteert een int naam x en produceert een int". Formeel gezien is het addOne toewijzen van een waarde uit de set gehele getallen aan de set gehele getallen. Het -> token geeft deze toewijzing aan. In F# kunt u meestal de functiehandtekening bekijken om een idee te krijgen van wat deze doet.

Waarom is de handtekening belangrijk? Bij getypeerde functionele programmering is de implementatie van een functie vaak minder belangrijk dan de werkelijke typehandtekening! Het feit dat addOne de waarde 1 toevoegt aan een geheel getal is interessant tijdens runtime, maar wanneer u een programma maakt, is het feit dat deze een programma accepteert en retourneert, int wat aangeeft hoe u deze functie daadwerkelijk gaat gebruiken. Bovendien, zodra u deze functie correct gebruikt (met betrekking tot de typehandtekening), kan het diagnosticeren van eventuele problemen alleen binnen de hoofdtekst van de addOne functie worden uitgevoerd. Dit is de impuls achter getypeerde functionele programmering.

Expressies

Expressies zijn constructies die een waarde opleveren. In tegenstelling tot instructies die een actie uitvoeren, kunnen expressies worden beschouwd als het uitvoeren van een actie die een waarde teruggeeft. Expressies worden bijna altijd gebruikt in functionele programmering in plaats van instructies.

Houd rekening met de vorige functie, addOne. De hoofdtekst is addOne een expressie:

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

Dit is het resultaat van deze expressie waarmee het resultaattype van de addOne functie wordt gedefinieerd. De expressie waaruit deze functie bestaat, kan bijvoorbeeld worden gewijzigd in een ander type, zoals:string

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

De handtekening van de functie is nu:

val addOne: x:'a -> string

Aangezien elk type in F# erop kan zijn ToString() aangeroepen, is het type x algemeen gemaakt (automatische generalisatie genoemd) en het resulterende type is een string.

Expressies zijn niet alleen de lichamen van functies. U kunt expressies hebben die een waarde produceren die u elders gebruikt. Een veelvoorkomende is 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

De if expressie produceert een waarde met de naam result. Houd er rekening mee dat u helemaal kunt weglaten result , waardoor de if expressie de hoofdtekst van de addOneIfOdd functie wordt. Het belangrijkste om te onthouden van expressies is dat ze een waarde produceren.

Er is een speciaal type, unitdat wordt gebruikt wanneer er niets is om terug te keren. Denk bijvoorbeeld aan deze eenvoudige functie:

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

De handtekening ziet er als volgt uit:

val printString: str:string -> unit

Het unit type geeft aan dat er geen werkelijke waarde wordt geretourneerd. Dit is handig als u een routine hebt die 'werk moet doen' ondanks dat er geen waarde moet worden geretourneerd als gevolg van dat werk.

Dit is in scherp contrast met imperatieve programmering, waarbij de equivalente if constructie een instructie is en het produceren van waarden vaak wordt uitgevoerd met het dempen van variabelen. In C# kan de code bijvoorbeeld als volgt worden geschreven:

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

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

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

    return result;
}

Het is de moeite waard om te vermelden dat C# en andere talen in C-stijl de ternaire expressie ondersteunen, waardoor voorwaardelijke programmering op basis van expressies mogelijk is.

In functioneel programmeren is het zelden om waarden met instructies te muteren. Hoewel sommige functionele talen instructies en mutatie ondersteunen, is het niet gebruikelijk om deze concepten in functionele programmering te gebruiken.

Pure functies

Zoals eerder vermeld, zijn pure functies functies die:

  • Evalueer altijd op dezelfde waarde voor dezelfde invoer.
  • Geen bijwerkingen.

Het is handig om wiskundige functies in deze context te bedenken. In de wiskunde zijn functies alleen afhankelijk van hun argumenten en hebben ze geen bijwerkingen. In de wiskundige functie f(x) = x + 1is de waarde alleen afhankelijk van f(x) de waarde van x. Pure functies in functioneel programmeren zijn op dezelfde manier.

Bij het schrijven van een zuivere functie moet de functie alleen afhankelijk zijn van de argumenten en geen actie uitvoeren die resulteert in een neveneffect.

Hier volgt een voorbeeld van een niet-zuivere functie omdat deze afhankelijk is van de globale, veranderlijke status:

let mutable value = 1

let addOneToValue x = x + value

De addOneToValue functie is duidelijk onzuiver, omdat value deze op elk gewenst moment kan worden gewijzigd in een andere waarde dan 1. Dit patroon, afhankelijk van een globale waarde, moet worden vermeden in functionele programmering.

Hier volgt een ander voorbeeld van een niet-pure functie, omdat het een neveneffect uitvoert:

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

Hoewel deze functie niet afhankelijk is van een globale waarde, wordt de waarde naar x de uitvoer van het programma geschreven. Hoewel er niets inherent mis is met dit, betekent dit wel dat de functie niet puur is. Als een ander deel van uw programma afhankelijk is van iets buiten het programma, zoals de uitvoerbuffer, kan het aanroepen van deze functie van invloed zijn op dat andere deel van uw programma.

Als u de printfn instructie verwijdert, wordt de functie puur:

let addOneToValue x = x + 1

Hoewel deze functie niet inherent beter is dan de vorige versie met de printfn instructie, garandeert dit dat al deze functie een waarde retourneert. Het aanroepen van deze functie levert een willekeurig aantal keren hetzelfde resultaat op: het produceert alleen een waarde. De voorspelbaarheid die door zuiverheid wordt gegeven, is iets waar veel functionele programmeurs naar streven.

Onveranderbaarheid

Ten slotte is een van de meest fundamentele concepten van getypeerde functionele programmering onveranderbaar. In F# zijn alle waarden standaard onveranderbaar. Dat betekent dat ze niet in-place kunnen worden gedempt, tenzij u ze expliciet markeert als veranderlijk.

In de praktijk betekent het werken met onveranderbare waarden dat u de programmeerbenadering wijzigt van 'Ik moet iets wijzigen' in 'Ik moet een nieuwe waarde produceren'.

Als u bijvoorbeeld 1 toevoegt aan een waarde, betekent het produceren van een nieuwe waarde, waarbij de bestaande waarde niet wordt gedempt:

let value = 1
let secondValue = value + 1

In F# muteert de volgende code de value functie niet. In plaats daarvan wordt er een gelijkheidscontrole uitgevoerd:

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

Sommige functionele programmeertalen ondersteunen helemaal geen mutatie. In F# wordt dit ondersteund, maar het is niet het standaardgedrag voor waarden.

Dit concept breidt zich nog verder uit tot gegevensstructuren. In functionele programmering hebben onveranderbare gegevensstructuren zoals sets (en nog veel meer) een andere implementatie dan u in eerste instantie zou verwachten. Conceptueel gezien verandert iets als het toevoegen van een item aan een set de set niet, het produceert een nieuwe set met de toegevoegde waarde. Onder de dekkingen wordt dit vaak bereikt door een andere gegevensstructuur waarmee een waarde efficiënt kan worden bijgehouden, zodat de juiste weergave van de gegevens als gevolg hiervan kan worden gegeven.

Deze stijl van het werken met waarden en gegevensstructuren is essentieel, omdat het u dwingt om een bewerking te behandelen die iets wijzigt alsof er een nieuwe versie van dat ding wordt gemaakt. Hierdoor kunnen zaken zoals gelijkheid en vergelijkbaarheid consistent zijn in uw programma's.

Volgende stappen

In de volgende sectie worden functies grondig behandeld, waarbij u verschillende manieren kunt verkennen waarop u ze kunt gebruiken in functionele programmering.

Het gebruik van functies in F# verkent functies diep en laat zien hoe u ze in verschillende contexten kunt gebruiken.

Meer lezen

De Reeks Functioneel denken is een andere geweldige resource voor meer informatie over functioneel programmeren met F#. Het behandelt de basisprincipes van functionele programmering op een pragmatische en gemakkelijk te lezen manier, met behulp van F#-functies om de concepten te illustreren.