Az F# programozási nyelv

Objektum-orientált programozás

Osztályok

F# típusok osztálytagokkal

A nyelv lehetőséget ad a rekordok és uniók osztálytagokkal való bővítésére. Ezek a tagok lehetnek az objektumpéldányhoz (instance) kötött és osztályhoz kötött statikus függvények, változók vagy property-k. Az objektumpéldányhoz kötött tagfüggvényeket a következő szintaxissal definiálhatjuk: member objname.methodname(x) = utasítások. A legtöbb objektumorientált nyelvtől eltérően az objektumpéldányra, amin végrehajtjuk a műveletet, nem a this vagy a self kulcsszóval hivatkozunk, hanem a definícióban tetszőlegesen választott objname névvel. A statikus tagfüggvényeket a static member methodname(x) = utasítások szintaxissal definiálhatjuk, itt ki kell hagyni a objname azonosítót. A statikus osztálytagokra anélkül is hivatkozhatunk, hogy példányosítanánk a típusból. Általában arra használják őket, hogy értékeket rendeljenek egy típushoz, nem pedig annak egy adott objektumához.

type Vector2D = {X : float; Y : float} member v.Scale(a) = {X=v.X*a; Y=v.Y*a} member v.Negate() = {X= -v.X; Y= -v.Y} static member Zero = { X=0.0; Y=0.0}

Az osztálytagokat lehet úgy definiálni, hogy valamilyen értéket állítanak be, vagy adnak vissza. Ezek az osztálymetódusok lesznek a get és set módosítószval ellátva.

Alapértelmezésben az osztálytagok public hozzáféréssel rendelkeznek. Természetesen hozzáférési módosítóval is el lehet látni őket. Ahhoz, hogy hozzáadjunk egy hozzáférési módosítót az osztálytaghoz, írjuk a módosítószót (public, private, internal; lejjebb a Láthatóságnál olvashatunk róluk) az osztálytag neve elé. Ha a getre és setre egyaránt vonatkozik a módosítószó, akkor kell rögtön a név elé írni, viszont ha külön láthatóság vonatkozik rájuk, akkor a get vagy set elé kell írni a módosítószót.

F# Osztályok

Az osztályok az F# objektumorientáltságának egyik legfontosabb építőelemei. Az osztályok a C# osztályokhoz hasonló tulajdonságokkal rendelkeznek: elrejthetik a belső állapotukat, származhatnak ősosztályokból, lehet konstruktoruk és megvalósíthatnak interfészeket is. Az osztályok szintaxisa nagyon hasonlít a tagváltozókat tartalmazó rekordokéhoz, azonban itt kötelező egy konstruktor megadása. Az osztályok definícióját a következő szintaxissal kell megadni:

//az [ as azonosítónév ] még az első sorhoz tartozik!!! type [hozzáférési-módosító] típus_neve [típus-paraméterek] [hozzáférési-módosító] ( konstruktor-paraméter lista ) [ as azonosítónév ] = [ class ] [ inherit ősosztálynév(ősosztály-konstruktor-argumentumok) ] [ let-kötések ] [ do-kötések ] tag-lista ... [ end ]

Az osztály definíciójában két helyen is megadhatunk hozzáférési módosítószót. Az elsőt a type kulcsszó után (az osztály neve előtt), amivel az osztályra vonatkozó megszorítást adhatjuk meg. Ez alapértelmezetten public. A másodikat pedig a típus paraméterek után lehet megadni (a típus paraméterek a konstruktor paraméterek előtt szerepelnek, de az osztály neve után), ez pedig az elsődleges konstruktorra vonatkozik. A konstruktor paramétereknél a típus paraméterek nevei szerepelnek és/vagy megszorítások lehetnek, amiket < és > jelek közé írhatunk.

Az azonosítónév, melyet az as kulcsszóval adunk meg egy nevet ad a példányváltozónak vagy a self azonosítónak, amit arra használhatunk, hogy a típus definíciójában (osztály leírásában) hivatkozzunk az aktuális típuspéldányra.

