Delen via


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 zijn de primaire constructies die u gebruikte
  • 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 concreet gemaakt, vooral bij het gebruik van functies die werken op dataverzamelingen. 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.

Functies

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 genaamd x en zal een int produceren". Formeel gezien is addOne het afbeelden van een waarde van de set gehele getallen naar de set gehele getallen. Het -> token betekent deze mapping. 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 de uitvoering, maar wanneer u een programma maakt, is het feit dat het een int accepteert en retourneert 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.

Uitdrukkingen

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 van addOne is een uitdrukking:

// '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 signatuur van de functie is nu:

val addOne: x:'a -> string

Aangezien op elk type in F# kan worden ToString() aangeroepen, is het type x generiek gemaakt, dit wordt 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 je result helemaal kunt weglaten, waardoor de if expressie het lichaam van de addOneIfOdd functie wordt. Het belangrijkste om te onthouden van expressies is dat ze een waarde produceren.

Er is een speciaal type, unit, dat wordt gebruikt wanneer er niets is om te retourneren. 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 wanneer u een routine hebt die 'werk moet uitvoeren' ondanks dat het geen waarde terug te geven heeft als resultaat van dat werk.

Dit staat in sterk contrast met imperatief programmeren, waarbij de equivalente if construct een instructie is en het produceren van waarden vaak wordt uitgevoerd met het veranderen 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 komt het zelden voor dat waarden worden veranderd door middel van onderdelen. 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 + 1 is de waarde van f(x) alleen afhankelijk van 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, schrijft het de waarde van x naar de uitvoer van het programma. 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.

Door de printfn instructie te verwijderen, wordt de functie zuiver.

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 ter plaatse kunnen worden veranderd, tenzij u ze expliciet markeert als muteerbaar.

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 functie value. 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.