F# funktionelle typer og objekt-orienteret programmering

by DotNetNerd 27. January 2008 17:51

F# funktionelt eller objektorienteret?

En stor del af dem der har hørt om F# har hørt at det er et funktionelt programmeringssprog, hvilket er sandt nok - det er bare ikke udelukkende funktionelt. Faktisk understøtter sproget i lige så høj grad at man kan programmere objekt orienteret med imperative teknikker. Det er faktisk netop derfor F# er brugbart i ”den virkelige verden” i modsætning til eksempelvis Haskell der stort set udelukkende benyttes til undervisning i praksis.
Jeg vil i dag kigge på forskellige teknikker til at arbejde med typer, først med udgangspunkt i funktionel programmering og sidenhen med fokus på objektorientering.

Simple typer til funktionel programmering

I funktionel programmering arbejder man meget med objekter på to måder, nemlig med records og discriminated unions. Records svarer til det man ellers kalder en entitet, som er et helt simpelt databærende objekt.

type Employee =
  { Name: string;
    HiredDate: System.DateTime; }

Som så kan bruges således, hvor type udledes via ducktyping - if it walks like a duck and it talks like a duck, its probably a duck.

let employee1 =
  { Name = "Christian";
    HiredDate = System.DateTime.Parse("11/07/2003") }

Eller således.

let employee2 =
  { new Employee
    with Name = "Hans"
    and HiredDate = System.DateTime.Parse("11/07/2003") }

En smart feature der skal nævnes I den forbindelse er at det er nemt at klone records, så hvis man skal lave en ny record der næsten er magen til en anden kan man eksempelvis gøre det sådan her.

let employee1Cloned = { employee1 with HiredDate = System.DateTime.Parse("15/10/2005") }

Discriminated Unions bruges til at modellere et endegyldigt og lukket sæt af muligheder der ikke kan ændres. Man arbejder typisk med med discriminated unions ved først at definere nogle type forkortelser, som ganske enkelt består i at man giver en eksisterende type et alias.

type Route = int
type Make = string
type Model = string

Derefter kan man så bygge sit discriminated union ved hjælp af forkortelserne sådan her.

type transport =
  | Car of Make * Model
  | Bicycle
  | Bus of Route

Som man forhåbentlig kan se ud af det her er det en meget præcis måde at modellere del slags data på. Ud fra ovenstående kan man nu arbejde med typerne sådan her.

let myMeansOfTransportation = [ Car("Skoda", "Fabia"); Bicycle; Bus 128 ]

Det har muligvis allerede slået dig at den her måde at modellere på minder om ”enumerations på speed”, og rent faktisk opbygges enums i F# sådan her.

[<Struct>]
type Shape =
  | Circle = 1
  | Rectangle = 2
 

Objektorienteret programmering

Den vigtigste egenskab ved objekter er at de indkapsler mutable værdier, og bruges dermed til at indkapsle tilstand - således at omdrejningspunktet for koden er tilstands checks og tilstands ændringer.
Som jeg skrev i sidste post er værdier som udgangspunkt immutable i F#, hvilket er en stor styrke idet man derved ligger op til at tænke i at skrive kode der ikke afhænger af tilstandsstyring hvilket hurtigt kan blive rodet og svært at vedligeholde. Det kan imidlertid være nødvendigt med mutable værdier, og i nogen tilfælde er det med til omvendt at gøre kode lettere at læse og mere effektivt.
Helt enkelt gøres en værdi mutabel ved hjælp af nøgleordet ”mutable” og den kan derefter modificeres ved hjælp af operatoren <- på samme måde som man kender der fra VB og C# med = tegnet.

let mutable n = 0
n <- 6
printfn "%d" n

En anden velkendt teknik er at anvende mutable reference celler ved hjælp af nøgleordet ref og operatorerne := til at sætte en værdi og ! til at hente den, som du kan se brugt her.

let n = ref 6
n := 7
printfn “%d” !n

Andre uundværlige teknikker fra objektorienteret programmering er boxing/unboxing af værdityper og upcasting/downcasting imellem typer. Boxing af en type vil sige at wrappe dem til objekter, således at de kan gives til metoder der tager typen object.

let o = box 6
let i = unbox<int> o  // kan også skrives som let i = unbox o : int

Upcasting bruges til at caste en type til en type den arver fra.

let stringObject = ("qwerty" :> obj)

Downcasting bruges til at caste en variable til en mere nedarvettype som den er en instans af.

let myString = stringObject  :?> string
 

Members - objektets ansigt udadtil

Som tidligere nævnt er indkapsling en nøgleegenskab i objektorienteret programmering. Ligesom i andre .NET sprog  kan værdier pyntes med modifieren private hvis de ikke skal være tilgængelige udefra. Derudover kan man naturligvis lave members der stilles til rådighed for andre som vi nu skal se nærmere på.
Mutable properties er nok den mest anvendte membertype, idet de helt enkelt bruges til at stille data til rådighed. En property ser sådan her ud i F#.