Természetesen az osztályoknál lehetőség van az öröklődésre is, erről lejjebb olvashatunk.

Mezőket és függvényeket, amik lokálisak az osztályra nézve a let kötéseknél definiáljuk és a let kötésekre vonatkozó szabályokat kell követni ilyenkor. A do-kötések pedig olyan kódot tartalmaznak, amelyeknek az objektum létrehozásakor kell lefutnia.

A tag-lista újabb konstruktorokat, példány és statikus eljárás deklarációkat, interface deklarációkat, absztrakt kötéseket, tulajdonság és esemény deklarációkat tartalmazhat.

Ezeken felül lehetőség van arra, hogy kölcsönösen rekurzív típusokat is létrehozzunk, amik egymásra tartalmaznak hivatkozásokat. Ezeknek a definícióit az and kulcsszóval kötjük össze, ahogyan a kölcsönösen rekurzív függvényeknél is.

A class és end, amik az osztálydefiníció elejét ill. végét jelzik, opcionálisak.

type MyObj(x:int) = class let state=x+1 member v.say() = printfn "%d" state end //A class és end elemeket elhagyhatjuk ha megfelelően formázzuk a kódot type Vector2D(x:float, y:float) = let len = sqrt(x*x+y*y) member v.X = x member v.Y = y member v.Length = len static member Zero = Vector2D(x=0.0, y=0.0) new(x:float) = Vector2D(x,0.0)

Az első sorban a Vector2D-t egy konstrukciós kifejezéssel definiáljuk, ezután az x és y konstruktor paraméterek az osztály összes nem statikus tagjában használható. A második sorban egy privát adattagot adtunk meg, amit az F# mindig csak az osztály egy példányának létrehozásakor számol ki. A példa utolsó sorában pedig a konstruktor egy explicit túlterhelését adtuk meg, ami csak egy float paramétert igényel.

Láthatóság

Az osztályok member kifejezéssel megadott tagjaihoz explicit rendelhetünk láthatóságokat. Az F#-ban háromféle láthatóság van: private, internal és public. A private tagokat csak a definiáló osztály érheti el. Az internal tagokat a definiáló osztály és az osztállyal egy .Net assemblyben lévő osztályok érhetik el. A public tagokat pedig bármely osztály elérheti. A member-el definiált tagok alapértelmezés szerint public elérésűek. A let-el definiált tagok mindig private elérésűek.

type MyObj(x:int) = let s = x member public v.text = "Hi" member private v.Inc() = x+1

Struct-ok

A struct olyan kompakt szerkezet, ami az osztályoknál hatékonyabb megoldást nyújthat olyan típusok megvalósítására, amik kevés adatot tartalmaznak és egyszerű működéssel bírnak.

A struktúrák érték típusúak ami annyit jelent, hogy közvetlenül a stack-en tárolódnak vagy, ha mezőként vagy tömb elemként használjuk, inline módon a szülőben.
Az osztályokkal és a rekordokkal ellentétben, a struct érték szerinti átadással rendelkezik! A struct nem öröklődhet, nem tartalmazhat let és do kötéseket és nem tartalmazhatja a saját típusát.
(Bár tartalmazhat olyan referenciát, ami a saját típusát hivatkozza meg.) Mivel nem használható a let, így minden mezőt a val kulcsszóval kell deklarálnunk, továbbá megadhatunk saját konstruktorokat.

// 3 immutable érték van megadva // x, y, és z 0.0-nak inicializálódik type Point3D = struct val x: float val y: float val z: float end // 2 immutable érték van megadva // viszont van egy saját konstruktor // Létrehozhatunk zéró-inicializált példányt vagy átadhatunk paramétereket az mezők feltöltéséhez type Point2D = struct val X: float val Y: float new(x: float, y: float) = { X = x; Y = y } end

Nevesített és opcionális paraméterek

