Introduktion till funktionella programmeringskoncept i F#

Funktionell programmering är en programmeringsstil som betonar användningen av funktioner och oföränderliga data. Typad funktionell programmering är när funktionell programmering kombineras med statiska typer, till exempel med F#. I allmänhet betonas följande begrepp i funktionell programmering:

  • Funktioner som de primära konstruktioner som du använder
  • Uttryck i stället för uttryck
  • Oföränderliga värden över variabler
  • Deklarativ programmering över imperativ programmering

I den här serien utforskar du begrepp och mönster i funktionell programmering med hjälp av F#. Längs vägen lär du dig lite F# också.

Terminologi

Funktionell programmering, liksom andra programmeringsparadigm, levereras med ett ordförråd som du så småningom behöver lära dig. Här följer några vanliga termer som du ser hela tiden:

  • Funktion – En funktion är en konstruktion som skapar utdata när de får indata. Mer formellt mappar det ett objekt från en uppsättning till en annan uppsättning. Denna formalism lyfts i betong på många sätt, särskilt när du använder funktioner som fungerar på samlingar av data. Det är det mest grundläggande (och viktiga) konceptet inom funktionell programmering.
  • Uttryck – ett uttryck är en konstruktion i kod som genererar ett värde. I F# måste det här värdet vara bundet eller uttryckligen ignorerat. Ett uttryck kan enkelt ersättas av ett funktionsanrop.
  • Renhet – Renhet är en egenskap hos en funktion så att dess returvärde alltid är detsamma för samma argument och att dess utvärdering inte har några biverkningar. En ren funktion beror helt på dess argument.
  • Referenstransparens – Referenstransparens är en egenskap för uttryck som gör att de kan ersättas med sina utdata utan att påverka ett programs beteende.
  • Oföränderlighet – Oföränderlighet innebär att ett värde inte kan ändras på plats. Detta står i kontrast till variabler som kan ändras på plats.

Exempel

Följande exempel visar dessa grundläggande begrepp.

Functions

Den vanligaste och grundläggande konstruktionen i funktionell programmering är funktionen. Här är en enkel funktion som lägger till 1 i ett heltal:

let addOne x = x + 1

Dess typsignatur är följande:

val addOne: x:int -> int

Signaturen kan läsas som, "addOne accepterar ett int namngivet x och skapar en int". Mer formellt addOne mappasett värde från uppsättningen heltal till uppsättningen heltal. Token -> betyder den här mappningen. I F# kan du vanligtvis titta på funktionssignaturen för att få en uppfattning om vad den gör.

Så varför är signaturen viktig? I maskinskriven funktionell programmering är implementeringen av en funktion ofta mindre viktig än den faktiska typsignaturen! Det faktum att addOne lägger till värdet 1 i ett heltal är intressant vid körning, men när du skapar ett program är det det faktum att det accepterar och returnerar ett int som informerar hur du faktiskt kommer att använda den här funktionen. När du använder den här funktionen korrekt (med avseende på dess typsignatur) kan diagnostisering av eventuella problem endast göras i funktionens addOne brödtext. Detta är drivkraften bakom typad funktionell programmering.

Uttryck

Uttryck är konstruktioner som utvärderas till ett värde. Till skillnad från instruktioner, som utför en åtgärd, kan uttryck tänkas utföra en åtgärd som ger tillbaka ett värde. Uttryck används nästan alltid i funktionell programmering i stället för instruktioner.

Överväg föregående funktion, addOne. Brödtexten addOne i är ett uttryck:

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

Det är resultatet av det här uttrycket som definierar funktionens resultattyp addOne . Uttrycket som utgör den här funktionen kan till exempel ändras till en annan typ, till exempel :string

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

Funktionens signatur är nu:

val addOne: x:'a -> string

Eftersom alla typer i F# kan ha ToString() anropat den har typen av x gjorts generisk (kallas automatisk generalisering) och den resulterande typen är en string.

Uttryck är inte bara funktioners kroppar. Du kan ha uttryck som ger ett värde som du använder någon annanstans. En vanlig är 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

Uttrycket if genererar ett värde med namnet result. Observera att du kan utelämna result helt och hållet, vilket gör if uttrycket till funktionens addOneIfOdd brödtext. Det viktigaste att komma ihåg om uttryck är att de skapar ett värde.

Det finns en särskild typ, unit, som används när det inte finns något att returnera. Tänk dig till exempel den här enkla funktionen:

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

Signaturen ser ut så här:

val printString: str:string -> unit

Typen unit anger att det inte finns något faktiskt värde som returneras. Detta är användbart när du har en rutin som måste "göra arbete" trots att du inte har något värde att returnera som ett resultat av det arbetet.

