Az F# programozási nyelv

Szabványos könyvtárak

Az F# könyvtárai

F#-ban a következő három fő könyvtár érhető el alapértelmezésben:

Továbbá a következő könyvtárakat szokták még használni F# alkalmazásokban:

C# és más .NET nyelvek és elemek direkt használata F#-ból

Az alapok

Típusok: A típusokat a "Namespace-neve.Type" előjelzővel használhatjuk. Egyszerűen "Type" is irható, ha egy "open Namespace-neve" deklarációt is megadtunk a program elején.

Konstruktorok: Egy .NET típus példányának létrehozására a new Type(arg1,...,argN) alakú kifejezések használhatók.

Metódusok és property-k: Objektumok metódusait, property-it és attribútumait a "obj.Method(arg1,...,argN)", "obj.Property" illetve "obj.Field" szintaktikával érhetjük el.

Statikus metódusok és property-k: Statikus tagfüggvényeket "Namespace.Type.Method(arg1,...,argN)" vagy a "Type.Method(arg1,...,argN)" hivatkozással érhetjük el, hasonlóan a property-khez és attribútumokhoz.

Például a System.IO.FileStream használata

let x : System.IO.FileStream = System.IO.FileStream.Open("myfile") //vagy open System.IO let x : FileStream = FileStream.Open("myfile")

Void: A „void” megfelelője F#-ban a "unit" .Például a System.Console.WriteLine() típusa "unit"

Delegate-ok (függvénypointerek): Delegate-okat egyszerűen a delegate kulcsszó használatával és a megfelelő argumentumlista megadásával definiálhatunk. Például, ha a delegate szignatúrája a következő: delegate void EventHandler(object,EventArgs) majd adjuk át a függvényt, melynek típusa: obj -> EventArgs -> unit, ahol a típusellenőrző meg fogja tenni a szükséges típuskényszerítéseket.

Event-ek: Az event Add/AddHandler/RemoveHandler/Fire metódusaival használhatjuk őket. Az Add egy függvény értéket kap paraméterként, a többi függvény használata egyszerûbb, delegált értékeket várnak.
Például: menu.Click.Add(fun evArgs -> expr), ahol expr egy callback függvény helyes implementációja. Az események szintén használhtók a Microsoft.FSharp.Idioms.IDelegateEvent és Microsoft.FSharp.Idioms.IEvent típusok értékeként.

open System.Windows.Forms;; let form = new Form();; let guiRefresh g = printf "refresh!\n";; form.Paint.Add(fun e -> guiRefresh e.Graphics);; let handler = new PaintEventHandler(fun sender e -> guiRefresh e.Graphics);; form.Paint.AddHandler(handler);; form.Paint.RemoveHandler(handler);;

Interfészek implementálása

Az F# értékek, amelyek .NET interfészeket implementálnak, objektum kifejezések használatával generálhatók.

Egy objektum kifejezés egy osztály vagy interfész kiterjesztettjét és/vagy implementációját deklarálja. Példának vegyük a következõt, az F# beépített, többalakú < és > operátorok funkcionalitását használjuk, hogy létrehozzunk egy objektumot, amely implementálja a .NET IComparer interfészt.

{new IComparer with Compare(a,b) = if a < b then -1 else if b < a then 1 else 0 }

Jellemzõen az F# egy új osztályt í, ami az adott típus kiterjesztettje vagy implementációja.

open System open System.Collections open System.Windows.Forms let myComparer = {new IComparer with Compare(a,b) = compare a b} let myFormattable = {new IFormattable with ToString(fmt,fmtProvider) = ""} let myForm title n = let res = {new Form() with OnPaint(paintEventArgs) = Printf.printf "OnPaint: n = %d\n" n and OnResize(eventArgs) = Printf.printf "OnResize: n = %d\n" } in res.Text <- title; res

Ha a virtuális propertiket szeretnénk felülírni vagy implementálni, akkor a "get_PropertyName" és "set_PropertyName" metódusokat kell felüldefiniálni, amelyek implementálják a propertiket. Hasonlóan mûködnek a virtuális eventek is, itt pedig az "add_EventName", "remove_EventName" és "fire_EventName" metódusokat kell felüldefiniálni.

