Az F# magasabb rendű típusai, mint a listák, tuple-ök vagy a függvénytípusok mind generikusak, ez azt jelenti, hogy különböző alaptípusokkal lehet őket példányosítani. Például: int list, string list, vagy az (int * int) list mind a generikus list típusból származnak. Hasonlóan, az int -> int vagy a string -> int típusok mind a generikus függvénytípusból származnak. Mind az F# beépített könyvtárai, mind a .NET tartalmaz még rengeteg generikus típust.
A generikus konstrukciók mindig típusváltozók segítségével vannak kifejezve, amik az F# szintaxisában a következőképpen néznek ki: 'a, 'b, 'key és így tovább. Például a generikus list típus így néz ki:
A típusváltozót írhatjuk prefix módon, mint ahogy az előző példában láttuk, vagy írhatjuk akár postfix módon is:
Az értékek is lehetnek generikusak. Egy tipikus generikus érték a List.map függvény, amelynek szignatúrája a következőképpen néz ki:
Minden alkalommal, amikor megnevezünk egy generikus típust vagy értéket, akkor az F# típusrendszere elvégzi a szükséges példányosításokat. Vegyük például a következő szignatúrájú egyszerű függvényt:
Ha ezt a dummy függvényt alkalmazzuk az előbb felírt szignatúrájú List.map generikus függvényre, akkor a következő példányosítások fognak megtörténni:
Így a map függvény visszatérési értékének a típusa (string * string) list lesz.
A generikus értékek és függvények (mint a List.map) teljesen szokványosak az F# nyelvben. Valójában annyira hétköznapiak, hogy a legtöbbször még a típusváltozókat sem szoktuk kiírni. Azonban van, amikor találkozhatunk olyan esettel, mikor ki vannak írva, például ha a Visual Studio mutatja meg nekünk a típust tooltip formájában. Ekkor ilyet is láthatunk:
Gyakran a típusváltozóknak implicit hatóköre van, ami az automatikus általánosítás (automatic generalization) szabályai szerint működik (lásd: Generikus függvények írása alfejezet). Ez azt jelenti, hogy új típusváltozók a legegyszerűbben úgy vezethetőek be, ha egyszerűen beleírjuk őket a típus annotációba.
Azonban ha akarjuk, akkor írhatjuk őket explicite a definícióba is. Ekkor azonban minden explicit kiírt típusváltozót használnunk is kell az annotációban.
Az F# kulcsfontosságú képessége a kód automatikus általánosítása (automatic generalization). Az automatikus általánosítás és a típuskikövetkeztetés kombinációja teszi a legtöbb F# programot jóval egyszerűbbé, hatékonyabbá és általánosabbá. Illetve a kód újrafelhasználhatóságát is nagyban növeli. Az automatikus általánosítás nélküli nyelvek arra kényszerítik a programozókat, hogy kigondolják, és explicite írják le a függvények legáltalánosabb típusát, és ez sokszor annyira fárasztó, hogy a programozók nem veszik a fáradságot, hogy minél absztraktabb és általánosabban felhasználható függvényeket írjanak.
Például a típus paraméterek automatikusan kerülnek bele a függvény szignatúrájába, ha olyan egyszerű függvényt írunk, amelynek a paraméterei nem függenek semmitől.
A kikövetkeztetett típus pedig:
Itt a getFirst függvény típusa automatikusan lett generikusra kikövetkeztetve. A függvénynek 3 típusváltozója van, az eredmény pedig a tuple első elemének a típusa lesz. Az automatikus általánosítás akkor lép életbe, amikor a let vagy member definíciók nem kötik meg minden input és output paraméter típusát. Azt mondhatjuk, hogy az automatikus általánosítást a jelen lévő típusváltozókra alkalmazzuk, így végül olyan függvényt kapunk, ami több típussal is használható.
Az automatikus általánosítás különösen hasznos, ha a bemenő paraméter függvény típusú. Például a következő kap két függvényt és egy tuple -t és a két függvényt alkalmazza a tuple két elemére.
És az általános kikövetkeztetett típus a következő:
Az F# és a .NET könyvtáraiban nagyon fontos generikus függvények is találhatók. Fontos ezek megértése (vagy legalább egy kép kialakítása arról, hogy hogyan is működnek), mivel az általunk írt F# kód néha automatikusan is generikussá válik, akár a programozó tudta nélkül is.
Az első ilyen primitív függvénycsoport a generikus összehasonlításhoz kapcsolódik, melyet gyakran szerkezeti összehasonlításnak (structural comparison) is neveznek. Minden alkalommal, mikor a < , > , < = , > = , = , < > , compare , min vagy max függvényeket használjuk az F# kódunkban, akkor gyakorlatilag generikus összehasonlítást használunk. Ezek az operátorok a Microsoft.FSharp.Core.Operators modulban találhatóak, amelyik minden F# programban automatikus megnyitódik. Fontos adatszerkezetek is használnak beépítetten generikus összehasonlítást, például ilyen a Microsoft.FSharp.Collections.Set vagy a Microsoft.FSharp.Collections.Map adatszerkezet. Az alapvető generikus összehasonlító operátorok szignatúrája a következő:
Ezek az operátorok mind a compare függvény segítségével vannak implementálva, amelyik 0-t ad vissza, ha az argumentumok egyenlőek, -1-et vagy 1-et, ha kisebb vagy nagyobb.
A legtöbb .NET-es típus implementálja a System.IComparable interfészt, így a generikus összehasonlítás ezeket az interfészeket is használja. Összehasonlítást használhatunk összetettebb típusokra is, például tuple-ök esetén is, ekkor balról jobbra haladó lexikografikus összehasonlítást használ az F#.
És ugyanígy használható a generikus összehasonlítás listákra és tömbökre is.
Amikor összetett, új adatszerkezeten akarjuk használni a generikus összehasonlítást, akkor a következőket kell végiggondolnunk:
A generikus összehasonlítás egyik fontos társa a generikus hashelés. Az elsődleges primitív függvény, amelynek hívására egy hash értéket kapunk az a hash függvény, amely szintén a Microsoft.FSharp.Operators modulban található.
A hash függvény szignatúrája:Amikor egyszerű szerkezetű típusokon használjuk a hash függvényt, akkor mindig egy egyedi int értéket kapunk vissza.
A generikus összehasonlításhoz hasonlóan a generikus hashelést is általában csak a beépített típusokra és algebrai adatszerkezetre (olyanokra, amelyek beépített alaptípusokból épülnek fel) használhatóak. A generikus hashelés új típusok esetén testreszabható a GetHashCode metódus felülírásával, vagy a Microsoft.FSharp.Core.IStructuralHash interfész megvalósításával.
Hasznos generikus függvény még az, amelyik képes bármilyen értéket formázottan kiírni. A legegyszerűbb mód ennek használatára a %A direktíva a printf függvényben, vagy pedig az any_to_string függvény:
Ezek a függvények a .NET és az F# reflexióját használják, hogy végigmenjenek az adott adatstruktúrán és formázottan jelenítsék meg az adatszerkezetet az értékekkel együtt. Az összetettebb típusok, mint például a listák, vagy a tuple-ök formázása az F# forrás szintaxisára hasonlít. A fel nem ismert értékeket a .NET-es ToString() metódus hívásával írják ki a pretty pinter függvények.
A box és unbox két nagyon hasznos generikus függvény arra, hogy az F# típusait egy "egységes" System.Object (F#-ban obj) típusra, illetve típusról konvertáljuk.
Nézzük pár példát, hogy hogyan is működnek ezek a függvények a valóságban:
Az unbox használata során általában meg kell adnunk egy cél típust. Ezt megtehetjük explicite úgy, hogy paraméterezzük az unbox függvényt (pl.: unbox< string > ), vagy megadhatjuk típusmegszorítás formájában is (pl.: unbox sobj : string). Ezek a formák ekvivalensek. Futtatás során az unbox-ok végrehajtása előtt még elvégez a rendszer egy ellenőrzést, hogy biztonságosan lehet-e a kiválasztott céltípusra konvertálni. Az obj típusú értékek tárolnak dinamikus típusinformációkat, és ha ezeknek ellentmond a kiválasztott céltípus, akkor a következő futási idejű kivétel váltódik ki:
Ezek a függvények azért is fontosak, mert sok .NET-es könyvtár nyújt további generikus függvényeket, melyeknek argumentumai obj típusúak lehetnek.
A .NET-es könyvtárak biztosítják egy implementációját a generikus szerializálásnak, ami egy egyszerű és gyors módja az adatok kimentésének, illetve hálózaton való átküldésének. A most következő rész felfogható egy példának is, amely megmutatja, hogy hogyan használhatóak a .NET-es könyvtárak az F#-ból a box és unbox függvények segítségével.
Először is definiálunk két függvényt a következő szignatúrával:A writeValue függvény kap egy bármilyen típusú értéket, és a megadott streamre írja ennek az értéknek a bináris reprezentációját. A readValue függvény megfordítja ezt a folyamatot, hasonló módon, mint ahogyan az unbox visszaalakítja a box hatását. Most nézzük meg a két függvény implementációját, amelyek a System.Runtime.Serialization.Formatters.Binary névtérben található .NET-es függvényeket is használnak.
A box és unbox függvények használatára azért volt szükség az implementációban, mert a Serialize és a Deserialize függvények paraméterei és visszatérési értékei is obj típusúak. Most pedig nézzünk egy példát arra, hogy hogyan használható ez a két függvény arra, hogy egy Microsoft.FSharp.Collections.Map< string, string > típusú értéket a FileStream -be írjunk és onnan visszaolvassuk.
A példában is látszik az, mint amiről már volt szó korábban, hogy a readValue használatakor meg kell adnunk explicite a kiolvasott érték típusát, mivel a readValue függvény unbox-ot használ. Ha a Map< string, string > helyére másféle típust írnánk, akkor futási idejű kivételt kapnánk, amikor vissza akarjuk olvasni az adatokat a FileStream-ről.
A következőkben megnézzük, hogy hogyan lehet a már meglévő kódjainkat generikussá (újrafelhasználhatóvá) tenni, és hogy hogyan jelöljük a generikus algoritmusok absztrakt paramétereit.
Általános programozási technika F# programok esetén, hogy a függvények paramétereket várnak, melyek befolyásolják a működésüket, és így lesznek az algoritmusok még általánosabbak, és még jobban újrafelhasználhatóak. A következő egyszerű példa a legnagyobb közös osztó (highest-common-factor = HCF) meghatározásának generikus implementációja.
Azonban ez az algoritmus nem generikus, ugyanis csak integer értékeken működik. Valójában, habár a (-) operátor az F#-ban alapértelmezésben túlterhelt, mégis legalább az egyik operandus típusának ismertnek kell lennie már fordítási időben. Továbbá a konstans 0 egy integer, és nincsen túlterhelve.
Mindezek ellenére egészen könnyen generikussá tehető a fent említett függvény. Ahhoz hogy elérjük ezt, szükségünk van egy explicit nulla értékre, egy kivonó függvényre és egy rendezésre. Erre itt egy példa:
A kikövetkeztetett, generikus szignatúra pedig a következő:
Ennek a generikus függvénynek az eredménye egy konkrét függvény, ami már használható.
Itt tulajdonképpen példányosítjuk a különböző típusokra a generikus függvényünket és kihasználjuk a (-) operátor túlterheltségét is.
Az előző generikus implementáció 3, egymással összefüggő paramétert vár. Gyakori technika, hogy összecsomagoljuk az összetartozó paramétereket. Egyik lehetőség erre, hogy definiálunk egy rekord típust, ami ezt a három értéket/függvényt összefogja.
A kikövetkeztetett szignatúrák a következők: