F# - brugbar funktionel programmering
For en god uges tid siden dumpede bogen "Expert F#" endelig ind af min brævsprække - noget jeg har glædet mig til i et stykke tid, da jeg har hørt mange spændende ting om sproget. Inspireret af at jeg engang læste at man bør lærer sig et nyt programmeringssprog hvert år synes jeg det var det perfekte næste skridt for mig. Jeg har absolut ingen erfaring med funktionel programmering udover det jeg har rodet med LINQ - hvilket ikke er helt "rigtig" funktionel programmering men rettere funktionel funktionalitet indarbejdet i et Objekt Orienteret sprog.
Idet jeg næppe er den eneste der ikke har rodet med funktionel programmering, da det hovedsageligt har hørt hjemme på universiteterne hidtil, vil jeg skrive en smugle om nogen af de basale ting i funktionel programmering og F#. Hvis du tænker "hvis det hvedsageligt hører hjemme på uni, hvad kan jeg så bruge det til i praksis", så er pointen netop at F# udover at være et funktionelt sprog, også giver mulighed for at tilgå .NET frameworkets mange herligheder, og arbejde imperativt hvis man vil. Jeg tror F# har gode muligheder for at vinde frem netop
af den årsag, da det tilbyder fantastisk ekspressivitet sammen med adgang til de ting vi bruger .NET frameworket til allerede.
Lidt basics
Lidt ligesom der i VB findes option strict og explicit er der i F# "#light", som aktiverer det der kaldes light syntaks. Med andre ord slipper man for at skrive en masse ting og afslute linier manuelt osv. Eller kort sagt, bare skriv det øverst i din kode!
Kommentarer i F# minder langt hen ad vejen om den måde man skriver dem på i C#, med undtagelse af flerlinies kommentarer.
// Her er en enlinies kommentar.
(* Her står der en
flerlinies kommentar *)
///<summary>Xml kommentarer ligner også sig selv som i C#</summary>
Lidt ligesom man har Dim, var o. lign. ord der deklarerer en variabel har man i F# ordet "let" der definerer starten på en funktion. For at illustrere og vise hvordan scope fungerer er her et eksempel:
let powerOfTwo number
let result = number * number
result
Her definerer man en funktion der hedder powerOfTwo, som tager et argument "number". Funktionen består af en lokal funktion der er tallet ganget med sig selv. Man kan også se at inferens af typer bruges i meget vid udstrækning og at indrykning bruges til at afgrænse scope ligesom i (Iron)Python.
For at undgå at skulle ud i den helt langt gennemgang af simple typer og operatorer vil jeg nøjes med som udgangspunkt at sige at de langt hen ad vejen er de samme som i C# og ellers er der masser af steder man kan finde oversigter over dem via vores allesammens bedste ven google.
Det er vigtigt at vide at operatorer er unchecked. Operationer der giver overflow kaster altså ikke exceptions men de wrappes istedet. En anden ting der er vigtig at forstå er at de fleste typer er immutable med mindre det explicit angives at de skal være mutable. Det giver en række fordele, da man derved kan være sikker på at en værdi ikke ændres "bag om rykken" på en. Noget der især er rart når man arbejder trådet da man så ikke skal bekymre sig så meget om "concurrent access".
Grundlæggende data strukturer
Fleksible datastrukturer er en af hovedingredienserne i et funktionelt sprog, da de gør det muligt at skrive meget ekspressiv kode til at arbejde med mængder af data.
Tuple:
Tupler bruges til at returnerer flere værdier fra en funktion, som ikke nødvendigvis er af samme type. En tupel angives helt enkelt med en parentes hvori værdierne adskilles af en komma. En funktion der tæller ord kunne for eksempel returnere, totalt antal ord og antallet af unikke ord.
let wordCount = (10, 8)
fst og snd er keywords der bruges til at anvende det første og andet element i en touple - og dermed til at arbejde med par.
let totalWordCount = fst wordCount
En anden mulighed er at decompose tuplen i flere værdier.
let totalWordCount, totalUniqueWordCount = wordCount
Array:
Arrays kender de fleste nok ganske godt så dem vil jeg gå lidt let hen over. En vigtig ting at nævne er dog at arrays er den eneste af de collection strukturer jeg skriver om her der er mutable. Et array i F# defineres således:
let myArray = [|1;2;3;4;5|]
En anden funktion der er værd at nævne er at man kan definere et litteral byte array som:
let myArray = "MAGIC"B // Det svarer til [|77uy; 65uy; 71uy; 73uy, 67uy|]
Array slicing som ellers er en ting man godt kan savne lidt i andre .NET sprog kan desuden laves således.
let slicedArray = myArray.[1 .. 3]
List:
List i F# er en implementation af en singularly linked list som er en struktur der læses hurtigt hvis man læser elementerne igennem fra
start til slut.
let oneHalfOfTheFamily = ["Claus";"Lena";"AC"]
let anotherHalfOfTheFamily = ["Allan";"Janni";"Peter";"Jacob"]
let bothHalvesOfTheFamily = oneHalfOfTheFamily @ anotherHalfOfTheFamily
Her set man to lister blive oprettet og smeltet sammen. Det er vigtigt at huske at lister er immutable, så efter dette er sket er værdierne af de to første lister uændrede.
Set:
Sets indeholder ordnede lister hvor der kun kan være 1 af hver værdi, og sets er case sensitive. Et eksempel illustrerer meget godt hvordan den bruges.
let myset = Set.of_list ["BAC";"ABC";"ABC"]
Vil give en Set<string> med indholdet ["ABC";"BAC"]
Sets kan laves til lister eller laves fra lister ved hjælp af to_list og of_list
Option:
Options bruges til at beskrive en struktur der enten har en værdi eller er None. Keywordet her er "Some" og kan bruges således.
let childAndParents = [ ("Jens", Some("Ib", "Hanne"));"Jonas", None]
Options har funktioner som get der returnerer værdien af en some option og is_option der returnerer true for en some option.
Sequence:
Sequences er en abstraktion over datastrukturer der kan læses sekventielt og er alt der implementerer IEnumerable, som man kender det fra andre .NET sprog. I F# har sekvenser deres eget keyword som er seq. Det vil sige alle typer der er IEnumerable kan arbejdes med som sekvenser, det inkluderer arrays, lists osv. En smart F# feature er at man kan beskrive ranges af værdier i sekvenser på følgende måde.
let myNumbers = seq {0 .. 5}
Som vil give en sequence af værdier fra 0 til 5. Denne liste er lazy således at værdierne ikke genereres før de skal bruges. Dette er meget vigtigt hvis man skal arbejde med rigtigt lange sekvenser. Man kan desuden definere et interval i sevensen således.
let myNumbers = seq {1 .. 2 .. 6}
Som vil give værdierne [1;3;5]
Eller man kan definere en sequence ud fra f.eks. et for expression:
let powersOfTwo = seq { for i in 0 .. 10 -> i*i }
List og Seq indeholder desuden en række funktioner som eksempelvis map, iter, filter, to_array og of_array. De først nævnte gør det nemt at arbejde med strukturerne ved at passe en funktion ind.
powersOfTwo.iter(fun number -> Printf.printf "Number = %s\n" (number)
Functionelle værdier
En anden vigtig bestantdel i et funktionelt sprog er naturligvis muligheden for at arbejde med funktionerne. En række metoder som for eksempel map og iterate der findes på blandt andet List og Sequence tager imod en funktion som argument. Map bruges til at omforme alle elementer i strukturen og iterate bruges til at iterere strukturen og udføre funktionen på hvert element. En funktion kan som shorthand skrives som:
let namesWithH = Names.Filter(fun name -> name.StartsWith("J"))
Man har desuden mulighed for at "pipe" funktioner, sådan at en funktion kaldes med retur værdien fra den forrige funktion. Piping er en god
måde at gøre kode mere let læseligt på da det kan læses fra højre mod venstre og ikke indefra ud som man ellers tit ender med at skulle.
let PrintNamesWith prefix =
NamesWith(prefix) |> List.iter(fun name -> Printf.printf "Name = %s\n" (name))
Man kan også arbejde med sammensatte funktioner og partiel applikation. Disse to teknikker viser hvor fleksibelt sproget er når man arbejder med funktioner.
let getNames s = String.split [';'] s
let countNames = getNames >> List.filter(fun (name:string) -> name.StartsWith("M")) >> List.length
let NameCount = countNames "Mia;Christian;Mig"
Eksmeplet viser hvordan man givet en funktion "getNames", kan definere en ny funktion countNames, som er en sammensætning af denne, List.filter og List.length - som derved kan bruges til at tælle antallet af navne i en semikolon separeret streng. F# har en meget powefull engine til at sammensætte effektive funktioner på denne måde.
let shift (x, y) (x1, y1) = (x1+x, y1+y)
shiftright = shift(1,0)
Dette eksempel viser helt enkelt at man kan definere en ny funktion ved at kalde en anden funktion med kun nogen af de argumenter den kræver. Dette giver mulighed for at være meget beskrivende i sin kode ved at lave en række partielle funktioner der løser specifikke opgaver på baggrund af en mere generel funktion.
Konklusion
Jeg håber ovenstående givet et godt indblik i nogle af de mest basale strukturer og teknikker der anvendes i F# i forbindelse med funktionel programmering. Som tidligere nævnt er F# ikke et "rent" funktionelt sprog så man kan også arbejde med imperativ programmering i sproget. Det kan være det bliver et emne jeg tager op senere. Selvom jeg er ny indenfor funktionel programmering synes jeg hurtigt man får et indtryk af hvor ekspressiv kode man kan skrive og at det er vejen frem især hvis man skal kode en applikation til et trådet miljø.