A nevesített argumentumok egy egyszerű koncepcióra épülnek: bármely tagfüggvény hívásakor lehetőségünk van, hogy a paraméterek megnevezésével adjuk át az értékeket és ne a paraméterek sorrendjének függvényében. Az opcionális paramétereket a paraméter neve elé rakott ? jellel deklarálhatjuk. Ezeknek a típusa mindig egy option<_> típus lesz, például egy int opcionális paraméterként option<int> típusú lesz.

Példa:

type MyObj(?text:string, num:int) = let txt = match text with | None -> "default" | Some v -> v let mynum = num let x = MyObj("hello", 5) let y = MyObj(num=1)

Property beállítók

Az objektumok létrehozásakor lehetőségünk van a konstruktor paraméterében értéket adni nevesített propertyknek. Az F# fordító a paraméterben átadott nevek közül kiválasztja azokat, amik nem egyeznek meg a konstruktor paramétereinek neveivel és megpróbál a kiválasztott nevű property-knek értéket adni. Például:

open System.Windows.Forms let form = new Form(TopMost=true, Text="Example") Megegyezik a következő kóddal: let form = let tmp = new Form() tmp.TopMost <- true tmp.Text <- "Example" tmp

Operátorok definiálása és túlterhelése

Az F# a legtöbb funkcionális nyelvhez hasonlóan lehetőséget a felhasználónak saját operátorok definiálására, akár osztályok között végzett műveletekhez is. A felhasználó által definiált operátorok szimbólumai a !$%&*+-./<=>?@^|~: jelekből állhatnak, azonban nem kezdődhetnek : jellel. Például a 2 dimenziós vektort a következőképpen egészíthetjük ki a skaláris szorzás operátorral:

type Vec2D(x:float, y:float) = member v.X = x member v.Y = y static member ( *. ) (v1:Vec2D, v2:Vec2D) = v1.X*v2.X+v1.Y*v2.Y //Használat Vec2D(1.0,2.0)*.Vec2D(1.0,3.0)

A típusainkban a .Net-es operátorokat is megvalósíthatjuk, és így a többi .Net nyelv által is használható operátorokat kapunk. Ennek a szintaktikája a következő:

type Vec2D(x:float, y:float) = member v.X = x member v.Y = y static member (+) (v1: Vec2D, v2: Vec2D) = Vec2D(v1.X+v2.X, v1.Y+v2.Y)

Metódusok túlterhelése

Az F#-ban lehetőség van a metódusok túlterhelésére, habár ezt az opcionális paraméterek és property beállítók mellett ritkán szokás használni.

type Test(start) = let s=start member v.S = s [< OverloadID("add1") >] static member (+) (t1:Test, t2:Test) = Test(t1.S+t2.S) [< OverloadID("add2") >] static member (+) (t1:Test, x:int) = Test(t1.S+x)

Az F# fordító 1.9.2.9-es verziója még nem megfelelően kezeli a túlterheléseket, ezért a különböző értékű OverloadID attribútumokkal segíteni kell a fordítónak.

Polimorfizmus, dinamikus kötés

Típuskonverzió

Típuskonverziónak egy adott változó statikus típusának megváltoztatását nevezzük. Az osztályok hierarchiája az obj (System.Object) típusnál kezdődik, ha ebben hierarchiában felfele lépve tesszük, akkor azt upcast-olásnak, ha a hierarchiában lefelé lépve, akkor pedig downcast-olásnak nevezzük a típuskonverziót.

Az upcast-olás biztonságos művelet, mivel fordítási időben eldönthető, hogy az adott változónak szerepel-e az ősei között a típus, amire konvertálni akarunk. Az upcast-olást a :> operátorral végezhetjük el.

let myobj = ("Test String" :> obj)

Az upcast-olást általában akkor használjuk, ha különböző típusokat akarunk egy tárolóba helyezni, majd ezeket egy közös felülettel kezelni.