Az objektum kifejezésen keresztül létrehozott objektumok szintén támogatják a többszörös interfészeket. A szintaxisa:
{ new with
interface with
...
interface with }

{ new Object() with Finalize() = cleanupFunction(false); interface IDisposable with Dispose() = cleanupFunction(true); }

Az interfészek nem részei az objektum kifejezéses típusnak, de a típus teszteken keresztül felderíthetõk.

Boxing System.Object-té és vissza

Minden F# és .NET érték konvertálható System.Object típusura - amit F#-ban obj-nek hívnak. Ennél fogva az F# értékek tárolhatók olyan heterogén gyûjteményekben mint a System.Collections.ArrayList. A vissza felé történõ konverzió dinamikusan ellenõrzött és így hibákat okozhat. Az F# a következõ operátorokat támogatja a konvertáláshoz oda, és vissza erre a típusra:

box: 'a -> obj
unbox: obj -> 'a

Típuskényszerítések felfelé és lefelé

Az upcast(típus kényszerítés felfelé) és boxing kikényszerítése akkor történik meg, amikor átadjuk az értéket a függvényeknek, amelyek elfogadnak általánosabb típusokat az F# kényszerítési relációja szerint. A kényszerítések akár mikor alkalmazhatók, amikor a feltételek teljesülnek egy paraméter elõírása és aktuális értéke között. Egy boxing kényszerítés csak akkor fog teljesülni, ha egy F# értékt egy .NET érték típus és a kényszerítés célpontja egy .NET referencia típus (tipikusan System.Object). A .NET és F# minden osztálya és interfésze konvertálható a bázis típusaik vagy interfészeik tranzitív lezártjára. Más F# típusok (rekordok, diszkriminánsos uniók és absztrakt típusok) nem rendezhetõk hierarchiába és így csak obj típusra vagy olyan interfész típusra konvertálható, amelynek explicit bõvíthetõk a típusai.

.NET típusok downcastolhatók is olyan típusra, amelyek benne vannak a kényszerítési relációban, ezek futásidõben kivételeket válthatnak ki. F# támogatja a következõ operátorokat, amelyek .NET értékrõl konvertálnak tiszteletben tartva a kényszerítési relációt:

expr==
| e :? ty -- dinamikusan teszteli, hogy 'e' 'ty' típusú-e. Fordítási idejû hibát okoz, ha a lokális változó típusa nem olyan, hogy érvényes lehet a lefelé irányuló típus tesztre.
| e :> ty -- statikus felfelé történõ típus kényszerítés 'e'-t 'ty'-ra. Fordítási idejû hibát okoz, ha a lokális változó típusa nem olyan, hogy érvényes lehet az upcast.
| e :?> ty -- dinamikusan lefelé történõ típus kényszerítés 'e'-t 'ty'-ra. Fordítási idejû hibát okoz, ha a lokális változó típusa nem olyan, hogy érvényes lehet a downcast.
| downcast e -- futás idejû downcast, 'e'-t egy tetszõleges a környezetében látható típusúra konvertálja. Fordítási idejû hibát okoz, ha a lokális változó típusa nem olyan, hogy érvényes lehet a downcast. Megjegyzés: a struktúra átdolgozás alatt
| upcast e -- statikusan ellenõrzött upcast'e'-rõl egy tetszõleges a környezetében látható típusúra. Fordítási idejû hibát okoz, ha a lokális változó típusa nem olyan, hogy érvényes lehet az upcast. Megjegyzés: a struktúra átdolgozás alatt és lehet, hogy elavult

pat==
| :? ty -- Ez a minta megegyezik egy .NET típus teszttel. Fordítási idejû hibát okoz, ha a lokális változó típusa nem olyan, hogy érvényes lehet a lefelé irányuló típus tesztre.
| :? ty as id -- Ez a minta megegyezik egy .NET típus teszttel. Ha sikeres akkor az 'id' változót az adott típusú értékhez köti.

Az e :? ty kifejezés ekvivalens egy dinamikus típus teszttel. Egy figyelmeztetést kapunk, ha az e típusról statikusan nem meghatározható, hogy 'ty'-nak egy altípusa. Egy error fog megjelenni, ha atípus teszt mindig sikerrel jár.

A :? minta különösen jól használható .NET kivételosztályok tesztelésére. Például:

try ... with | e :? System.Net.Sockets.SocketException -> ... | e :? System.OutOfMemory -> ...

Érvényes kasztolás lehetséges a .NET típusok és azok osztály- vagy interfész kiterjesztettje között, valamint az F# referencia típusai és 'obj' típusa között.

Nullness

F# kódban a .NET referencia ípusok null értékez is felvehetnek. A null érték ellenõrizhetõ és a következõ konstrukciókat kozták létre:

expr==
| null -- egy tetszõleges típusnak a null értékét generálja a közvetlen környezetbõl. Fordítási idejû hibát okoz, ha a lokális típus információ nem garantálja, hogy a típus érték biztosan egy .NET referencia típus.

pat==
| null -- a minta egy null tesztnek felel meg. Fordítási idejû hibát fog okozni, ha a lokális típus nem biztosítja, hogy az érték, amivel összehasonlítják az egy .NET referncia típus.

Itt egy példa, ahol egy null tesztet valósítunk meg mintával:

let d = new OpenFileDialog() in if d.ShowDialog() = DialogResult.OK then match d.OpenFile() with | null -> Printf.printf "Ooops... Could not read the file...\n" | stream -> ... let r = new StreamReader(stream) in Printf.printf "The first line of the file is: %s!\n" (r.ReadLine());

Itt egy példa, ahol a null tesztet kifejezéssel használjuk:

let stream = new FileStream("hello.txt", FileMode.Open) let myReader = new StreamReader(stream) in while true do Console.WriteLine(myStream.ReadLine() ?? raise End_of_file); done;

Forráskód szinten a null nem egy érvényes érték más F# típusok számára és ha valaki megkísérli a null ellenõrzõ konstrukciók használatát olyan típusokkal, mint az int akkor a fordító panaszkodni fog. Abban az esetben szintén, ha egy null ellenõrzést hajt végre vagy egy null érékez generál egy változónak, de néhány F# értékre használható lesz.

A teljes F# kódban gyakran felteszik, hogy a .NET referencia értékek nem null-ok. Ezért egy jó megoldás, hogy ha kiküszöböljük a null-t olyan hamr, amint lehet. Igazából csak az API-knál lenne szükséges használatuk.
A null nem része egyébként az F# rendszernek, ezen oknál fogva a fordító nem ellnörzi, hogy lett e ellenörzés téve az API hívások után. Ha nem végez ellenörzést akkor a C#-hoz hasonlóan NullPointerException-t vált ki, amikor a az objektumon meghívódik egy metódus.

Megjegyzés: lehetséges generálni null értéket minden F# típushoz számos hátsó ajtón keresztül, például veszünk egy null objektumot és egy F# típussá alakítjuk.

Példa
A következő példa azt mutatja be, hogy miként érhetjük el F#--ból a .NET Reflection könvvtárát. Kékkel jelöltük azokat a kódrészleteket, ahol a .NET könyvtárakkal kommunikálunk.

open System open System.Reflection open System.IO type search = | ExactMatch | NamespaceMatch let kind = ref ExactMatch let interfaces = ref false let basetypes = ref false let modinfo = ref false let includes = ref [] let deep = ref false let findDLLs dir = (* call a static member in the System.IO.Directory class *) if (Directory.Exists dir) then let files = Directory.GetFiles(dir, "*.dll") in Arr.to_list files else [] let print_indented ind s = for i = 1 to ind do print_char ' '; done; print_endline s let rec dumpType ind shallow (t:Type) = let baseType = t.BaseType in (* access a property in the Type class *) let desc = if t.IsClass then "class" else if t.IsInterface then "interface" else if t.IsValueType then "struct" else if t.IsArray then "array" else "??" in print_indented ind (desc^" "^t.FullName); if not shallow then begin if !kind = ExactMatch or !modinfo then print_indented (ind+2) ("Module: "^ t.Module.FullyQualifiedName ); if !kind = ExactMatch or !interfaces then begin let intfs = t.GetInterfaces() in for i = 0 to intfs.Length - 1 do dumpType (ind+2) true intfs.[i]; done; end; end; if (!basetypes & nonnull baseType) then dumpType 0 shallow baseType let searchFile (pat:string) file = match (try Some (Assembly.LoadFrom(file)) with _ -> None) with | Some a -> let modules = a.GetModules() in let pat = pat.ToUpper() in prerr_endline("Searching Module "^file); if (nonnull modules) then for i = 0 to modules.Length - 1 do let m = modules.[i] in let types = try m.GetTypes() with _ -> prerr_endline ("Types in module "^m.Name^" failed to load\n"); Arr.zero_create 0 in for j = 0 to types.Size - 1 do let t = types.[j] in let name = t.FullName.ToUpper() in let show = match !kind with | ExactMatch -> name = pat | NamespaceMatch -> nonnull t.Namespace & t.Namespace.ToUpper() = pat in if show then dumpType 0 false t done; done | None -> () let searchDir pat dir = List.iter (searchFile pat) (findDLLs dir) let search pat = let mscorlib = Assembly.Load("mscorlib.dll") in prerr_endline "Searching System Libraries"; let dirFrameworks = Path.GetDirectoryName(mscorlib.Location) in searchDir pat dirFrameworks; if !verbose then prerr_endline "Searching the current directory..."; searchDir pat "."; List.iter (fun dir -> if !verbose then prerr_endline ("Searching directory "^dir); searchDir pat dir) !includes