let mutable text = ""
member t.Text
  with get() =
    text
  and set v =
    text <- v

Indexers bruges på samme måde, men bruges til at tilgå data som en collection af data der tilgås via en eller flere indeks parametre.

let elems = [| "A"; "B" |]
member t.Item
  with get(id) = elems.[id]
 

Argumenter der holder

I F# kan man benytte sig af teknikker som optionelle- og navngivne argumenter. Optionelle argumenter angives ved hjælp af ? hvilket gør at værdien bliver en option og den kan altså dermed være ingenting.
defaultArg funktionen bruges til at trække værdien ud eller alternative en default værdi hvis optionen er None.

type LabelInfo(?text:string) =
  let text = defaultArg text ""

Named arguments er en anden teknik der relaterer sig til at arbejde med argumenter. Ved at bruge named arguments angiver man værdien for et argument præcist frem for ud fra argumentets placering. En smart feature i den forbindelse er at man kan angive argumenter som der ikke nødvendigvis er en constructor til så længe typen har en public property til den angivne parameter.

let form = new Form(Visible=true, TopMost=true)
 

Implementation af interfaces

Nu er det vidst ved at være tid til at se på hvordan man implementerer en klasse som vi kender den fra VB eller C#. For at starte fra bunden ses her hvordan man deklarerer et interface.

type IShape =
  abstract Contains : Point -> bool

Ovenstående interface kan derefter implementers af en funktion på følgende måde.

let Circle() =
  { new IShape with
    member x.Contains(p:Point) = true
  }

Teknikken der er anvendt her kaldes object expressions og det er en simpel teknik der anvendes ofte når man implementerer interfaces eller extender typer.
Definitionen af en object expression er sådan her.

{ new Type optionalle-argumenter with
            Member-definitioner
  Optionalle-ekstra-interface-definitioner }

En implementation af et interface på en konkret type som man nok oftere vil bruge den kan se sådan her ud.

type MutableCircle() =
  let mutable center = Point(x=0, y=0)
  member c.Center with get() = center and set(v) = center <- v
  interface IShape with
    member c.Contains(p:Point) = true
 

Brug af delvist konstruerede typer

Delvist konstruerede typer er typer der har abstrakte members, som skal implementeres af en specialiseret type. Alternativt kan man angive en default implementation som kan overskrives af den type der nedarver.

type Formatter() =
  abstract Format : string -> string
  abstract ReplaceWhitespace : string -> string
  default x.ReplaceWhitespace(s:string) = s.Replace(" ", s)

En sådan klasse kan implementers ved hjælp af object expressions.

let myFormatter =
  { new Formatter() with
    member f.Format(s) = s }

Eller den kan ligeledes implementeres via implementation inheritence.

type HtmlFormatter() =
  inherit Formatter()
  default f.Format(s) = s

I forbindelse med implementation af en given member kan nøgleordene override og default bruges til at indikere om man overskriver en metode der har en implementation i forvejen eller ej.

Statiske metoder og extension methods

Statiske metoder i F# implementeres ikke på en klassen, men på et module. Ønsker man at en statisk metode skal ligge på en klasse (eller dele navn med den om man vil) skal modulet dekoreres med en CompilationRepresentation attribute.

[<CompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix)>]
module MyOperations =
  let MyStaticMethod(i:int, i2:int) = "Taller er " + (i + i2).ToString()

En relateret teknik er deklerationen af extension methods som også for nylig er kommet med i VB og C#.
En extension method i F# deklareres som en del af modulet med følgende syntaks.

type System.Int32 with
    member i.Add(i2:int) = MyStaticMethod(i, i2)

For at bruge extension metoden skal man derefter blot åbne modulet, på samme måde som i VB.

open MyOperations

let i = 1
let s = i.Add(12)
 

Afrunding - muligheder for fremtiden

Jeg håber at have illustreret med ovenstående gennemgang at man kan bruge F# til at arbejde fuldt ud objekt-orienteret, på holde med det man kan i andre .NET sprog. Netop det at man kan mix and matche imellem paradigmer er efte rmin mening den helt store styrke ved F#. Jeg er selv kun ved at komme igang med sproget og kaster mig i næste uge ud i at lave mit første modul i sproget, som bliver et stregkodeparser til mig FLOwer Tracker system. Jeg håber det har givet andre lyst til at prøve kræfter med sproget som jeg nok ender med at skrive meget mere om med tiden efterhånden som det vinder frem og bliver kørt ind i .NET frameworket fuldt ud som planen er fra Microsofts side.

 

Tags:

Who am I?

My name is Christian Holm Diget, and I work as an independent consultant, in Denmark, where I write code, give advice on architecture and help with training. On the side I get to do a bit of speaking and help with miscellaneous community events.

Some of my primary focus areas are code quality, programming languages and using new technologies to provide value.

Microsoft Certified Professional Developer

Microsoft Most Valuable Professional

Month List