Az F# programozási nyelv

Funkcionális programozás

Azonosítók

Az F#-ban azonosítók segítségével adunk nevet az értékeknek, így tudunk majd rájuk hivatkozni a program későbbi részeiben. Egy azonosítót a let kulcsszó segítségével definiálhatunk, a let-et követi az azonosító, egy egyenlőségjel, majd egy kifejezés, amely meghatározza az azonosítóhoz rendelt értéket. Kifejezés lehet mindenféle kódrészlet, amely kiszámítható, és egy értékkel tér vissza. A következő kifejezés mutatja, hogy hogyan rendelhetünk egy azonosítóhoz egy értéket:

let x = 42

A legtöbb embernek, aki eddig imperatív nyelvekben programozott, ez úgy tűnhet, mint egy értékadás. Valóban van sok hasonlóság, de ugyanakkor nagyon fontos eltérések is vannak. A tiszta funkcionális nyelvekben, ha egy változó egyszer értéket kap, akkor az többé már nem változtatható meg. Ez az, amiért azonosítóként, és nem változóként hívjuk az x-et a fenti példában. Később, a Láthatóság című részben olvashatjuk, hogy bizonyos körülmények között mégiscsak lehetséges az azonosítókat felüldefiniálni, illetve az F# támogat imperatív programozást is, ott pedig majd azt fogjuk látni, hogy az azonosítókhoz rendelt értékek megváltozhatnak.

Egy azonosító hivatkozhat egy értékre vagy egy függvényre is, ez aligha meglepő, mivel az F# függvényei egyben értékek is. (Erről a későbbiekben még lesz szó, az Értékek és függvények részben). Ez azt jelenti, hogy az F#-nak nincs igazi koncepciója a függvények nevére, paraméterek nevére; mindegyikük azonosító. Ugyanúgy írjuk a függvényazonosítót, mint az érték azonosítót, azzal a különbséggel, hogy a függvényazonosító neve és az egyenlőségjel között ott vannak a paraméterek nevei is.

let raisePowerTwo x = x ** 2.0

Értékek és függvények

Az értékek és a függvények megkülönböztethetetlenek F#-ban, mivel a függvények egyben értékek is, és az F# szintaxisa is ugyanúgy kezeli őket. Például nézzük a következő kódot:

#light
let n = 10
let add a b = a + b
let addFour = add 4
let result = addFour n
printfn "result = %i" result
result = 14

Az első sorban a 10 érték rendelődik az n azonosítóhoz, a második sorban definiáljuk az add függvényt, ami két számot ad össze. Észrevehetjük, hogy a szintaxis a két paramétertől eltekintve teljesen megegyezik. Az a+b kifejezés eredménye is egy érték, ami automatikusan az add függvény eredménye lesz. Az is feltűnő, hogy az imperatív nyelvektől eltérően, nem kell megadnunk visszatérési értéket a függvénynek.

Általában jobban használhatóak azok a függvények, amelyeknél van lehetőség a részleges alkalmazásra, mint azok, amelyek tuple-öket használnak. Ez azért van, mert a részlegesen alkalmazott függvények sokkal flexibilisebbek. Ez különösen igaz egy programkönyvtár fejlesztésénél, ahol nem tudunk felkészülni minden felhasználói igényre, ezért meglehetősen fontos a rugalmasság.

Soha nem térünk vissza explicite értékkel, de akkor mégis hogyan van lehetőség köztes számítások elvégzésére a függvényen belül? Az F#-ban ezt a whitespace-ek kontrollálják, ez az úgynevezett margó szabály. A behúzás azt jelenti, hogy az a sor egy köztes értéket számol ki. Gyakorlatilag a blokkszerkezetet a behúzás mértéke alakítja ki. A következő példa szemlélteti a margószabályt:

#light
let halfWay a b =
    let dif = b - a
    let mid = dif / 2
    mid + a
printfn "(halfWay 5 11) = %i" (halfWay 5 11)
printfn "(halfWay 11 5) = %i" (halfWay 11 5)
(halfWay 5 11) = 8
(halfWay 11 5) = 8

A program két szám átlagát számolja ki, a 3. és a 4. sor köztes értéket számol ki. Ennek jelzésére szolgál a 4 szóköz behúzás, aminek nem feltétlenül kell 4-nek lenni (miniálisan 1 lehet), bár ez a konvenció. Fontos, hogy TAB-okat nem használhatunk, bár a fordító jelenlegi verziója warningot ad minden egyes sorra, amiben TAB van. TAB-okat egyrészt azért nem használhatunk, mert ezeket a különböző editorok különbözőképpen jelenítik meg, ami problémákhoz vezethez. Visual Studioban viszont alkalmazhatjuk, mert beállítható, hogy a TAB-ot meghatározott számú szóköz karakterre cserélje automatikusan.

Láthatóság

Egy azonosító láthatósága azt határozza meg, hogy a program mely részeiben tudjuk felhasználni. Ha olyankor próbálunk meg használni egy azonosítót, amikor az nem látható, akkor fordításai hibát fogunk kapni.

Minden azonosító, attól függetlenül, hogy érték, vagy függvény van hozzárendelve, a definíció végétől a szekciója végéig látható, azaz addig, amíg vele azonos, vagy tőle nagyobb a behúzás a sor elején. Tehát a legfelső szintű azonosítók (amelyek nem lokális azonosítói egyetlen más azonosítónak sem) a definiálásuktól a forrásfájl végéig látszanak. Ha egy legfelső szintű azonosító egyszer értéket kapott, akkor az a program futása során nem változtatható meg. A függvényeken belüli azonosítók annak a kifejezésnek a végéig láthatóak, amelyben szerepelnek. Ez azt jelenti, hogy csak abban a függvénydefinícióban látszanak, amiben megjelennek. Erre egy példa:

#light
    let defineMessage() =
    let message = "Help me"
print_endline message
print_endline message
Prog.fs(34,17): error: FS0039: The value or constructor 'message' is not defined.

A függvényeken belül definiált azonosítók kicsit másképpen viselkednek, mint a legfelső szintűek, mert a lokális azonosítókat felül lehet definiálni a let kulcsszó segítségével. Ez hasznos, mivel azt jelenti, hogy az F# programozónak nem kell mindig új nevet kitalálnia, hogy a köztes állapotok értékeit megtartsa. Erre is nézünk egy példát. A következő program egy matematikai feladványt számol ki, ahol sok köztes számításra van szükség, amelyeknek csak egyszer van szükség az értékére, utána nem érdekes, hogy mi történik velük.

#light
let mathsPuzzle() =
    print_string "Enter day of the month on which you were born: "
    let input = read_int ()
    let x = input * 4 // Multiply it by 4
    let x = x + 13 // Add 13
    let x = x * 25 // Multiply the result by 25
    let x = x - 200 // Subtract 200
    print_string "Enter number of the month you were born: "
    let input = read_int ()
    let x = x + input
    let x = x * 2 // Multiply by 2
    let x = x - 40 // Subtract 40
    let x = x * 50 // Multiply the result by 50
    print_string "Enter last two digits of the year of your birth: "
    let input = read_int ()
    let x = x + input
    let x = x - 10500 // Finally, subtract 10,500
    printf "Date of birth (ddmmyy): %i" x
mathsPuzzle()
Enter day of the month on which you were born: 23
Enter number of the month you were born: 5
Enter last two digits of the year of your birth: 78
Date of birth (ddmmyy): 230578

Azt azért meg kell jegyeznünk, hogy ez nem értékadás, mert itt felüldefiniáljuk az azonosítót, akár a típusát is megváltoztathatnánk, de itt mindig megőrizzük a típusbiztonságot.
Típusbiztonság, vagy más néven erősen típusosság, valójában ez azt jelenti, hogy az F# nem engedi, hogy helytelen műveletet hajtsunk végre az értékeken. Erre is legyen itt egy példa:

#light
let changeType () =
    let x = 1 // x integer típusú
    let x = "change me" // x legyen string típusú
    let x = x + 1 // stringhez próbálunk meg hozzáadni!!
    print_string x
prog.fs(55,13): error: FS0001: This expression has type
int
but is here used with type
string
stopped due to error

Lehetőség van arra is, hogy egy külsőbb szinten definiált, nagyobb láthatósági tartománnyal rendelkező azonosítót, egy lokális függvényben újra definiáljunk, ekkor a lokális függvényben az új érték rendelődik hozzá, a lokális függvény befejeződése után pedig az eredeti értéke lesz elérhető a továbbiakban is. Ezt a következő példa mutatja:

#light
let printMessages() =
    // definiálja a message-et és ki is írja
    let message = "Important"
    printfn "%s" message;
    // definiál egy lokális függvényt, és felüldefiniálja a message-et
    let innerFun () =
        let message = "Very Important"
        printfn "%s" message
    // definiálja a message-et és ki is írja
    innerFun ()
    // végül kiírja a message-et még egyszer
    printfn "%s" message
printMessages()
Important
Very Important
Important

Rekurzió

A rekurzív függvény alatt olyan függvényt értünk, aminek a definiálására saját magát is felhasználjuk. A rekurziót gyakran alkalmazzuk a funkcionális programozásban, különösen az olyan helyzetekben, amikor imperatívan egy ciklusra gondolunk. Sokan úgy tartják, hogy egy algoritmus könnyebben megérthető, ha rekurzióval van leírva, mint ha ciklusokat használna.
Ahhoz, hogy az F#-ban rekurzív programot írhassunk, jeleznünk kell a fordítónak, hogy a függvény rekurzív lesz. Ezt a let és az azonosító közé tett rec kulcsszóval tehetjük meg. A következő példa bemutatja a rekurzió egyszerű alkalmazását. Figyeljük meg, hogy az 5. sorban magát a függvényt is felhasználjuk a definiáláshoz.

#light
let rec fib x =
    match x with
    | 1 -> 1
    | 2 -> 1
    | x -> fib (x - 1) + fib (x - 2)
printfn "(fib 2) = %i" (fib 2)
printfn "(fib 6) = %i" (fib 6)
printfn "(fib 11) = %i" (fib 11)
(fib 2) = 1
(fib 6) = 8
(fib 11) = 89

A függvény a Fibonacci sorozat n-edik elemét számolja ki, amire a legmegfelelőbb módszer a rekurzió, mivel a sorozat matematikai definíciója is rekurzív.
Habár a rekurzió egy nagyon erős eszköz, mindig oda kell figyelnünk, hogy mikor használjuk, ugyanis puszta figyelmetlenségből is nagyon könnyű olyan rekurziót írni, ami sohasem fog megállni. Bár vannak esetek, mikor határozottan az a szándékunk, hogy végtelen ciklust írjunk, mert így lesz hatékony a kód. A rekurzió felfogható egy egyszerű ág, és egy rekurzív ág kombinációjaként, ez a modell segíthet belátni, hogy a programunk terminál-e vagy sem. A rekurzív ág az, amit majd később, egy újabb függvényhívás fog kiszámolni, a fib esetében ez minden érték az 1-et és 2-őt kivéve. Az alap ág egy nem rekurzív ág, ahol konstansok, kifejezések(nem rekurzívak) értékei szerepelnek. A példában ez az 1, és a 2. Azonban csupán az alapeset megléte nem ad garanciát a terminálásra, figyelnünk kell arra, hogy a rekurzív ág az alap ág felé tartson. A példában, ha x nagyobb egyenlő mint három, akkor a rekurzió az alapág felé halad, mivel az x értéke mindig csökkeni fog, és így előbb-utóbb eléri az alapágat. Azonban ha az x kisebb mint 1, akkor egy végtelen rekurzióba kerülünk, ahol a x mindig csökkenni fog. Ez veremtúlcsordulást fog eredményezni előbb-utóbb. A fenti fib függvény használ mintaillesztést is, amiről a Mintaillesztés részben lesz szó.