open System.Windows.Forms let myControls = [| (new Button() :> Control) , (new Label() :> Control) |]

Upcast-olás során bármely .Net Framework-ös érték típus automatikusan box-olódik. Az érték típusok a programveremben tárolódnak, a box-olás során azonban ezek átkerülnek a managed heap-re, ahol már referenciaként hivatkozhatunk rájuk.

let myobj = (3 :> obj)

Downcast-olás során az objektumot egy leszármazott típusára konvertáljuk. Ez a művelet nem biztonságos, mivel fordítási időben általában nem dönthető el, hogy az adott változónak a dinamikus típusa kompatibilis-e a céltípussal. Ha nem kompatibilisek, akkor System.InvalidCastException kivételt fog kiváltani a típuskonverzió. A downcast-olást a :?> operátorral végezhetjük el.

let myobj = (5 :> obj) let myint = (myobj :?> int)

Típus lekérdezés

Sokszor hasznos lehet egy változó dinamikus típusát lekérdezése. Ezt F# -ban a :? operátorral tehetjük meg.

let myobj = ("Test" :> obj) if (myobj :? string) then printfn "myobj is a string"

Típus annotáció

Az F# típusrendszere szigorúbb, mint a más objektumorientált nyelvekben megszokott (pl.: C#) típusrendszerek. Ha egy típus annotációval rögzítjük egy kifejezés típusát, akkor csak a rögzített típusú változókat használhatjuk és azok leszármazottjait már nem.

let showform(form : Form) = form.Show() //A PrintPreviewDialog a Form-ból származó osztály let preview = new PrintPreviewDialog() showform preview

A fenti példaprogram nem fog lefordulni, mivel a PrintPreviewDialog statikus típusa nem Form, hanem annak egy leszármazottja.

Egy megoldás erre a problémára az lenne, hogy a kódban mindenhol a megfelelő upcast operátort használjuk. Azonban az F#-ban egy sokkal elegánsabb megoldás is létezik a problémára, ez a flexibilis típus annotáció. Egy típus annotációt a típusnév elé írt # jellel tehetünk flexibilissé. A flexibilis annotációk a megadott típust és összes leszármazottját is elfogadják, ahogy azt más objektumorientált nyelvek típusainál megszoktuk.