F# kódrészek használata C#-ból és más .NET nyelvekből

F# kód direktben hozzáférhető C#-ból és más .NET nyelvekből, úgy, hogy egyszerűen a felhasználható értékeket statikus metódusokként illetve statikus mezőkként használjuk. F# környezetben definiált típusok is hasonló egyszerűséggel érhetők el.
A következő példa az mutatja, hogy mikét tehetőek meg a fent leírt dolgok.

let rec loop n = if n <= 0 then () else begin print_endline (string_of_int n); loop (n-1) end type mydata = A | B of int * mydata let rec mydataPrint d = match d with A -> print_endline "the end!" | B (n,d) -> print_endline (string_of_int n); mydataPrint d let rec mydataMap f d = match d with A -> A | B (n,d) -> B (f n,mydataMap f d) class Tester { static void Main() { Mydll.loop(10); Mydll.mydata x1 = Mydll.mydata.A(); Mydll.mydata x2 = Mydll.mydata.B(3,x1); Mydll.mydata x3 = Mydll.mydata.B(2,x2); Mydll.mydata x4 = Mydll.mydata.B(1,x3); Mydll.mydataPrint(x4); Mydll.mydata x5 = Mydll.mydataMap(new System.Func(square), x4); Mydll.mydataPrint(x5); if (Mydll.mydata.IsB(x5)) System.Console.WriteLine("Correct!"); } // Ha F# assembly-t generikusan fordíthattuk volna, akkor ezt írhatnánk: // static int square(int x) { return x * x; } static object square(object x) { return (int) x * (int) x; } }

Hivatkozás az F# típusokra más .NET nyelvbõl

Konkrét típusok (rekord, absztrakt és disztkriminánsos unio típusok): Elnevezett F# típusokra könnyen hivatkozhatunk más .NET nyelvbõl egyszerûen idézve a helyes típus nevet. A beépített típusokra, mint a list és az option a Microsoft.FSharp.List és Microsoft.FSharp.Option nevek használhatók.

Generic típusok: Konkrét paraméteres F# típusoknak meg kell adni az alkalmazott generic paramétereket abban az esetben, ha az F# kód le lett fordítva ahhoz, hogy használhassuk.

Típus rövidítések. F# típus rövidítések kerülendõk, mert például nem láthatók más .NET nyelvbõl.

F# diszkriminánsos unió létrehozása és felhasználása

F# adat típusok .NET osztélyokhoz történõ fordítása támogatással megy végbe azért, hogy elérjék az adat típus által hordozott informácóz. Néha kiegészítõ alosztályokat is generálnak diszkriminánsnak, bár ezek nem használhatók közvetlenül.

F# diszkriminánsos uniót felépítõ értékek.

Egy statikus metódus minden egyes diszkrimináns számára hozzáférhetõ, például:

using Microsoft.FSharp; List x = List.Cons(3,null); Option y = Option.Some(3);

F# diszkriminánsos unió megkülönböztetése C#-ban: A következõ rész arra az esetre vonatkozik, ha több mint egy diszkriminánsunk van. A támogatottsága nagyon kis mértékkben függ attól, hogy a "null"-t használja-e az F# compiler az adattípus egy lehetséges értékének reprezentációjaként. Ez azért van, mert a típusok, melyek "null"-t használnak reprezentációban, azok nehány elõforduló tagját nem képesek támogatni (ezek NullPointerException-t fognak eredménezni, amikor C#-ból használnánk õket).

A "null"-t az F# fordító csak pontosan a következõ szituációkban fogja használni:

Ha a "null"-t nem használjuk a típus reprezentálásakor, akkor a típus rendelkezni fog

Viszont abban az esetben, ha a "null"-t használjuk a típus reprezentációjában, a típus rendelelkezni fog egy egy GetTag() statikus metódussal, és különbözõ tag_... fordítási idejû konstansokkal. A megkülönböztetések ezek segítségével oldhatók meg. Ebben az esetben a a predikátumos megkülönböztetés végrehajtható az értékek "null"-lal történõ összehasonlításával, mivel a "null" érték akkor garantáltan csak az üres konstruktorban használt. Ezen oknál fogva, ha egy C# érték x MyType típusú és a MyType F#-beli definíciója: type MyType = A of ... | B of ... | C of ... akkor a következõ C# kód helyes:

Console.WriteLine("{0}", x.IsA()); Console.WriteLine("{0}", x.IsB()); switch (x.Tag) { case MyType.tag_A: Console.WriteLine("A"); break; case MyType.tag_B: Console.WriteLine("B"); break; default: Console.WriteLine("Must be a C"); break; }

F# rendezett n-esek (tuple) létrehozása és felhasználása

Egy tuple típus neveinek és tagfüggvényeinek használata a következő alakban lehetséges:

Tuple<_,_>.Item1
Tuple<_,_>.Item2

Tuple<_,_,_>.Item1
Tuple<_,_,_>.Item2
Tuple<_,_,_>.Item3

Tuple<_,_,_,_>.Item1
Tuple<_,_,_,_>.Item2
Tuple<_,_,_,_>.Item3
Tuple<_,_,_,_>.Item4

...

Tuple<_,_,_,_,_,_,_>.Item1
...
Tuple<_,_,_,_,_,_,_>.Item7

A 7-nél nagyobb elemszámú rendezett n-eseknek iylen típusú elérése még specifikálás alatt van. A fenti nevek némiképpen megváltoznak, amikor .NET 1.0 vagy 1.1 keretrendszer alá programozunk, mivel akkor nem használhatók a generic típus aritásai, hogy egyértelmûvé tegyék a "Tuple" név aktuális variációját. Így a nevek Tuple2, Tuple3, stb-k lesznek egészen Tuple7-ig.

F# rekordok létrehozása és felhasználása

Az F#-beli rekordok adatmezõit property-ként kell használni (nem mezõként), például:

type recd = { Name: string; Size: int } //támogatni fogja recd.Name //Ez itt egy property recd.Size //Ez is egy property. NEM mező.

Természetesen azok a mezők, melyek nem csak olvashatóak, rendelkeznek set propertyvel is.

F# függvény típusok és értékek létrehozása és felhasználása

Az F# függvény típusának lefordított alakja Microsoft.FSharp.FastFunc<A,B>. Ezen már nem fognak változtatni, bár a Microsoft.FSharp.FastFunc osztály implementációjának részleteit lehet, hogy átdolgozzák. A FastFunc<A,B> nem egy delegált típus, ugyanis ezt a lehetőséget végül elvetették a rossz hatékponyság és a nehézkes interoperabilitás miatt.

Függvényhívások:
A függvény értékek az f.Invoke metódus használatával hívhatók meg. Több paramétert is elfogadó függvény típusok az iterált függvény típusokon keresztül érhetõk el mint például: int -> int -> int. Létezik ennek néhány optimalizált megoldása, melyek statikus metódusai használhatók e célra: Microsoft.FSharp.FastFunc.InvokeFast2, Microsoft.FSharp.FastFunc.InvokeFast3 stb.

Pontosan egy paramétert váró függvények létrehozása
Fontos, hogy képesek legyünk létrehozni és használni a FastFunc típusú értékeket C#-ból olyan egyszerû és természetes módon amennyire csak lehetséges. Egy lehetõség függvény érték létrehozására, hogy a FastFunc<A,B> alosztályait használjuk az "Invoke" metódus túlterhelésével. Azonban nem ez az ajánlott módja ilyen értékek létrehozására, mivel ekkor nem lehetséges a C# névnélküli delegált tulajdonságának kihasználása, és így mikor létrejönne a delegált, minden változóját, amelyre a függvényben hivatkozunk, kézzel kellene "elkapni".

A FastFunc létrehozásának ajánlott módja egy névtelen delegált használata. Egyszerûen létrehozható egy megfelelõ .NET függvény szerû delegált (pl.: System.Converter) és azután meghívható a Microsoft.FSharp.FuncConvert.ToFastFunc egy túlterhelt példánya. Jellemzõen FuncConvert.ToFastFunc(...) a következõ túlterheléseket támogatja:

FuncConvert.ToFastFunc(System.Converter f) //producing type FastFunc FuncConvert.ToFastFunc(System.Action f) //producing type FastFunc FuncConvert.ToFastFunc(System.Predicate f) //producing type FastFunc

Ráadásul van egy implicit konverzió a System.Converter<T,U>-ról FastFunc<T,U>-ra, és ezen oknál fogva kihagyható a FuncConvert.ToFastFunc() hívás, de CSAK akkor, amikor a delegált típusa System.Converter<A,B> (valamilyen A-ra, B-re). Pédául a következõk ekvivalensek:

List.map (FuncConvert.ToFastFunc ((Converter) delegate(int x) { return x.ToString() + x.ToString(); }), myList); //és List.map((Converter) delegate(int x) { return x.ToString() + x.ToString(); }, myList);

Több paramétert elfogadó függvények létrehozása:
A fenti technikák remekül müködnek, amikor olyan F# függvény értéket hozunk létre, amelyek egy paramétert várnak. Azonban ezek alkalmatlanok lehetnek, amikor olyan függvényekre próbáljuk használni, melyek több paramétert is elfogadnak. Ezen oknál fogva az F# könyvtár definiál kiegészítésként hasonló típusokat azért, hogy támogassa a konverziókat. Jellemzõen a következõk definiáltak:

delegate void System.Action(A1 x, A2 y); delegate void System.Action(A1 x, A2 y,A3 z); delegate B System.Converter(A1 x, A2 y,A3 z);

Továbbá a Microsoft.FSharp.FuncConvert.ToFastFunc(...) metódus a következő túlterhelésekkel rendelkezik, amelyekkel a "curried" függvényeket lehet készíteni:

ToFastFunc(System.Converter f) producing type 'A1 -> B', //i.e. FastFunc ToFastFunc(System.Converter f) producing 'A1 -> A2 -> B', //i.e. FastFunc> ToFastFunc(System.Converter f) producing 'A1 -> A2 -> A3 -> B', //i.e. FastFunc> > ToFastFunc(System.Action f) producing 'A1 -> unit', //i.e. FastFunc ToFastFunc(System.Action f) producing 'A1 -> A2 -> unit', //i.e. FastFunc> ToFastFunc(System.Action f) producing 'A1 -> A2 -> A3 -> unit', //i.e. FastFunc>>

Például:

using System; using Microsoft.FSharp; using Microsoft.FSharp.MLLib; List myList = List.of_array(new int[] { 4, 5, 6 }); string joined = List.fold_right (FuncConvert.ToFastFunc((Converter) delegate(int i,string acc) { return i.ToString() + "-" + acc; }), myList, "");

Olyan F# függvény értékek létrehozására, melyek rendezett n-est kapnak paraméterként, a Microsoft.FSharp.FuncConvert.ToTupledFunc használható.

ToTupledFastFunc(System.Converter f) producing 'A1 -> B', //i.e. FastFunc ToTupledFastFunc(System.Converter f) producing 'A1 * A2 -> B', //i.e. FastFunc> ToTupledFastFunc(System.Converter f) producing 'A1 * A2 * A3 -> B', //i.e. FastFunc> > ToTupledFastFunc(System.Action f) producing 'A1 -> unit', //i.e. FastFunc ToTupledFastFunc(System.Action f) producing 'A1 * A2 -> unit', //i.e. FastFunc> ToTupledFastFunc(System.Action f) producing 'A1 * A2 * A3 -> unit', //i.e. FastFunc>>