Névtelen (lambda) függvények

Az F# biztosít egy alternatív lehetőséget is függvények definiálására a fun kulcsszó segítségével. Általában akkor használjuk ezt a lehetőséget, amikor nem szükséges, hogy nevet adjunk a függvénynek. Ezért hívják ezeket névtelen függvényeknek, lambda függvényeknek, vagy csak simán lambdáknak. Az ötlet -miszerint nem szükséges, hogy egy függvénynek neve legyen- elsőre kicsit furcsának tűnhet, de amikor egy függvényt, mint egy másik függvény paraméterét használjuk, akkor nincsen feltétlenül szükségünk a nevére. Most nézzünk erre egy példát, illetve a későbbiekben a Listák részben is fel fogjuk használni a lambda függvényeket.

#light
let x =(fun x y -> x + y) 1 2

Névtelen függvényeket képezhetünk a function kulcsszóval is. Az ilyen módon képzett függvények argumentumain végezhetünk mintaillesztést (amit a fun kulcsszóval történő deklarálás nem enged), ugyanakkor csak egyetlen argumentuma lehet az ilyen módon képzett névtelen függvénynek. Ennek az áthidalására vannak különböző technikák, erre nézzünk most példát.

let x1 = (function x -> function y -> x + y) 1 2
let x2 = (function (x, y) -> x + y) (1, 2)