Detta står i skarp kontrast till imperativ programmering, där motsvarande if konstruktion är en -instruktion, och skapande av värden görs ofta med muterande variabler. I C# kan koden till exempel skrivas så här:

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

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

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

    return result;
}

Det är värt att notera att C# och andra C-formatspråk stöder ternary-uttrycket, vilket möjliggör uttrycksbaserad villkorsstyrd programmering.

I funktionell programmering är det ovanligt att mutera värden med -instruktioner. Även om vissa funktionella språk stöder instruktioner och mutationer är det inte vanligt att använda dessa begrepp i funktionell programmering.

Rena funktioner

Som tidigare nämnts är rena funktioner funktioner som:

  • Utvärdera alltid till samma värde för samma indata.
  • Har inga biverkningar.

Det är bra att tänka på matematiska funktioner i det här sammanhanget. I matematik är funktioner endast beroende av deras argument och har inga biverkningar. I den matematiska funktionen f(x) = x + 1beror värdet för f(x) endast på värdet för x. Rena funktioner i funktionell programmering är samma sätt.

När du skriver en ren funktion måste funktionen bara vara beroende av dess argument och inte utföra någon åtgärd som resulterar i en biverkning.

Här är ett exempel på en icke-ren funktion eftersom den är beroende av globalt, föränderligt tillstånd:

let mutable value = 1

let addOneToValue x = x + value

Funktionen addOneToValue är helt klart oren, eftersom value kan ändras när som helst för att ha ett annat värde än 1. Det här mönstret för beroende på ett globalt värde ska undvikas i funktionell programmering.

Här är ett annat exempel på en icke-ren funktion, eftersom den utför en bieffekt:

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

Även om den här funktionen inte är beroende av x ett globalt värde, skriver den värdet för till programmets utdata. Även om det inte är något fel i sig med att göra detta, betyder det att funktionen inte är ren. Om en annan del av programmet är beroende av något utanför programmet, till exempel utdatabufferten, kan anrop av den här funktionen påverka den andra delen av programmet.

Om du tar bort -instruktionen printfn blir funktionen ren:

let addOneToValue x = x + 1

Även om den här funktionen inte är bättre än den tidigare versionen med -instruktionen printfn , garanterar den att allt den här funktionen gör är att returnera ett värde. Att anropa den här funktionen valfritt antal gånger ger samma resultat: det genererar bara ett värde. Den förutsägbarhet som renheten ger är något som många funktionella programmerare strävar efter.

Oföränderlighet

Slutligen är ett av de mest grundläggande begreppen för typad funktionell programmering oföränderlighet. I F# är alla värden oföränderliga som standard. Det innebär att de inte kan muteras på plats om du inte uttryckligen markerar dem som föränderliga.

I praktiken innebär arbete med oföränderliga värden att du ändrar din inställning till programmering från "Jag behöver ändra något" till "Jag behöver skapa ett nytt värde".

Om du till exempel lägger till 1 i ett värde skapas ett nytt värde, vilket inte muterar det befintliga värdet:

let value = 1
let secondValue = value + 1

I F#muterar value inte följande kod funktionen. I stället utför den en likhetskontroll:

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

Vissa funktionella programmeringsspråk stöder inte mutation alls. I F# stöds det, men det är inte standardbeteendet för värden.

Det här konceptet sträcker sig ännu längre till datastrukturer. I funktionell programmering har oföränderliga datastrukturer som uppsättningar (och många fler) en annan implementering än förväntat. Konceptuellt ändrar något som att lägga till ett objekt i en uppsättning inte uppsättningen, utan skapar en ny uppsättning med det extra värdet. Under täcket utförs detta ofta av en annan datastruktur som gör det möjligt att effektivt spåra ett värde så att lämplig representation av data kan ges som ett resultat.

Den här typen av arbete med värden och datastrukturer är viktig eftersom det tvingar dig att behandla alla åtgärder som ändrar något som om det skapar en ny version av den saken. Detta gör att saker som likhet och jämförbarhet kan vara konsekventa i dina program.

Nästa steg

I nästa avsnitt går vi igenom funktionerna noggrant och utforskar olika sätt att använda dem i funktionell programmering.

Med hjälp av funktioner i F# utforskas funktioner djupt och visar hur du kan använda dem i olika sammanhang.

Ytterligare läsning

Serien Thinking Functionally är en annan bra resurs för att lära sig mer om funktionell programmering med F#. Den beskriver grunderna i funktionell programmering på ett pragmatiskt och lättläst sätt med hjälp av F#-funktioner för att illustrera begreppen.