let showform(form : #Form) = form.Show() showform(new PrintPreviewDialog())

A showform paraméterének módosításával már lefordul a kódunk. A .Net Framework-ből származó kódok automatikusan flexibilis típus annotációval kerülnek importálásra az F# -ban.

Öröklődés

Mint minden objektumorientált nyelv, az F# is lehetőséget ad osztályhierarchiák kialakítására. Öröklődés során a már meglévő osztályainkat kiegészíthetjük plusz funkcionalitással. Ezt az inherit kulcsszóval tehetjük meg.
Az öröklődés egy ritkán használt technika az F# nyelvben, mivel az öröklődés komplexebbé teszi az objketumokat, a funkcionális programozás során pedig arra törekszünk, hogy egyszerű objektumok kompozíciójával érjük el a célunkat.

type Base(state : int) = member x.State = state type Derived(state) = inherit Base(state) member x.OtherState = state

Interfészek

Interfacek definiálása

Az F# interface-k a többi .Net Framework interface-hez hasonlóan csak publikus absztrakt metódusokat tartalmazhatnak. Az interface-k segítségével szétválaszthatjuk a definíciót felhasználó osztályokat az implementációk részleteitől. Egy osztálynak csak egy ősosztálya lehet, azonban egyszerre tetszőleges számú interface-t valósíthat meg.

type IMyListener = interface abstract StateChanged : obj -> unit end

Interfacek implementálása

Egy interface implementációját struct-ok és osztályok tartalmazhatják. Egy interface implementálásához először interface név with sorral kezdjük az implementációt. Majd member x.függvényneve = … szintaktikával tagfüggvényekként megadjuk az interface összes implementálandó függvényét, az implementációt végül az end kulcsszóval zárjuk.

type MyInterface = interface abstract say : unit -> unit end type Implementation = interface MyInterface with member x.say() = printfn "hi"

Objektum kifejezések

Az objetum kifejezések az interfacek és absztrakt objektumok implementálásához adnak egy egyszerűen használható módszert. Egy anonim rekordokhoz hasonló, anonim osztállyal implementáljuk a szükséges függvényeket.

open System.Collections.Generic let revcomparer = { new IComparer<string> with member x.Compare(s1:string, s2:string) = let reversed = new string(Array.rev(s2.ToCharArray())) s1.CompareTo(reversed) }

Delegate-ek

A delegate-ek úgy reprezentálnak egy függvényhívást, mint egy objektumot. Az F#-ban általában a függvény értékeket arra használjuk, hogy elsőrendű értékekként reprezentáljuk a függvényeket. Ezzel szemben a delegáltakat a .NET keretrendszerben használjuk, és akkor van rájuk szükség, amikor interoprációt hajtunk végre API-k között, amik számítanak erre. Akkor is használhatjuk őket, amikor olyan könyvtárakat engedélyezünk, amelyeket más .NET keretrendszer nyelveken íródtak.

type delegate-typename = delegate of type1 -> type2

A type1 reprezentálja az argumentum típust vagy típusokat és type2 adja meg a visszatérési típust. A type1 által reprezentált argumentum típusok automatikusan curry-zve vannak. Az automatikus curry-zés eltávolít zárójeleket, így olyan állapotba kerül a paraméterlista, ami megfelel a céleljárásnak.

Az Invoke metódus a delegált típusokon szolgál arra, hogy meghívja a közrezárt függvényt. Valamint a delegáltakat átadhatjuk függvény értékekként is, úgy, hogy az Invoke eljárás nevére hivatkozunk a zárójelek nélkül.

Példa:

type MyDelegate = delegate of string -> unit let inst = new MyDelegate(fun x -> printfn "%s" x) inst.Invoke("hello")

Reflection

A reflection szó alatt azt a folyamatot értjük, amikor egy program futás közben megfigyelheti, és módosíthatja a saját felépítését és viselkedését. Reflection használatával olyan osztályok adattagjait, és metódusait is elérhetjük és hívhatjuk, amelyekről nem rendelkezünk típusinformációval (pl.: Egy külső modul osztályai). Ezenkívül attribútumokat definiálhatunk az osztályoknak, metódusoknak, adattagoknak, melynek segítségével metaadatokkal láthatjuk el ezeket a kódrészeket.

Attribútumok

Ha valamely kódrészt attribútummal szeretnénk ellátni, használhatjuk a beépített attribútumokat, vagy saját magunk is definiálhatunk egyet. Egy attribútumot a [< és >] közé téve helyezhetjük el valamely programrészben.

type MyAttribute(text : string) = inherit System.Attribute() member this.Text = text [< MyAttribute("Hello world") >] type MyClass() = member this.SomeProperty = "This is a property"

Ezután az attribútumhoz reflection segítségével férhetünk hozzá.

> let x = new MyClass();; val x : MyClass > x.GetType().GetCustomAttributes(true);; val it : obj [] = [|System.SerializableAttribute {TypeId = System.SerializableAttribute;}; FSI_0028+MyAttribute {Text = "Hello world"; TypeId = FSI_0028+MyAttribute;}; Microsoft.FSharp.Core.CompilationMappingAttribute {SequenceNumber = 0; SourceConstructFlags = ObjectType; TypeId = Microsoft.FSharp.Core.CompilationMappingAttribute; VariantNumber = 0;}|]

Attribútumokat metódusokhoz, rekordokhoz, mezőkhöz, vagy kivételekhez is kapcsolhatunk ugyanúgy, mint osztályokhoz

type MyClass2() = //Attribútum kapcsolása privát mezőhöz [< SomeAttribute > ] let mutable MyVariable = 0 //Attribútum kapcsolása függvényhez [< SomeAttribute > ] let myFunction x = x + 1 // Attribútum kapcsolása felsorolási típushoz [< SomeAttribute > ] type Color = | [< SomeAttribute > ] Red = 0 | Green = 1 | [< SomeAttribute > ] Blue = 2 // Attribútum kapcsolása kivételhez [< SomeAttribute >] exception MyError of string

Egy objektumhoz akár több attribútumot is kapcsolhatunk, ekkor pontosvesszővel választjuk el az attribútumokat a [< >] jelek között.

[< SomeAttribute; SomeAttribute2 >] type MyClassWithGroupedAttribute() = class end

Típusinformációk

Reflection segítségével megvizsgálhatjuk az egyes típusokat, és futási időben hívhatjuk meg az objektumok metódusait, kérdezhetjük le a tulajdonságait, akár a privát adattagjait is megváltoztathatjuk anélkül, hogy az objektum típusát fordítási időben ismernénk.

Típusvizsgálat

Többféle lehetőségünk is van az objektumok típusinformációinak lekérdezésére. A tradicionális megoldás a System.Object osztálytól örökölt GetType() metódus hívása bármely nem null értékű objektumra:

> "hello world".GetType() val it : System.Type = System.String {Assembly = mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089; AssemblyQualifiedName = "System.String, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"; Attributes = AutoLayout, AnsiClass, Class, Public, Sealed, Serializable, BeforeFieldInit; BaseType = System.Object; ContainsGenericParameters = false; ...}

Objektumpéldány nélkül is lekérdezhetjük egy osztály típusinformációját a beépített typeof metódussal.

> typeof < System.IO.File> ;; val it : System.Type = System.IO.File {Assembly = mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089; AssemblyQualifiedName = "System.IO.File, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"; Attributes = AutoLayout, AnsiClass, Class, Public, Abstract, Sealed, BeforeFieldInit; BaseType = System.Object; ...}

Mind az Object.GetType(), mind a typeof metódus egy System.Type példánnyal tér vissza, amelynek tulajdonságain keresztül érhetjük el a típusinformációkat az osztályunkról.

Az InvokeMember metódussal meghívhatjuk az objektum egy megadott nevű, azon metódusát, amelyre az adott argumentumlista megfelel.

Példa: Tulajdonságok olvasása

Reflection segítségével lekérdezhetjük az egyes példányok tulajdonságait, és azok értékét. Az alábbi program kiírja a paraméterként átadott objektum tulajdonságait:

type Car(make : string, model : string, year : int) = member this.Make = make member this.Model = model member this.Year = year type Fruit(name : string, isExotic : bool) = member this.Name = name member this.IsExotic = isExotic let printProperties x = let t = x.GetType() let properties = t.GetProperties() properties |> Array.iter (fun prop -> let value = prop.GetValue(x, null) printfn "%s: %O" prop.Name value) let carInstance = new Car("Ford", "Focus", 2009) let fruitInstance = new Fruit("Mango", true) printProperties carInstance printfn "\n" printProperties fruitInstance

A fenti program kimenete a következő:

Make: Ford Model: Focus Year: 2009 Name: Mango IsExotic: True
Példa: Privát mezők elérése és módosítása

A reflectiont számos további esetben használhatjuk, akár egy objektum privát (és immutable) mezőinek értékét is megváltoztathatjuk:

open System.Reflection type MyType() = class let _myField = 5 member this.MyField with get() = _myField end let myTypeInstance = new MyType() let field = myTypeInstance.GetType().GetFields(BindingFlags.NonPublic ||| BindingFlags.Instance).[0] field.SetValue(myTypeInstance, 12) printfn "%d" myTypeInstance.MyField
A Microsoft.FSharp.Reflection névtér

A reflection F# típusokra történő kiegészítése a Microsoft.FSharp.Reflection névtérben található, itt a beépített típusokra, mint: Unió, Tuple, stb. találhatóak reflection metódusok.