Az első sorban arra látunk példát, hogy hogyan lehet névtelen függvényeket parciálisan használni, egymásba ágyazni, és ezáltal több argumentumot használni. A második sorban pedig egy tuple az egyetlen bemenő adat. Általában azonban (magában az F# forráskódjában is) a fun kulcsszót használják.

Operátorok

Az F#-ban úgy gondolhatunk az operátorokra, mint kifinomult függvényhívásokra. A nyelv támogatja az infix és a prefix operátorok használatát is. Az F#-nak elég gazdag és változatos operátorkészlete van, sokféle típusra (pl.: számok, boolean, string és különböző kollekciók). Ennek a fejezetnek nem célja felsorolni az összes elérhető operátort, inkább nézzük meg, hogyan tudunk újakat definiálni és azokat használni.
Ugyanúgy, mint a C#-ban, az F#-ban is túlterheltek az operátorok. Azonban a C#-al ellentétben itt az operátor mindkét operandusának azonos típusúnak kell lennie, vagy különben fordítási hibát kapunk. Az F# lehetőséget ad operátorok definiálására és felüldefiniálására is, nemsokára következik, hogy hogyan.
A + operátor segítségével összeadhatunk stringeket, vagy egy System.TimeSpan-hez egy System.DateTime-ot, mert rájuk is van definiált + operátor.

#light
let ryhm = "Jack " + "and " + "Jill"
open System
let oneYearLater =
    DateTime.Now + new TimeSpan(365, 0, 0, 0, 0)

A felhasználók definiálhatják a saját operátoraikat, vagy felüldefiniálhatnak már meglévőket, habár ez nem igazán tanácsos, mert onnantól kezdve az operátor már nem támogatja a túlterhelést. Nézzünk meg egy nem túl szerencsés felüldefiniálást.

#light
let (+) a b = a - b
print_int (1 + 1)

A saját operátorunk a következő karakterekből állhat (lehet egy vagy több karakteres is):
!$%&*+-./<=>?@^|~
:
Az első karakternek az első sorból kell kikerülnie, a másodiktól kezdve pedig már : is használható pluszban. A szintaxis nagyon hasonló az azonosítók definiáláshoz, azzal az eltéréssel, hogy az operátorokat zárójelbe kell tennünk, hogy a fordító felismerje őket. A példában definiáljuk a +:* operátort, ami először összeadja, majd összeszorozza az operandusokat.

#light
let ( +:* ) a b = (a + b) * a * b
printfn "(1 +:* 2) = %i" (1 +:* 2)
(1 +:* 2) = 6

Listák

A listák egyszerű kollekció típusok, amik be vannak építve a nyelvbe. Egy F# lista lehet üres lista: [], vagy lehet egy elem és egy másik lista konkatenációja. Az F# lista elejére a :: operátorral tudunk egy elemet konkatenálni. Ezt a folyamatot jól tükrözi a következő példa:

#light
let emptyList = []
let oneItem = "one " :: []
let twoItem = "one " :: "two " :: []
let shortHand = ["apples "; "pairs "]
let twoLists = ["one, "; "two, "] @ ["buckle "; "my "; "shoe "]
let objList = [box 1; box 2.0; box "three"]
let printList l =
    List.iter print_string l
    print_newline()
let main() =
    printList emptyList
    printList oneItem
    printList twoItem
    printList shortHand
    printList twoLists
    for x in objList do
    print_any x
    print_char ' '
    print_newline()
main()
one
one two
apples pairs
one, two, buckle my shoe
1 2.000000 "three"

Kicsit problémás lenne úgy listát definiálni, hogy minden elemét külön fűzzük hozzá, ennek sokkal kényelmesebb módját mutatja a shortHand függvény. Ekkor szögletes zárójelben kell felsorolni az elemeket egymás után, a szögletes zárójel választja el őket egymástól. A twoList függvényben ismét új operátorral találkozunk, a @ operátor két listát fűz össze. Az F# listákban minden elemnek ugyanolyan típusúnak kell lennie, ezért ha egy int listát próbálunk összekonkatenálni egy string listával, akkor hibaüzenetet fogunk kapni. Ha mégis különböző típusú elemek listájára van szükségünk, akkor létre tudunk hozni obj típusú listát, mint ahogyan az objList függvény mutatja is. Az F# típusokról részletesebben a Típus, és típuskikövetkeztetés illetve a Típusok definiálása részben lesz szó.
Az F# listák immutable tulajdonságúak, tehát nem változtatható meg a lista egyetlen eleme sem. A listákon működő függvények sem teszik ezt, mindig építenek egy új listát és azt adják vissza. A következő példa ezt mutatja be.

#light
let one = ["one "]
let two = "two " :: one
let three = "three " :: two
let rightWayRound = List.rev three
let printList l =
    List.iter print_string l
    print_newline()
let main() =
    printList one
    printList two
    printList three
    printList rightWayRound
main()
one
two one
three two one
one two three

Az általános módja a listák feldolgozásának a rekurzió. Az üres lista az alap ág. Amikor a függvény üres listát kap, akkor terminál, különben feldolgozza a lista első elemét (head), azután a lista maradék részét (tail) rekurzívan feldolgozza. Erre is egy példa:

#light
let listOfList = [[2; 3; 5]; [7; 11; 13]; [17; 19; 23; 29]]
let rec concatList l =
    if List.nonempty l then
        let head = List.hd l in
        let tail = List.tl l in
        head @ (concatList tail)
    else
        []
let primes = concatList listOfList
print_any primes
[2; 3; 5; 7; 11; 13; 17; 19; 23; 29]

Először definiálunk egy listát, ami 3 listát tartalmaz, aztán definiálunk egy rekurzív függvényt (concatList), ez fogja a listákat összefűzni. A függvény veszi a lista fejelemét és a maradékot, egészen addig, amíg nem üres (List.nonempty) a lista, és a fejelemet mindig hozzákonkatenálja a rekurzív függvényhívás eredményéhez. Egy másik beépített függvény, amit már az előző példában is használtunk, a List.iter, ami 2 argumentumot vár. Az elsőt végrehajtja a lista minden elemére, a második argumentum pedig a lista amire végrehajtja a függvényt. Ez csupán egy gyorsítás a nyelvben, megspórol nekünk pár sornyi kódot. Emellett még rengeteg ilyen kis függvény van, például van List.concat is, ami nagyon hasonló az előbbi példában szereplő concatList függvényhez.

Listák generálása

Az F#-ban elég könnyen tudunk létrehozni listákat, szekvenciákat (az F# ilyen néven hívja a .NET BCL's IEnumerable típust), és tömböket (a Lusta kiértékelésről szóló részben lesz róluk szó részletesebben) a következő nyelvi elemek használatával.

#light
let numericList = [ 0 .. 9 ]
let alpherSeq = { 'A' .. 'Z' }
printfn "%A" numericList
printfn "%A" alpherSeq
[0; 1; 2; 3; 4; 5; 6; 7; 8; 9]
seq ['A'; 'B'; 'C'; 'D'; ...]

A soron következő példa kicsit bonyolultabb:

#light
let multiplesOfThree = [ 0 .. 3 .. 30 ]
let revNumericSeq = [ 9 .. -1 .. 0 ]
printfn "%A" multiplesOfThree
printfn "%A" revNumericSeq
[0; 3; 6; 9; 12; 15; 18; 21; 24; 27; 30]
[9; 8; 7; 6; 5; 4; 3; 2; 1; 0]

Itt már 3 értéket adtunk meg a szögletes zárójelek között. A középső érték jelenti a lépésközt, eltérően más funkcionális nyelvektől, mivel ott a lista következő eleme szokott lenni a második helyen.
Lehetőség van ciklus definiálására, ami egy lista elemeiből állítja elő az eredménylista elemeit. A következő példában végigiterálunk 1-től 10-ig a lista elemein, és mindegyiknek vesszük a négyzetét, így képezzük az új listát.

#light
let squares =
    { for x in 1 .. 10 -> x * x }
print_any squares
seq [1; 4; 9; 16; ...]

Tudunk őrfeltételt is hozzáadni a when kulcsszóval, így megszűrhetjük, hogy mely elemek kerüljenek a listába és melyek ne. A when kulcsszót egy Boolean típusú kifejezésnek kell követnie. Az aktuális elem csak akkor fog bekerülni az új listába, ha a feltétel true-ra értékelődik ki. A következő példában a 2-vel vett maradék vizsgálatával döntjük el, hogy a szám páros-e, azaz bekerül-e az új listába, vagy sem.

#light
let evens n =
    { for x in 1 .. n when x % 2 = 0 -> x }
print_any (evens 10)
seq [2; 4; 6; 8; ...]

Természetesen lehetőség van az iterálások egymásba ágyazására, azaz tudunk többdimenziós indexeket létrehozni. Nézzünk erre is egy példát, amiben egy 2 dimenziós mátrix indexeit kapjuk vissza tuple-ök formájában.

#light
let squarePoints n =
    { for x in 1 .. n
       for y in 1 .. n -> x,y }
print_any (squarePoints 3)
[(1, 1); (1, 2); (1, 3); (2, 1); ...]

Kiértékelési sorrend

Az F#-nak szigorú kiértékelési sorrendje van, ilyen téren eltér a tiszta funkcionális nyelvektől, ahol ez nem jellemző, mivel ott a kifejezések bármilyen sorrendben kiértékelődhetnek. A szigorúságra jó példa az if _ then _ else.
Az F#-ban az if _ then _ else egy kifejezés, azaz van visszatérési értéke. A két lehetséges érték közül az egyiket adja majd vissza, a Boolean kifejezés értékétől függően.
Az if _ then _ else kifejezésnek van 1-2 olyan tulajdonsága, ami az imperatív környezethez szokott programozók számára nagyon furcsa lehet. Az F# típusrendszere megköveteli, hogy a két lehetséges visszatérési érték típusa megegyezzen. Persze ezt ki lehet játszani, ha a visszatérési érték obj típusú. Ezt mutatja a következő példa.

#light
let result =
    if System.DateTime.Now.Second % 2 = 0 then
        box "heads"
    else
        box false
print_any result

A másik furcsaság pedig, hogy mindenképpen kell else ágat is írnunk. Ez nem meglepő, ha belegondolunk a kifejezés függvény voltába, azaz, hogy mindig vissza kell adnia valamilyen értéket. Persze ha trükközünk, akkor itt is van lehetőség else nélküli if írására, de az már eléggé imperatív megközelítés, szóval arról majd az Imperatív programozás részben lesz szó.

Típus és típuskikövetkeztetés

Az F# erősen típusos nyelv, ami azt jelenti, hogy a függvényeket nem használhatjuk nem oda illő argumentumokkal. Például nem hívhatunk meg string típusú argumentummal egy függvényt, ami int-et vár, csak akkor, ha előtte explicit átkonvertáljuk int-re. Az F#-ban minden értéknek van típusa, és ebbe beletartoznak azok az értékek is, amik valójában függvények.
A legtöbbször nincsen szükség arra, hogy explicit meghatározzuk a típusokat, ezt majd a fordító kikövetkezteti. Ha mindent rendben talál, akkor a programozó nem is kap visszajelzést a típusokról, azonban ha valamilyen típuseltérést észlel, akkor hibával áll le a fordítás az adott helyen, a fordító pedig közli velünk, hogy milyen típust talált, és mit várt volna. Ha többet akarunk megtudni a függvényeik típusáról, akkor konzolos fordítás esetén a -i kapcsolót használjuk, míg a Visual Studio tooltip-ek segítségével tájékoztat minket. Most nézzünk egy példát a -i kapcsoló használatára, és a fordító válaszára.

#light
let aString = "Spring time in Paris"
let anInt = 42
val aString : string
val anInt : int

A következő példa során egy függvény típusát nézzük meg, ami szintén a val kulcsszóval kezdődik, mivel a függvények is értékek. A típusokat egy sorban -> -ak választják egymástól, először a függvény argumentumainak típusát látjuk, majd a sor végén a visszaadott érték típusát.

#light
let makeMessage x = (string_of_int x) + " days to spring time"
let half x = x / 2
val makeMessage : int -> string
val half : int -> int

Nem lehet azonos a típusa egy részlegesen alkalmazható függvénynek, és egy olyan függvénynek, ami egy tuple-t vár paraméterként. Erről már volt szó az Értékek és függvények című részben. A példa ezt bizonyítja:

let div1 x y = x / y
let div2 (x, y) = x / y
let divRemainder x y = x / y, x % y
val div1 : int -> int -> int
val div2 : int * int -> int
val divRemainder : int -> int -> int * int

A div1 függvényt tudjuk részben alkalmazni, mert a típusa int -> int -> int. Ez azt jelenti, hogy a bemenő paramétereket külön-külön is megkaphatja. Ezzel ellentétben a div2 típusa int * int -> int, ami azt jelenti, hogy egy párt vár bemenő paraméterként, és ebből visszaad egy int-et.
A következő függvény a doNothing, amely igazából elég érdektelennek látszik, de a típusa nem szokványos. Az 'a -> 'a jelentése, hogy kap egy bármilyen típusú paramétert, majd egy ugyan olyan típusút ad is vissza. Minden típus ami ' jellel kezdődik, változó típust jelent.

let doNothing x = x
val doNothing : 'a -> 'a

Habár a típuskikövetkeztetés teljesen automatizált, és mindig a lehető legáltalánosabb típust találja meg, amit az adott környezetben lehet használni, van arra lehetőségünk, hogy mi határozzuk meg a paraméterek, azonosítók típusát, vagy adott esetben megszorítsuk azt, ha úgy van rá szükség. Erre a .NET könytárak esetében lehet gondolni, ahol a fordítónak kevesebb információja van a típusokról, ezért néha segítenünk kell neki, hogy mindent rendben találjon. Nézzünk egy példát a típusmegszorításra.

let doNothingToAnInt (x : int) = x
let intList = [1; 2; 3]
let (stringList : list<string>) = ["one"; "two"; "three"]
val doNothingToAnInt _int : int -> int
val intList : int list
val stringList : string list

Mintaillesztés

A mintaillesztés lehetőséget ad arra, hogy bizonyos tulajdonságú elemeket -amiket a különböző mintákkal választunk ki- különbözőféleképpen dolgozzuk fel. Ez kicsit hasonlít az imperatív nyelvek if és switch szerkezeteire, de annál sokkal hatékonyabb és erősebb eszköz.
A mintaillesztés eszköze használható sokféle típuson, sokféle formában. Pontosan emiatt nagyon sok helyen előfordul a nyelvben. A következő bevezető példával csupán a szintaxist kívánjuk szemléltetni.

#light
let rec luc x =
    match x with
        | x when x <= 0 -> failwith "value must be greater than 0"
        | 1 -> 1
        | 2 -> 3
        | x -> luc (x - 1) + luc (--x - 2)
printfn "(luc 2) = %i" (luc 2)
printfn "(luc 6) = %i" (luc 6)
printfn "(luc 11) = %i" (luc 11)
printfn "(luc 12) = %i" (luc 12)
(luc 2) = 3
(luc 6) = 18
(luc 11) = 199
(luc 12) = 322

A mintaillesztés szintaxisában a match kulcsszót követi az azonosító, amelyik illesztve lesz, ezután a with kulcsszó, majd soronként egy minta található, | jellel a sor elején, a mintát a -> követi, majd a végrehajtandó kifejezés. A szabályokra a definiálás sorrendjében történik az illesztés, ezért ha nem diszjunktak az illesztendő halmazok, akkor vigyáznunk kell a sorrendre. Továbbá a fordító warning-ot fog adni, ha nem teljes az illesztés, azaz van olyan értéke az illesztett kifejezésnek, amire egyik minta sem fog illeszkedni. Illetve a fordító akkor is fog egy warning-ot adni, ha azt veszi észre, hogy van olyan mintánk, amire sosem lesz illesztés. Lehetőség van egy őrfeltétel hozzáadására is, a when kulcsszó segítségével. Fontos tisztázni a pontos működési elvet. Ha a minta illeszkedik, és van őrfeltétel, akkor ha az őrfeltétel kifejezése true-ra értékelődik ki utána végrehajtódik a mintához rendelt kifejezés. Amennyiben false az őrfeltétel, akkor további mintaegyezőséget keres a lejjebb definiált mintákban. A fenti példában, és általában is az első minta szokott a hibakezelés lenni. A következő példában láthatjuk, hogy lehetőség van az első | jel elhagyására. Ennek egy minta esetén van értelme, mert így egy sorba tudjuk írni az egészet, bár ez ronthatja a kód olvashatóságát. A _ karakter pedig a joker, azaz mindenre illeszkedik.

#light
let booleanToString x =
    match x with false -> "False" | _ -> "True"

Másik hasznos lehetőség, hogy több mintát együttesen is rendelhetünk egy szabályhoz.

#light
let stringToBoolean x =
    match x with
        | "True" | "true" -> false
        | "False" | "false" -> true
        | _ -> failwith "unexpected input"

Szinte az összes F#-beli típusra lehet mintaillesztést végezni, a következő két példa a tuple-ökre mutat rá. A logikai and és or műveletet valósítjuk meg velük. Vegyük észre, hogy a két példa két különböző megközelítését használja a mintaillesztésnek.

#light
let myOr b1 b2 =
    match b1, b2 with
        | true, _ -> true
        | _, true -> true
        | _ -> false

let myAnd p =
    match p with
        | true, true -> true
        | _ -> false

Ha a tuple-ön belül akarunk mintát illeszteni, akkor az egyes mezőket , -vel kell elválasztanunk egymástól. Ez történik a myOr első két szabályában, illetve a myAnd első szabályában. Ugyanakkor csak azért, mert tuple-ökkel dolgozunk, nem kell mindig rájuk egy érték n-esként tekintenünk, erre példa a myOr, és myAnd utolsó szabálya, ahol a _ karakter az egész tuple-re illeszkedik. De ehelyett akár írhattunk volna egy azonosítót is, ami szintén az egészre vonatkozna.

F#-ban a mintaillesztést legtöbbször listákra használjuk, sőt ez a listák kezelésének legkézenfekvőbb és leghatékonyabb módja. A következő példához hasonló már szerepelt a Listák részben, csak ott if _ then _ else -el volt megoldva. Most megnézzünk egy mintaillesztési megoldást. A mintaillesztéshez a :: operátort használjuk, head::tail formában, ahol a head fog a fejelemre illeszkedni, a tail pedig a lista maradék része. Mindkettő lehet üres lista is. A conactListOrg a régi megoldás, a concatList pedig az új, gyorsabb, hatékonyabb.

#light
let listOfList = [[2; 3; 5]; [7; 11; 13]; [17; 19; 23; 29]]

let rec concatList l =
    match l with
        | head :: tail -> head @ (concatList tail)
        | [] -> []

let rec concatListOrg l =
    if List.nonempty l then
        let head = List.hd l in
        let tail = List.tl l in
        head @ (concatListOrg tail)
    else
        []

let primes = concatList listOfList
print_any primes

A legáltalánosabb módja a listák kezelésének a mintaillesztés segítségével történik: venni a lista fejelemét, feldogozni, majd rekurzívan feldolgozni a maradék listát. A következő példa még mutat 1-2 lehetőséget.

#light
let rec findSequence l =
    match l with
        | [x; y; z] ->
            printfn "Last 3 numbers in the list were %i %i %i" x y z
        | 1 :: 2 :: 3 :: tail ->
            printfn "Found sequence 1, 2, 3 within the list"
            findSequence tail
        | head :: tail -> findSequence tail
        | [] -> ()

let testSequence = [1; 2; 3; 4; 5; 6; 7; 8; 9; 8; 7; 6; 5; 4; 3; 2; 1]
findSequence testSequence
Found sequence 1, 2, 3 within the list
Last 3 numbers in the list were 3 2 1

Az első szabály azt mutatja, hogy hogyan illesszünk fix hosszúságú listát úgy, hogy megkapjuk a lista elemeit. A második szabályban a lista első 3 értékére vagyunk kíváncsiak, és ha a 3 érték az 1,2,3 sorozat, akkor illeszkedik a minta. A 3. szabály a hagyományos head::tail, hogy vagdossuk az elemeket, a 4. szabály pedig a lista elfogyását kezeli.

Aktív minták

Az aktív minták lehetővé teszik a bemenő adatok felosztását (osztályozását) úgy, hogy az egyes partícióknak nevet adhatunk, és ezek a nevek később felhasználhatók mintaillesztésekben. Az aktív mintákat tehát adatok dekomponálására használhatjuk, és az egyes partíciókhoz egyéni viselkedést rendelhetünk. Megkülönböztetünk teljes és részleges aktív mintákat, ezt mutatja az alábbi kódrészlet:

// teljes aktív minták definiálása
let (|identifier1|identifier2|…|) [arguments] = expression
// részleges aktív minták definiálása
let (|identifier1|_|) [arguments] = expression

Használat

Ahogy az előző kódrészlet is mutatja, az azonosítók az input adathoz tartozó partíciók neveinek felelnek meg, magát az input adatot az [arguments] rész reprezentálja. Más szóval, a nevek az argumentumok értékeiből képzett halmaz lehetséges részhalmazait jelölik. Egy aktív mintában legfeljebb hét partíció adható meg. Az expression rész azt írja le, hogy a bemenő adatot hogyan kell részekre osztani. Az aktív mintákkal tulajdonképpen olyan szabályok definiálhatók, amelyek meghatározzák, hogy az egyes (névvel ellátott) partíciók a bemenő adatok mely értékeihez legyenek hozzákötve.

Terminológia:

Példaként tekintsük a következő aktív mintát egy argumentummal:

let (|Even|Odd|) input = if input % 2 = 0 then Even else Odd

Az előbbi aktív minta a következőképpen használható fel egy mintaillesztésben:

let TestNumber input =
    match input with
    | Even -> printfn ”%d is even” input
    | Odd -> printfn ”%d is odd” input

A következő tesztadatokkal végrehajtva a függvényt, az alábbi eredmények adódnak:

TestNumber 7 // 7 is odd
TestNumber 32 // 32 is even

Az aktív minták egy másik lehetséges használata az, amikor a bemenő adatot többféle módon kívánjuk dekomponálni, azaz, amikor a szóban forgó adatnak több különböző reprezentációja is elképzelhető. Erre egy példa a Color objektum, amely felbontható az RGB és a HSB reprezentáció mentén is.

open System.Drawing

let (|RGB|) (col : System.Drawing.Color) =
    (col.R, col.G, col.B)

let (|HSB|) (col : System.Drawing.Color) =
    (col.GetHue(), col.GetSaturation(), col.GetBrightness())

let printRGB (col: System.Drawing.Color) =
    match col with
    | RGB(r, g, b) -> printfn " Red: %d Green: %d Blue: %d" r g b

let printHSB (col: System.Drawing.Color) =
    match col with
    | HSB(h, s, b) -> printfn " Hue: %f Saturation: %f Brightness: %f" h s b

let printAll col colorString =
    printfn "%s" colorString
    printRGB col
    printHSB col

printAll Color.Red "Red"
printAll Color.Black "Black"
printAll Color.White "White"
printAll Color.Gray "Gray"
printAll Color.BlanchedAlmond "BlanchedAlmond"

A fenti program eredménye a következő:

Red
 Red: 255 Green: 0 Blue: 0
 Hue: 0.000000 Saturation: 1.000000 Brightness: 0.500000
Black
 Red: 0 Green: 0 Blue: 0
 Hue: 0.000000 Saturation: 0.000000 Brightness: 0.000000
White
 Red: 255 Green: 255 Blue: 255
 Hue: 0.000000 Saturation: 0.000000 Brightness: 1.000000
Gray
 Red: 128 Green: 128 Blue: 128
 Hue: 0.000000 Saturation: 0.000000 Brightness: 0.501961
BlanchedAlmond
 Red: 255 Green: 235 Blue: 205
 Hue: 36.000000 Saturation: 1.000000 Brightness: 0.901961

Részleges aktív minták

Bizonyos esetekben szükség lehet arra, hogy a bemenő adatoknak csak egy részét particionáljuk. Ekkor részleges aktív minták (partial active patterns) egy halmazát kell elkészítenünk, amelyek közül mindegyik a bemenő adatoknak csak egy részére fog illeszkedni, a maradék részre a mintaillesztés meghiúsul. Tehát azokat az aktív mintákat nevezik részlegesnek, amelyek nem minden esetben szolgáltatnak értéket, valójában azonban van visszatérési értékük, ami Option típusú. A részleges aktív minták definiálásakor használnunk kell a wildcard (_) karaktert, erre mutat egy példát az alábbi kódrészlet:

let (|Integer|_|) (str: string) =
    let mutable intvalue = 0
    if System.Int32.TryParse(str, &intvalue) then Some(intvalue)
    else None

let (|Float|_|) (str: string) =
    let mutable floatvalue = 0.0
    if System.Double.TryParse(str, &floatvalue) then Some(floatvalue)
    else None

let parseNumeric str =
    match str with
    | Integer i -> printfn "%d : Integer" i
    | Float f -> printfn "%f : Floating point" f
    | _ -> printfn "%s : Not matched." str

parseNumeric "1.1"
parseNumeric "0"
parseNumeric "0.0"
parseNumeric "10"
parseNumeric "Something else"

Az eredmény a következő:

1.100000 : Floating point
0 : Integer
0.000000 : Floating point
10 : Integer
Something else : Not matched.

A részleges aktív minták használatakor az egyes konkrét minták lehetnek egymáshoz képest diszjunktak vagy egymást kölcsönösen kizáróak, azonban ezeknek nem kötelező teljesülnie. A következő példában a Square és Cube minták nem diszjunktak, ugyanis léteznek olyan számok, amelyek négyzet-, és köbszámok is egyszerre (ilyen például a 64). A példaprogram azokat a számokat írja ki a képernyőre 1 millióig, amelyek négyzetszámok és köbszámok is egyszerre.

let err = 1.e-10

let isNearlyIntegral (x:float) = abs (x - round(x)) < err

let (|Square|_|) (x : int) =
    if isNearlyIntegral (sqrt (float x)) then Some(x)
    else None

let (|Cube|_|) (x : int) =
    if isNearlyIntegral ((float x) ** ( 1.0 / 3.0)) then Some(x)
    else None

let examineNumber x =
    match x with
    | Cube x -> printfn "%d is a cube" x
    | _ -> ()
    match x with
    | Square x -> printfn "%d is a square" x
    | _ -> ()

let findSquareCubes x =
    if (match x with
         | Cube x -> true
         | _ -> false
         &&
         match x with
         | Square x -> true
         | _ -> false
        )
    then printf "%d \n" x

[ 1 .. 1000000 ] |> List.iter (fun elem -> findSquareCubes elem)

Az eredmény a következő:

1
64
729
4096
15625
46656
117649
262144
531441
1000000

Paraméteres aktív minták

Az aktív minták legalább egy argumentummal mindenképpen rendelkeznek, ez ugyanis a későbbi mintaillesztésekben az illesztendő elem szerepét tölti majd be. Előfordulhat azonban, hogy egy adott aktív mintához további argumentumok is szükségesek, ilyenkor paraméteres aktív mintáról beszélünk. Ezek a további argumentumok egy általánosabb minta specializálását teszik lehetővé. Például azok az aktív minták, amelyek egy karakterlánc elemzését egy reguláris kifejezés alapján végzik el, abban az esetben maga a reguláris kifejezés egy további extra paraméternek tekinthető. A következő kódrészlet ezt mutatja be, ami egyébként a korábban már ismertetett Integer nevű részleges aktív mintát is felhasználja. Ebben a példában a karakterláncokhoz egy reguláris kifejezés is tartozik, amelyek segítségével dátumok különböző formátumban történő megjelenítése valósítható meg (az általános ParseRegex minta speciális esetei az egyes dátumformátumok attól függően, hogy milyen konkrét reguláris kifejezést adunk meg extra paraméterként). Az Integer aktív minta az egyes dátumrészeknek (nap, hónap, év) megfelelő szövegek egész számmá történő konvertálására használható fel, amelyek aztán átadhatók a DateTime osztály konstruktorának.

open System.Text.RegularExpressions

let (|Integer|_|) (str : string) =
    let mutable intvalue = 0
    if System.Int32.TryParse(str, &intvalue) then Some(intvalue)
    else None

// A ParseRegex minta a megadott regex reguláris kifejezés alapján elemzi az str stringet,
// és stringek egy listájával tér vissza, amennyiben a regex minden csoportjára illeszkedett az str string.
// A lista első eleme sikeres illesztés esetén az egész regex-nek megfelelő karakterlánc lenne,
// itt azonban csak az egyes csoportokra van szükség.
let (|ParseRegex|_|) regex str =
    let m = Regex(regex).Match(str)
    if m.Success
    then Some (List.tail [ for x in m.Groups -> x.Value ])
    else None

// Az alábbi mintaillesztés három különböző formátumú dátumra történik.
let parseDate str =
    match str with
    | ParseRegex "(\d{1,2})/(\d{1,2})/(\d{1,2})$" [Integer m; Integer d; Integer y]
        -> new System.DateTime(y + 2000, m, d)
    | ParseRegex "(\d{1,2})/(\d{1,2})/(\d{3,4})" [Integer m; Integer d; Integer y]
        -> new System.DateTime(y, m, d)
    | ParseRegex "(\d{1,4})-(\d{1,2})-(\d{1,2})" [Integer y; Integer m; Integer d]
        -> new System.DateTime(y, m, d)
    | _ -> new System.DateTime()

let dt1 = parseDate "12/22/08"
let dt2 = parseDate "1/1/2009"
let dt3 = parseDate "2008-1-15"
let dt4 = parseDate "1995-12-28"

printfn "%s %s %s %s" (dt1.ToString()) (dt2.ToString()) (dt3.ToString()) (dt4.ToString())

Az előző kódrészlet eredménye:

2008.12.22. 0:00:00 2009.01.01. 0:00:00 2008.01.15. 0:00:00 1995.12.28. 0:00:00

Típusok definiálása

Az F# több típus definiálási módszert engedélyez. Ezen a honlapon a következőkről találunk leírást:

Lusta kiértékelés

Az F# kiértékelése alapjáraton a Haskell mintáját követi, azaz mohó kiértékelésű. A lusta kiértékelés lényegében azt jelenti, hogy a számítás nem azonnal van kiértékelve, hanem csak akkor, amikor az eredményre ténylegesen szükség is van. Ezzel javíthatjuk a kód teljesítményét.

A lusta kiértékelést úgy tudjuk kikényszeríteni az F#-ból, hogy a következőképp írjuk meg a kifejezéseinket.

let identifier = lazy ( expression )

Az előző szintaxisban az expression egy olyan kód, ami csak akkor értékelődik ki, amikor szükség van az eredményére, és az identifier pedig egy érték, ami tárolja ezt az eredményt. Az érték típusa Lazy<'T>, ahol a tényleges típus, a 'T a kifejezés eredményének típusából lett kinyerve.

A lusta kiértékeléssel lehetőség van a teljesítmény növelésére azáltal, hogy korlátozzuk a számítások végrehajtását olyan módon, hogy csak olyan szituációkban hajtódjanak vére, amikor szükség van az eredményre.

Ha mégis kényszeríteni akarjuk a számítás elvégzését, akkor meghívatjuk a Force.Force metódusát, ami egyetlen egyszer elvégzi a művelet végrehajtását. Ha többször hívnánk meg a Force-t, akkor az mindig ugyanazzal az eredménnyel tér vissza, és nem hajt végre semmilyen kódot ekkor.

A következő kód bemutatja a lusta kiértékelés használatát a Force-val együtt. Ebben a kódban a result típusa Lazy és a Force visszatér egy inttel.

let x = 10 let result = lazy (x + 10) printfn "%d" (result.Force())

A lusta kiértékelést (de nem a Lazy típust!) használják még a sorozatok.