Haskell programozás bevezető
==============================
Nagy Donát, 2012. március-április
Ez az anyag a három előadásom ténylegesen elhangzott részeinek a kicsit összeszerkesztett változata.
Észrevehetőbb módosítások:
 * A 2. rész 2. bekezdése írja le, hogy mi az a Prelude (amit egyébként csak a 3. előadás legvégén mondtam el, de talán jobb hamarabb néven nevezni az "automatikusan importálódó függvénykönyvtár"-at).

0. Bevezető
============
Az előadásomban nem tételezek fel semmilyen előismeretet, túl nagy C/C++ gyakorlat esetleg hátrányt is jelent. Kérek mindenkit, hogy az előadásra tegye félre a (nem funkcionális) programozással kapcsolatos ismereteit. A nyelvet a következők miatt ajánlom mindenkinek:
 * matematikához közeli, tiszta, tömör szintaxis viszonylag kevés kivétellel, kulcsszóval és speciális szerkezettel
 * magasszintű programnyelv, C/C++/Pascal/stb-nél általában gyorsabban lehet kódolni és rövidebb kódot lehet írni
 * ennek ellenére nem szkriptnyelv, a lefordított kód egész gyorsan tud futni
 * az erős típusrendszer, a tömörség és az, hogy nem kell mindenhez pointerekkel bűvészkedni, lecsökkenti a programhibák számát
 * a könnyen létrehozható, teljes értékű adattípusként kezelt függvényekkel hatékonyan elkerülhető a kódduplikáció
 * hasznos apróságok: nagyon könnyen kezelhető csak memóriaméret által korlátozott méretű egészek; C kóddal való jó együttműködés

A Haskell nyelv szerkezete olyan, hogy sok dolog, ami más nyelvekben önálló egység, itt csupán egy általános fogalom egy speciális esete. Például számos gyakran használt operátor (teljesség igénye nélkül pl. +, -, *, <, >=, ==, ||, &&) ami más nyelvben a szintaxis része, itt egy (automatikusan importált) függvénykönyvtárban van definiálva, legtöbbjük egy megfelelő típusosztályok metódusaként, így a teljes megértésükhöz szükséges a típusosztály fogalma; vagy például az input/output műveletek az IO monádbeli értékek manipulálásán alapulnak, így a teljes megértésükhöz kell a Monad típusosztály megértése (de végül mindkét területen a hagyományoshoz hasonló szintaxist kapunk). Ezek miatt nyelv bemutatását nem a más programnyelveknél szokásos példák ("Hello world!", majd hasonló egyszerű teljes programok) bemutatásával kezdem, hanem apró kódrészleteket mutatok be példaként az egyes nyelvtani elemeket. Igyekszek nem recepteket adni, hanem a nyelv viszonylag tiszta, logikus szerkezetét feltárni, ezért néha bemutatok olyan dolgokat is, amelyeket alapszintű programozáshoz nem feltétlenül kell tudni, de ameyek végig ott vannak a dolgok háta mögött.

Az anyag elején megpróbálok a nyelv eszközeiből minél többet bemutatni, cserében nem fogom az elmondottakat rögtön aprópénzre váltani (például a lista típus említése után nem sorolom fel az (egyébként nagyon hasznos) beépített listakezelő függvényeket, hanem inkább ugrok más témákra).
Ebben a bevezetőben nem, vagy csak viszonylag sokára említek meg néhány szerkezetet, amelyek könnyen helyettesíthetők más szerkezetekkel (esetleg kicsivel hosszabb kód árán).

-- (e-mail szöveg vége)

1. Egy Haskell program felépítése
==================================
A Haskell kód deklarációkból áll, amelyek lehetnek egyszerű értékek (a továbbiakban "változók" néven emlegetem őket, de kb. a C/C++ const konstansoknak ill. a függvényeknek vagy a matematikai értelemben vett változóknak felelnek meg, nem lehet az értéküket utólag változtatni), típusdeklarációk (egy változó típusának a jelölése), új típusok és típusszinonimák definíciói, típusosztályok (lásd később) definíciói és példányosításai, továbbá egy-két ritkán használt egyéb fajta (saját operátorok kötési erősségének megadása, C-vel való együttűködéshez tartozó deklarációk stb.). A deklarációkat alapból pontosvesszők választják el egymástól, de ha egy sor első karaktere nem whitespace, akkor az elé automatikusan odaértendő egy pontosvessző -- ez lényegében csak annyit jelent, hogy ha egy deklarációt a következő sorban akarunk folytatni, akkor beljebb kell kezdeni (különben egyébként is olvashatatlan lenne a kód), a gyakorlatban szinte sosem használ az ember explicit pontosvesszőt. A deklarációk sorrendje lényegtelen, nyugodtan lehet a kódban később deklarált értékeket/függvényeket/típusokat/... használni. Végül a main nevű, kötelezően "IO ()" típusú változó fog értelmeződni a program futásakor (az IO () típus leírását lásd később).

Nagyobb Haskell programok állhatnak több modulból is, az itt elmondottak egy modul szerkezetét adják meg (az anyag vége felé írom le, hogy lehet több modulból álló programot készíteni). Egy speciális modul a Prelude, ami számos alapvető dolgot (pl. számok, karakterek, logikai értékek, listák, függvények típusai és a rajtuk végzett alapvető műveletek) definiál és a benne definiált dolgok automatikusan elérhetőek lesznek minden Haskell programban. Sokszor Prelude-beli definíciókat használok példaként, ezeket saját prgramban nem adhatjuk ki, mivel az "... kétszer lett definiálva" hibákhoz fog vezetni, de nem is kell kiadnunk, mert az általuk definiált dolgok már elérhetőek alapból.

A Haskell nyelv erősen típusos, minden érték típusa fordítási időben ismert, azonban a C/C++-szal ellentétben a fordító meg tudja határozni az egyes értékek típusát és csak ritka esetekben szükséges jelezni, hogy mi egy adott változó/érték típusa. Mindenesetre általában a globális változók/függvények típusát ki szokás írni -- egy függvény nevéből és típusából gyakran ki lehet találni, hogy mit csinál és ez lényegesen növeli a kód áttekinthetőségét.

A nyelv egy alapvető tulajdonsága a lusta kiértékelés: Egy Int (kb. a C "int" megfelelője; vagy bármi más hasonló típus) valójában nem egy/néhány az értéket tároló gépi szót jelent a memóriában, hanem egy pár bitnyi fejlécet és egy pointert, ami vagy a tényleges értéket tároló címre mutat, vagy arra a kódra, ami ki tudja számolni azt az értéket (a fejléc megmondja, melyikre). Ez konstansszorzónyi tárhely- és időveszteséget jelent, de cserébe a kód csak akkor fut le, ha tényleg szükség van az értékre (és lefutás után a pointer átáll a kiszámolt értéket tároló memóriacímre és a fejléc módosul jelezve, hogy most már a kiszámolt értéket tárolja a pointer, és ha újra kell az érték, az egyszerűen kiolvasódik onnan, ahova el lett mentve). Sajnos én eléggé hozzá vagyok ehhez szokva és nem mindig figyelmeztetek az anyagban arra, hogy valami emiatt működik; ha valami nagyon gyanúsan néz ki, megéri belegondolni, hogy nem-e emiatt működik jól.

2. Változók és típusaik megadása
=================================
Néhány példa:

a :: Int
a = 42
_mode :: Int
_mode = 0o777
c, c', c2 :: Integer
c = 18446744073709551616; c' = 0xdeadbeef
c2 = 42
tau = 6.283185307179586
n_A = 6.022e23
tau, n_A :: Double
ch, newLine :: Char
ch = 'A'
newLine = '\n'

Itt a "változónév(, változónév2, változónév3, stb.) :: típusnév" alakú dekalrációk a típusdeklarációk, a "változónév = érték" alakú deklarációk pedig a változók deklarációi. A változónevek kisbetűvel vagy aláhúzásjellel kezdődnek, utána tartalmazhatnak nagybetűket, számokat és aposztrófot is. A típusnevek (egy-két beépített, speciális szintaxist használó típustól eltekintve) nagybetűvel kezdődnek, utána ugyanúgy tartalmazhatnak kis- és nagybetűket, aláhúzásjelet, számokat és aposztrófot. Az itt használt típusok közül Int a gépi szó méretű (32 vagy 64 bites) egész, Integer a tetszőleges méretű egész, Double a C/C++ double megfelelője, Char pedig a karakter típus. A literálok megadása nagyon hasonló a C/C++-ban megszokotthoz (számok alapból decimálisban, 0o-val kezdve oktálisan (C-ben az oktális megadást csak egy 0 vezeti be!) , 0x-szel kezdve hexadecimálisan; lebegőpontos számokban tizedespont van (előtte és utána is legalább egy számjegy kell) és kaphatnak e-vel bevezetve egy módosítót, ami azt jelenit, hogy az értéket 10 az annyiadikonnal kell szorozni (így n_A az Avogadro-állandót értéke lesz); karaktereket aposztófok között kell megadni, '\n' az újsor, '\t' a tabulátor, '\0' a nulla kódú karakter, '\'' az aposztróf, '\\' a visszaperjel karakter (más escape-kódok is vannak, többnyire a C-hez hasonlóan)).

Megjegyzés1: A Haskell alapból Unicode-ot (UTF8) használ, úgyhogy a Char típus nem csak 128/256 értéket tárolhat, és a változónevekben is elvileg engedélyezettek az Unicode alfanumerikus karakterek (pl. ékezetes betűk), de ezt nem igazán szokás kihasználni. Fontos, hogy Haskellben a Char típus nem számtípus, nincsen automatikus konverzió pl. 100 és 'd' között.
Megjegyzés2: A következő 23 kulcsszavat nem lehet változónévnek használni:
case, class, data, default, deriving, do, else, foreign, if, import, in, infix, infixl, infixr, instance, let, module, newtype, of, then, type, where, _

3. További összetetteb típusok
===============================

Rendezett n-esek:
pt1, pt2 :: (Int, Int)
pt1 = (2, 3)
pt2 = (8, 0)
unit :: ()
unit = ()
q :: (Int, Integer, (Char, Char), (Int, Int), Double)
q = (42, 0xffffffffff, ('a', 'Z'), pt1, 123.45)

Egy rendezett n-es (n egytől különböző természetes szám) típusa zárójelek között, vesszőkkel elválasztva n másik típus. Egy ilyen típusú érték zárójelek között, vesszőkkel elválasztva megfelelő számú és típusú érték. A példák között pt1 és pt2 rendezett számpárok, unit egy rendezett nullás (ezt a () típust, aminek az egyetlen értéke (), szokás a triviális típusnak nevezni), q pedig egy rendezett ötös.

Listák:
numlist :: [Integer]
numlist = [1, 2, 3, 999999]
pairlist :: [(Int, Int)]
pairlist = [(1, 2), (3, 4)]
empty :: [Double]
empty = []
listOfLists :: [[()]]
listOfLists = [[], [()], [(), ()]]
cons1, cons2, inf :: [Integer]
cons1 = 23 : numlist
cons2 = 1024 : 42 : cons1
inf = 1 : 2 : 3 : inf

Egy lista típusa szögletes zárójelek között az elemeinek a típusa, egy lista típusú érték szögletes zárójelek között, vesszőkkel elválasztva néhány azonos típusú érték. Az utolsó három érték a : operátor segítségével van definiálva, ami egy (megfelelő típusú) elemet beilleszt egy lista elejére (tehát cons1 a [23, 1, 2, 3, 999999] lista). Ez az operátor alapvető fontosságú olyan szempontból hogy valójában egy nemüres [x1, x2, ..., xN] listakifejezés (x1 : x2 : ... : xN : []) rövidítése. A : operátor jobbra csoportosul, tehát cons2 az 1024 : (42 : cons1) kifejezést jelenti, nem a típushibás, értelmetlen (1024 : 42) : cons1 kifejezést. inf egy végtelen lista (lényegében egy olyan egyirányban láncolt lista, amiben a harmadik elem next tagja ismét az elsőre mutat); teljesen nyugodtan definiálható, de ne próbáljuk rekurzívan bejárni, mert az tényleg végtelen ciklust ad ...

Sztringek:
str1, str2, str3 :: String
str1 = "blah blah ' \" \n \0 xxx "
str2 = 'x' : ['a', 'b', 'c']
str3 = ""

Haskell-ben a sztring típus (String) egyszerűen a [Char] (karakterlista) típus szinonimája, szringeket megadhatunk dupla idézőjelek között vagy karakterlistaként (általában a kényelmesebb dupla idézőjeles megoldást választjuk). Természetesen a nulla karakter ('\0') is szerepelhet a szringben; a dupla idézőjelek között megadott sztringben szereplő szimpla idézőjelet nem kell visszaperjellel levédenünk, viszont a dupla idézőjelet igen.

4. Típusszinonimák
===================

A String típus alap függvénykönyvtárban szereplő definíciója a következő:
type String = [Char]
Általában egy típusszinonimát (ez a C/C++ typedef megfelelője) úgy lehet definiálni, hogy "type Szinonimanév = <típus>", vagy általánosabban, a definiáladnó szinonima függhet néhány (típus)paramétertől:
type Szinonimanév param1 param2 ... paramN = <típuskifejezés ami a param1 param2 ... paramk típusoktól függ>
Erre az általánosabb formára példák:
type PairOf a = (a, a)
type Assoc a b = [(a, b)]
A szinonimanév egy típusnév, tehát nagybetűvel kell kezdődnie, a paraméterek típusváltozók -- a nevükre ugyanazok a formai megkötések vonatkoznak, mint a változókra, csak típusokat jelölnek. A szinonima csupán az eredeti típus egy másik neve -- tehát az "(Int, Int)" típus helyett mindenütt lehet írni "PairOf Int"-et, vagy [(Int, Int)] helyett egyaránt lehet írni [PairOf Int]-et és Assoc Int Int-et is.

5. Függvények
==============

A függvényeknek központi szerepe van a funkcionális programozásban. Alapvetően függvény alatt egy olyan egyváltozós függvényt értünk, ami tiszta, azaz ugyanarra a bemenetre ugyanazt a kimentetet adja és a visszatérési értékén kívül nincs mellékhatása (nem ír fájlba/képernyőre, nem lő ki atomrakétát stb.).

Egy (eléggé egyszerű) függvény:
pushSpace :: String -> String
pushSpace = \s -> ' ':sű

A "->" típusszinten üzemelő operátor két típusból képzi azt a függvénytípust, ami az első típusú értékből második típusút állít elő. A függvényt pedig egy lambda abszrakcióval definiáltuk: a visszaperjel jelzi, hogy lambda absztrakció kezdődik (ez a lambdához a nyelv készítői szerint legjobban hasonlító ASCII karakter), utána egy változónév, nyíl ("->") és egy kifejezés, amiben a "\" után megadott változó helyére helyettesítődik be a függvénynek megadott érték (ez a nyíl utáni kifejezés a lehető legmesszebb terjed, tehát az előbbi definíció azzal egyenértékű, hogy "pushSpace = \s -> (' ':s)", nem "pushSpace = (\s -> ' '):s"-sel, ami egyébként is marhaság).

A függvényhívást egyszerű egymásután írás jelöli, "f x" az f függvény értéke az x helyen. Ez erősebben köt, mint az infix operátorok, tehát "pushSpace 'a' : ['b']" típushibás, mivel azt jelenti, hogy "(pushSpace 'a') : ['b']" és 'a' nem egy sztring; ezen felül balra csoportosul, azaz "f x y" azt jelenti, hogy "(f x) y". Ez azért van így megválasztva, hogy a többváltozós függvények előálljanak úgy, mint olyan egyváltozósak, amelyek elfogadják az első változójuk és visszaadnak egy függvényt, ami elfogadja a második változót és visszad egy függvényt ami ... végül az utolsó változó elfogadása után visszaadja a végeredményt.

Például a következő construct függvény ilyen módon kétváltozós:
construct :: Char -> (String -> String)
construct = \ch -> \str -> ch : str
abc :: String
abc = construct 'a' "bc"

A többváltozós függvények gyakori használata miatt (1) a nyíl típusoperátor jobbra csoportosul, azaz egyszerűen írhatjuk azt a típust, hogy "construct :: Char -> String -> String" (2) "\ch -> \str -> ch : str" helyett elég azt írni, hogy "\ch str -> ch : str" (de persze ekkor is megtehetjük, hogy csak az első paramétert adjuk meg a függvény meghívásakor és kapunk egy, a másik paraméterért még váró függvényt).

6. Polimorf függvények
=======================

Ha ennek a construct függvénynek nem adtunk volna meg kézzel a típusát, a fordító a következő típust állapította volna meg:
construct :: t -> [t] -> [t]
Ebben a típusmegadásban "t" egy típusváltozó (általában típuskörnyezetben egy kisbetűvel/aláhúzásjellel kezdődő "dolog" típusváltozó), és ez azt jelenti, hogy "construct típusa minden t típusra lehet t -> [t] -> [t]". Azokat a dolgokat, amiknek a típusában típusváltozó is szerepel, polimorfnak nevezzük. Nem csak egy függvény lehet polimorf, például a következő üres lista típusa minden típushoz lehet olyan típusú elemekből álló lista:
emptyList :: [a]
emptyList = []

Két további polimorf függvény az automatikusan importált függvénykönyvtárból, id az identitásfüggvény, flip pedig egy kétváltozós függvény két paraméterének sorrendjét fordítja meg (vegyük észre, hogy a típusában "b -> a -> c" körül felesleges a zárójel, de "a -> b -> c" körül nem):
id :: a -> a
id = \x -> x
flip :: (a -> b -> c) -> (b -> a -> c)
flip = \f x y -> f y x
Mivel gyakran (de nem mindig!) rendelünk a létrehozott függvényeinkhez rögtön egy nevet, ezért "függvénynév = \argumentumok -> törzs" helyett írhatjuk azt, hogy "függvénynév argumentumok = törzs", például flip esetében
flip f x y = f y x
vagy persze akár
flip f = \x y -> f y x
Ezt a lehetőséget általában ki szoktuk használni, ha mégsem, belefuthatunk a monomorfizmus korlátozás nevű őskövületbe (amiről az anyag vége felé írok) és az kellemetlen programhibákat okozhat.

7. Mintaillesztés
==================

A nyelv egy további erős eszköze a mintaillesztés, ami lényegében arról szól, hogy egy függvény definíciójában a paramétereket kereshetjük valamilyen alakban. A következő függvényeket (szintén beépítettek) mintaillesztéssel definiálhatjuk, rendezett pár első (FirST) ill. második (SecoND) elemét jelölik ki:
fst :: (a, b) -> a; snd :: (a, b) -> b
fst (x, y) = x
snd (x, y) = y
Gyakran előfordul, hogy egy paraméter többféle alakú lehet, ekkor mintaillesztéssel esetszétválaszthatunk, úgy hogy több összefüggést írunk fel a függvényre, például a következő (Prelude-beli) függvény megadja egy lista hosszát kihasználva, hogy egy lista vagy [] (azaz üres) vagy x : xs alakban van konstruálva egy x első elemből és egy xs listából
length :: [a] -> Int
length [] = 0
length (x:xs) = 1 + length xs
(Itt + persze az összeadás operátor; x:xs köré kell a zárójel, különben randán félre lenne értelmezve a dolog; ez a függvény rekurzív, ami Haskellben eléggé gyakori dolog.) Fontos, hogy míg egyébként a deklarációkat tetszőleges sorrendben írhatjuk a kódfájlba és nem hiba a kódban később definiált dologra hivatkozni, egy függvényre vonatkozó több eset közé nem keveredhet be más szemét, mert az "két helyen definiált változó" [jelen esetben függvény] hibát okoz.

Mintaként az eddig említett dolgok közül a következők használhatók:
 * változók, ezek mindig sikeresen illeszkednek és rajtuk keresztül elérhető az, amire illeszkedtek -- egy változó csak egyszer használható, nem lehet például "(a, a)"-val vadászni azon párokra, amiknek a két eleme ugyanaz.
 * az _ kulcsszó, ami mindig sikeresen illeszkedik, de nem menti el az értéket, így többször is használható. Ha az érték nem lényeges, általában ezt szokás használni, jelezve, hogy (az esetleg jó nagy) törzsben nem használjuk az értéket. Példa a használatára a következő (Prelude-beli) konstansfüggvény-generátor (amire "const x" a konstans x függvény):
const :: a -> b -> a
const x _ = x
 * változónév@(másikMinta), akkor illeszkedik, ha másikMinta illeszkedik és ekkor illeszti a másikMinta mintát, plusz ráadásul elmenti az egész illesztett kifejezést a megnevezett változóban. Például
func :: [a] -> [a]
func xs@(x:y:_) = x:y:xs
func _ = []
egy legalább kételemű lista lista első két elemét még egyszer beilleszti a lista elejére; bármi másra pedig üres listát ad vissza. (Az egy függvényre felírt sok egyenlet sorrendje is számít, az egyenletekben a minták felülről lefelé tsztelődnek, tehát pl. ha fordított sorrendben lenne ez a két egyenlet, akkor a függvény mindenre az üres listát adná vissza.)
 * rendezett n-es kifejezések (mint fst, snd definíciójában)
 * listáknál : "[]" és "minta : másikMinta", továbbá a kényelem kedvéért "[minta1, minta2, ..., mintaN]" és sztringek dupla idézőjelek között, például
func :: String -> Int
func "brisingr" = 42
func ('*':xs) = func xs
func ['f', _, 'c', 'k'] = -1
func [] = 0
func _ = 0xdead
 * konkrét számok (akár lebegőpontosak is, bár azoknak az egyenlősége gyanús téma), például a következő faktoriálisfüggvénynél a 0
fact :: Integer -> Integer
fact 0 = 1
fact n = n * fact (n-1)
 * karakterkonstansok (ez már ki volt használva a listás példában!)

8. A "case" szerkezet
======================

Ha az esetszétválasztást nem egy függvény paramétere szerint akarjuk végezni, a "case ... of ..." szerkezetet használhatjuk:
describe :: [a] -> String
describe xs = case length xs of
    0 -> "empty list"
    1 -> "one element"
    n -> show n ++ " elements"
(Itt a show függvény sztringgé alakít egy megfelelő értéket (jelen esetben számot), ++ pedig összefűz két listát (azaz speciálisan sztringet is).)
A case szerkezetet eredetileg úgy kellene írni, hogy "case kifejezés of { eset1 -> érték1 ; eset2 -> érték2 ; ... ; esetN -> értékN }", de egy layout-szabálynak nevezett elv szerint meg lehet spórolni a pontosvesszőket és a kapcsos zárójeleket. A szabály a következő: ha az "of" kulcsszó után nem egy "{" következik, érvénybe lép a layout-szabály: az of után következő első nem whitespace karakter (amire (*) néven hivatkozok) elé beszúródik egy "{", majd a következő sorokra, ha sor a (*) karakter oszlopában kezdődik, beszúródik elé egy ";", ha pedig a (*) karakter oszlopa előtt kezdődik, beszúródik elé egy  "}" és véget ér a blokk. A sor kezdésének a sok első nem whitespace karaktere számít; a tabulátorok a legközelebbi 8-cal osztható sorszámú oszlopig ugranak előre; a blokk akkori is bezáródik, ha olyan dolog következik utána, ami a blokkon belül értelmetlen lenne (pl. a példában a "2" esetbeli beágyazott case utáni záró zárójel).
Egy példa a layout-szabály használatára:
f :: Int -> Int -> Int
f x y = case x of
    0 -> 1
    1 -> 2 + 2 + 2 + 2
     + 2
    2 -> (case y of
        1 -> 2
        _ -> 23
            )
    _ -> case y of
        29 -> 45
        foo -> foo
ÁTALAKÍTVA:
f :: Int -> Int -> Int
f x y = case x of
    {0 -> 1
    ;1 -> 2 + 2 + 2 + 2 +
     + 2
    ;2 -> (case y of
        {1 -> 2
        ;_ -> 23
            })
    ;_ -> case y of
        {29 -> 45
        ;foo -> foo
}}
Valójában ennek a layout-szabálynak a speciális esete az egész programban a deklarációkat elválasztó pontosvesszők, valójában azok is ugyanígy működnek (tehát például ha valamiért az első nemüres sort whitespace-szel kezdjük, akkor az egész programot annyival beljebb kell kezdeni).

9. Logikai értékek
===================

debug :: Bool
debug  = True
longRun :: Bool
longRun = False
numCycles :: Int
numCycles = if debug then 10 else if longRun then 1000000 else 10000

A Bool típusnak két értéke van, True és False. Hozzá kapcsolódó szerekezet az if-then-else kifejezés, aminek az alakja if <logikai kif.> then <igaz érték> else <hamis érték>. Ez lényegében "case <logikai kif.> of {True -> <igaz érték>; False -> <hamis érték>}" rövidítése (a True, False értékekre lehet mintailleszteni). Az else ág olyan messzire nyúlik ki, amennyire csak tud, tehát "if True then 1 else 2 + 1" értéke 1 ("+" természetesen az összeadás). Viszonylag új apróság a nyelvben, hogy az "else" előtt szerepelhet egy pontosvessző, ez azért hasznos, mert így néha természetesebben írhatunk if-then-else-t egy layout-szabályos blokkon belül.

10. Lokális változók
=====================

A következő szerkezet nem létfontosságú a programozáshoz, de szükséges mások programjainak megértéséhez, mivel igen gyakori a használata:
A let-in kifejezés lokális változók deklarálására szolgál, alakja "let {dekl1; dekl2; ...; deklN} in <kifejezés>", itt dekl1, ..., deklN változódeklarációk vagy típusdeklarációk, amik csak a let-en belüli deklarációkban és <kifejezés>-ben láthatók; <kifejezés> pedig egy kifejezés, ami használhatja a lokális deklarációkat és aminek a típusa lesz az egész let kifejezés típusa. A pontosvessző-kapcsos zárójel blokkot a layout-szabállyal szokás megspórolni, <kifejezés> pedig a lehető legtovább tart, ha nem lenne egyértelmű, hogy meddig tart. A lokális változók elfedhetnak globális (vagy kijjebbi let blokkban definiált) változókat.
Példák:
repeat :: a -> [a]
repeat x = let xs = x:xs in xs
s1 :: String
s1 = let _'Freedom' = "Slavery" in _'Freedom'++" is a great thing!"
func :: Int -> String
func n = let n = max 0 n
    in [...hosszú kifejezés ami értelmetlen negatív n-re ...]
Itt repeat egy hasznos Prelude-beli függvény végtelen listák előállítására (lényegében olyan láncolt listát készít, aminek a next pointere önmagára mutat); s1 bemutatja, hogyan használható a let utasítás és a kreatív változónevek propagandacálokra; func pedig egy veszélyes buktatót mutat be, amikor ki akartunk javítani egy hibát és létrehoztunk egy végtelen ciklust adó rekurziót, mivel a "max 0 n" kifejezésben szereplő "n" nem a függvényünk paramétere, hanem az épp definiálandó "n" lesz.

11. Őrfeltételek
=================

Az őrfeltételek lehetővé teszik, hogy egy függvény egyenleteiben vagy egy case szerkezetben ne csak mintát illesszünk, hanem logikai feltételeket is megfogalmazzunk, például:
foo :: Int -> String
foo n
    | n > 10000 = "large"
    | n < 0 = "negative"
    | n == 6 = "perfect"
    | n == 28 = "perfect #2"
foo 0 = "zero"
foo 42 = "the Answer"
foo 2 = "even prime"
foo n | even n = "even"
      | n == 1 = "one"
      | otherwise = "odd"
foo _ = "buggy"

bar :: Int -> String
bar n = case foo n of
    "buggy" -> "buggy"
    _ | n>100000 -> "very large"
    ('e':xs) -> "description starts with 'e'"
    s | length s > 8 -> "long description"
      | length s < 4 -> "short description"
      | otherwise -> "[" ++ s ++ "]"

A lényeg az, hogy az adott (mintaillesztés szerinti) eset után egy függőleges az " = kifejezés" vagy " -> kifejezés" helyett függőleges vonal karakterek után logikai feltételek következnek, és minden egyes logikai kifejezés után külön " = kifejezés"/" -> kifejezés"-t adhatunk meg. Az őrfeltételek felülről lefelé ellenőrződnek és az első igaz értékűnek az esete adja az eredményt; ha nincs igaz értékű őrfeltétel az adott mintaillesztés szerinti esetben, akkor a következő mintaillesztás szerinti eset (vagy ha az nincs, futásidejű hiba) következik. A beépített könyvtár definiál egy "otherwise" nevű "Bool" típusú "True" értékű változót [konstanst] az őrfeltételek olvashatóbbá tételére.

11. A where záradék
====================

A let-in szerkezettel nem lehet megoldani, hogy egy lokális változót úgy definiáljunk, hogy az közös legyen az őrfeltételekben és az általuk szétvélasztott esetek törzsében; erre alkalmas a where záradék (amit őrfeltétel nélkül is használhatunk). Minden változódeklaráció, függvénydeklarációbeli eset és case-beli eset után írhatunk egy "where {dekl1; dekl2; ...; deklN}" alakú záradékot, ahol dekl1, ..., deklN változódeklarációk vagy típusdeklarációk, amik csak a where-en belüli deklarációkban és az adott eset őrfeltételeiben és törzsében láthatók; a pontosvessző-kapcsos zárójel blokkot a layout-szabállyal szokás megspórolni itt is.
Például:
quadratic :: Double -> Double -> [Double]
quadratic p q
    | d > 0 = [(-p + sqrt_d)/2, (-p - sqrt_d)/2]
    | d == 0 = [-(p/2)]
    | d < 0 = []
    where
    d = p*p - 4*q
    sqrt_d = sqrt d
Itt az aritmetikai és rendezési operátorok azt csinálják, ami elvárható tőlük, sqrt a gyökvonás függvény, aminek a típusa lehet pl. Double -> Double (ennél valamivel általánosabb a típusa); vegyük észre, hogy bár egy negatív számra futásidejű hiba sqrt-t alkalmazni, a lusta kiérétkelés miatt sqrt_d csak akkor értékelődik ki, ha d>0.

12. Operátorok
===============

Sok más programnyelvvel ellentétben Haskell-ben az operátorok nem speciális jelenségek, csak a változó(neve)k egy másik típusa. Egy operátor neve a ! # $ % & * + . : / < = > ? \ ^ | - ~ karakterekből (plusz bizonyos Unicode karakterekből) állhat, de nem lehet a :: = \ | <- -> @ ~ => jelsorozatok egyike sem és nem állhat kizárólag legalább két mínuszjelből. Ezeken felül a :-tal kezdődő operátornevek lényegében a nagybetűs azonosítók megfelelői, ilyen neveken nem lehet egyszerű változókat definiálni. Például itt van két logikai operátor definíciója (a tagadás pedig egyszerűen a "not" Bool -> Bool függvény), ahogy a Prelude-ban:
(&&), (||) :: Bool -> Bool -> Bool
True && x = x
False && _ = False
False || x = x
True || _ = True
Ha egy operátornevet zárójelbe rakunk, ugyanúgy használhatjuk, mintha egy változónév lenne (így adtuk meg a két operátor típusát), amikor egyenleteket írunk fel az operátorokra, írhatjuk őket infix alakban, mint a példákban, de zárójelek között prefix alakban is. (A lusta kiértékelés miatt ráadásul ezek automatikusan, speciális szintaxis nélkül rövidrezáróvá váltak: pl. "True || x"-ben x értéke nincs felhasználva, így nem gond, ha a kiszámítása futásidejű hibát (pl. nullával osztás), végtelen ciklust stb. okozna. Ezt a viselkedést pl. C-ben speciális esetként kellett kikötni ezekre ez operátorokra, ott nem tudunk olyan saját függvényt írni, ami ugyanezt a trükköt tudja, míg Haskell-ben ez egyáltalán nem rendkívüli viselkedés.)

Egy operátornak lehet deklarálni a kötési erősségét és a csoportosulási irányát az infix, infixl, infixr kulcsszavakkal.
Néhány fontos Prelude-beli operátor kötési erősségének a megadása:
infixr 9 .
infixl 7 *, /
infixl 6 +, -
infixr 5 :, ++
infix  4 ==, /=, <, <=, >=, >
infixr 3 &&
infixr 2 ||
infixr 0 $
Általában a egy ilyen deklaráció "infix[l/r/semmi] kötéserősség operátorok_felsorolása" alakú, ahol "kötéserősség" egy 0 és 9 közötti egész lehet. Egy "a <op1> b <op2> c" alakú kifejezés azt jelenti, hogy "(a <op1> b) <op2> c", ha op1 kötéserőssége a nagyobb, vagy ugyanaz a kötéserősségük és mindkettő infixl (az l a left rövidítése); azt jelenti hogy "a <op1> (b <op2> c)", ha op2 kötéserőssége a nagyobb, vagy ugyanaz a kötéserősségük és mindkettő infixr; végül minden más eset (azonos kötéserősség és vagy infixl/infixr pár, vagy legalább az egyik operátor infix) fordítási idejű hibát jelent. Ha egy operátorhoz nincs ilyen deklaráció, akkor az "infixl 9"-nek számít. Figyelem: a függvényalkalmazás erősebben köt minden operátornál, tehát "someFunction x*x+2" azt jelenti, hogy "(someFunction x)*x+2", nem azt, hogy "someFunction (x*x+2)"!

13. Néhány nagyon fontos Prelude-beli operátor
===============================================

A fentebb emlegetett operátorok a következőket csinálják:
. a függvénykompozíció operátor
(.) :: (b -> c) -> (a -> b) -> (a -> c)
f . g = \x -> f (g x)
*, /, +, - rendre a szorzás, valódi (azaz nem egész-)osztás, összeadás, kivonás operátorok
++ a listaösszefűzés operátor:
(++) :: [a] -> [a] -> [a]
[] ++ ys = xs
(x:xs) ++ ys = x:(xs++ys)
==, /=, <, <=, >=, > rendre az egyenlő, nem egyenlő, kisebb, kisebb egyenlő, nagyobb egyenlő, nagyobb operátorok (figyelem: C/C++-szal ellentétben a nem egyenlő operátor "/=" (a perjel az áthúzás akar lenni), nem pedig "!=").
&&, || a fentebb definiált logikai operátorok
$ a függvényalkalmazás operátor, ami gyakorlatilag nem csinál semmit:
($) :: (a -> b) -> a -> b
f $ x = f x
Mivel nagyon gyengén köt, elég gyakran szokás használni úgy, mint egy nyitó zárójelet, aminek a párja automatikusan odaértendő a kifejezés legvégére (gyakoriak például az olyasmi kifejezések, hogy "display . modifyBy foo . transform $ items ++ otherItems ++ [somethingElse]").

14. Részlegesen alkalmazott operátorok
=======================================

Az operátorokat is lehet részlegesen alkalmazni, egy tetszőleges # operátorra "(param #)" ekvivalens "(\x -> foo # x)"-szel, "(# param)" pedig ekvivalens "(\x -> x # param)"-mal; így például az a függvény, ami egy számhoz hozzáad egyet, egyszerűen a (+1), az a függvény pedig, ami egy értékhez hozzárendeli az azt tartalmazó egyelemű listát, úgy írható, hogy (:[]). Az operátor melletti kifejezésnek erősebben kötöttnek kell lennie, mint az operátornak, tehát (* 2 + 2) fordítási idejű hiba, nem a néggyel szorzó függvény és nem a kettővel szoroz, majd kettőt hozzáad függvény.

Kivétel: a mínusz operátort nem lehet az első paraméterét elhagyva részlegesen alkalmazni, mivel "(-x)" x ellentettjét fogja jelenteni, nem pedig a valamiből x-et levonó függvényt. Ez az ellentettképzés hatos erősséggel köt, tehát szinte mindig zárójelbe kell tenni, kivéve ha hatosnál gyengébb operátor mellett áll (tehát "func -5" azt jelenti, hogy func-ból le akarunk vonni ötöt, nem azt, hogy func-ot alkalmazni akarjuk a (-5)-re, de "func $ -5" vagy "-3 : numlist" azt csinálja, amit kell neki; természetesen pl. "[-1, -2]" és "(-3, '*')" is működnek). Ha az ellentettképzés függvényre van szükségünk, azt "negate" néven használhatjuk (vagy persze "(0-)"-ként is), (-) a kivonásfüggvény marad. A tiltott részleges alkalmazásforma helyett a subtract függvényt lehet használni: subtract = flip (-), azaz subtract n = \x -> x - n.

Ahogyan egy operátor zárójelek közé téve prefix alkalmazható függvénnyé alakítható, egy változót is használhatunk operátorként, ha vissza-idézőjelek (`, magyar billentyűzeten AltGr+7-tel lehet előbűvölni) közé tesszük a nevét. Példa erre az elem és a notElem beépített függvény, amik azt vizsgálják, hogy valami eleme-e (ill. nemeleme-e) egy adott listának. Például "elem 2 [1, 2, 4, 6]" igaz, de sokkal olvashatóbb, ha "2 `elem` [1, 2, 4, 6]"-ként írjuk. Fontos, hogy a vissza-idézőjelek között csak egy változónév állhat, bonyolultabb kifejezés nem (például "[2, 4] `flip elem` 4" szintaxishibás). Egy visszaidézőjeles kifejezést lehet operátorként részlegesen alkalmazni (tehát pl. "(4 `elem`)" ugyanaz, mint "elem 4", "(`elem` [2, 3])" pedig ugyanaz mint "\x -> elem x [2, 3]" (azaz "flip elem [2, 3]")). Egy visszaidézőjeles kifejezésnek lehat kötéserősséget megadni, például a beépített könyvtárban
infix 4 `elem`, `notElem`
szerepel.

Ennek a két függvénynek a típusának a megadásához a típusosztály fogalma szükséges, amit majd később írok le.

 -- (második előadás)

15. Típusdefiniálás, típuskonstruktorok
========================================

A Haskell erős típusrendszerébe könnyű új, saját típusokat bevezetni, sőt némelyik beépített alapvető típus is definiálható a nyelven belül.

Egy felsorolás jellegű típust egyszerűen a következő módon leht definiálni:

data Bool = False | True
data Monster = Orc | Kobold | Troll | Dragon | Necromancer | Skeleton

Egy ilyen data deklarációban létrejön egy típus és annak a lehetséges értékei -- ezek a lehetséges értékek az (adat)konstruktorok (nem keverendők a típuskonstruktorokkal és az objektumorientált programozásbeli konstruktorokkal). A konstruktorok nagybetűkkel kezdődnek, és lehet rájuk mintailleszteni (az új típusunk értékeit lényegében csak mintaillesztéssel vizsgálhatjuk).

Kicsivel összetettebb eset, amikor a konstruktoroknak paraméterei/adattagjai vannak:

data Point = Point Double Double
data Shape = Circle Point Double | Rectangle Point Point

A Point típus példa arra is, hogy a típusok és a konstruktorok külön névtérben vannak (ugyanúgy mint a típusváltozók és a normális változók), nyugodtan lehet ugyanaz egy típus és egy konstruktor neve; sőt egykonstruktoros típusoknál ez gyakori. A létrejött konstruktorokat függvényként használhatjuk, típusaik
Point :: Double -> Double -> Point
Circle :: Point -> Double -> Shape
Rectangle :: Point -> Point -> Shape
A következő módon lehet rájuk mintailleszteni:
area :: Shape -> Double
area (Circle _ r) = pi * r * r
area (Rectangle (Point x1 y1) (Point x2 y2)) = abs $ (x1-x2)*(y1-y2)
(Itt pi a Prelude-ban definiált konstans, abs az abszolútértékfüggvény.)

A data deklarációval olyan típusokat is létre lehet hozni, amik a (speciális szintaxissal jelölt) listákhoz hasonlóan más típus(ok)kal vannak paraméterezve (Tree saját példa, a másik kettő Prelude-beli típus):

data Maybe a = Nothing | Just a
(Nagyon gyakran használt típus, a Nothing értéket hiba jelzésére lehet használni, így nem kell minden felsorolás típusban fenntartani egy hibaértéket, hanem "Maybe Típus"-t lehet használni.)

data Tree a = Leaf a | Branch (Tree a) (Tree a)
 (Branch (Leaf 2) (Leaf 34))
data Either a b = Left a | Right b

Az itt létrejövő konstruktorok típusai:
Nothing :: Maybe a (azaz "Nothing" típusa minden "a" típusra lehet "Maybe a" -- nem csak egy függvény típusa lehet polimorf)
Just :: a -> Maybe a
Leaf :: a -> Tree a
Branch :: Tree a -> Tree a -> Tree a
Left :: a -> Either a b
Right :: b -> Either a b

Mintailleszteni természetesen lehet a konstruktorokra:
maybe :: b -> (a -> b) -> Maybe a -> b
maybe def _ Nothing = def
maybe _ func (Just val) = func val

Ez a függvény megpróbál egy "a -> b" függvényt alkalmazni egy "Maybe a"-ra, ha nem sikerül neki, mert Nothing van a "Maybe a"-ban, akkor a külön megadott alapártlmezett értékkel tér vissza.

either :: (a -> c) -> (b -> c) -> Either a b -> c
either f1 _ (Left x) = f1 x
either _ f2 (Right y) = f2 y

Az előbbihez nagyon hasonló függvény, akár "a", akár "b" van az "Either a b"-ben, mindig a megfelelő függvénnyel csinál belőle "c"-t

example :: Tree Char -> Bool
example (Branch (Branch (Leaf 'a') (Leaf 'd')) (Leaf 'X')) = True
example (Leaf _) = True
example _ = False

Ez az előzőeknél lényegesen kevésbé hasznos függvény a "(Branch (Branch (Leaf 'a') (Leaf 'd')) (Leaf 'X'))" fára és minden egyetlen levélből álló fára igazat ad, minden más fára hamisat.

16. Fajták
===========

Eddig a típuskifejezésekben főleg olyat "dolgok" szerepeltek, amik lehetnek egy változó típusai. A most létrehozott Maybe, Tree és Either nem ilyenek -- egy változó típusa nem lehet egyszerűen Maybe, hanem a Maybe típusszinten üzemelő (matematikai értelemben vett) függvényt alkalmazhatjuk egy típusra és ezután kapunk egy olyan típust (pl. "Maybe Int") ami lehet egy változó típusa. Ahogyan az értékeket, függvényeket és kifejezéseket típusokba soroljuk, a típuskifejezéseket is különböző fajtának (kind) nevezett csoportokba sorolhatjuk.

A fajták jóval egyszerűbbek, mint a típusok, kétféle van belőlük:
 - a "*" fajta azon típusok fajtája, ami egy változó típusa lehet.
 - ha "K1" és "K2" fajták, "K1 -> K2" az a fajta, amire ha egy "K1 -> K2" fajájú típust alkalmazunk egy "K1" fajtájú típusra, az eredmény "K2" fajtájú.

 - tehát például Maybe fajtája "* -> *", hiszen egy "*" fajtájú típusra (pl. "Int") egy szintén "*" fajtájút ("Maybe Int") kaphatunk.
 - Either fajtája "* -> (* -> *)" vagy egyszerűen "* -> * -> *", pl. "Either Integer" fajtája "* -> *".
 - ennél bonyolultabb fajták nagyon ritkák (legdurvább ami eszembe jut az RWST típus
"* -> * -> * -> (* -> *) -> * -> *" fajtával egy eléggé elterjedt függvénykönyvtárból).
 - a típusok fajtáit a fordító automatikusan ki tudja találni abból, hogy a data kulcsszó után álló "Típusnév p1 p2 ... pN" kifejezés "*" fajtájúra kell, hogy kijöjjön. (Alapból nem is lehet a kódba fajtajelölést beírni.)

17. Típusosztályok
===================

 * A típusosztályoknak nagyon kevés közük van az objektumorientált programozásbeli osztályfogalomhoz, némileg hasonlítanak az absztrakt ősosztály/interfész jellegű dolgokhoz, de számos dologban különböznek tőlük.
 * Számos olyan operátor/függvény van, ami jó lenne, ha sokféle típusú dolgot tudna kezelni, de nem értelmes mindenféle objektumra - például ilyen az egyenlőségvizsgálat, a rendezés, a különféle numerikus műveletek (összeadás, szorzás, osztás, trigonometrikus függvények stb.).
 * Erre Haskell-ben a típusosztályok adnak megoldást:
   (I) Egy típusosztály létrehozásakor meg kell adnunk néhány műveletnek (metódusnak) a típusát, amiket a bele tartozó típusoknak támogatni kell.
   (II) Egy típust egy típusosztály példányává tehetünk egy instance deklarációval, amiben megadjuk, hogy arra a típusra mit csinálnak a műveletek.
   (III) Egy polimorf függvény típusában megkövetelhetjük, hogy bizonyos típusváltozók csak bizonyos típusosztályok elemein fussanak végig, és így használhatjuk azokra a paraméterekre a típusosztály műveleteit.

(I) Típusosztály létrehozása -- például az egyik legegyszerűbb Prelude-beli típusosztály, az Eq típusosztály definíciója (picit lerövidítve):

class Eq a where
    (==), (/=) :: a -> a -> Bool

A fejlécben megadtuk a típusosztály nevét, amit egy típusváltozó, a típusosztály paramétere ("a") követ; ezután definiáltuk két metódust, az (==) (egyenlő) és (/=) (nem egyenlő) operátorokat és megadtuk, hogy a típusuk "a -> a -> Bool" lesz (ez itt most kivételesen nem azt jelenti, hogy minden  "a" típusra "a -> a -> Bool", mivel "a", a típusosztály paramétere, így ugyanazt a rögzített típust jelenti a típusosztály egész törzsében). A where kulcsszó után egy pontosvessző-kapcsos zárójel blokk jön, ami layout-szabályal elhagyható (azaz lényegében minden belső deklarációt beljebb kell kezdeni ugyanannyival).

(II) Példányosítás -- az Eq típusosztályt így lehet példányosítani a Bool típusra:

instance Eq Bool where
    True == True = True
    False == False = True
    _ == _ = False
    x /= y = not (x == y)

Itt is egy where utáni layout blokk van, amiben a metódusok ez esetbeli értékét definiáljuk -- itt már nem kell (és lehet) megadni a metódusok típusát (az már meg volt adva az osztály deklarációjakor).

Annak a típusnak, amire a típusosztályt példányosítjuk egy "atomi" típus néhány (0 vagy több) típusváltozóra alkalmazott változatának kell lennie, tehát lehet típusosztályt példányosítani például a "Bool", "Int", "Maybe a", "(a, b, c)", "a -> b", "[a]" típusokra, de nem lehet a "Maybe Int", "a" (vagy bármi más típusváltozó), "Int -> a", "(Bool, a)", "Maybe (a, b)" típusokra. Ez a korlátozás azért szükséges, mert ha van példányunk pl. "Maybe a"-ra és "Maybe Int"-re, nehéz eldönteni, hogy egy konkrét esetben melyik példány metódusait kell használni.

Ha a típusosztályt típusváltozókat is tartalmazó kifejezésre példányosítjuk, azokra előírhatunk típusosztályba tartozási feltételeket:

instance (Eq a) => Eq [a] where
    [] == [] = True
    (x:xs) == (y:ys) = x == y && xs == ys
    _ == _ = False
    x /= y = not (x == y)

(Természetesen lehet írni olyasmit, mint "instance (Foo a, Bar a, Baz b) => Foo (a, b) where ...".)

(III) Típusosztály használata -- a létrehozott típusosztályt így használhatjuk egy egyenlőségvizsgálatot használó függvény típusának megadására:

elem :: (Eq a) => a -> [a] -> Bool
elem _ [] = False
elem x (y:ys) = x == y || x `elem` ys

Ez a (korábban már bemutatott) függvény megmondja, hogy egy adott elem benne-van e egy listában; gyakran szokás visszaidézőjelek között operátorként használni.

(Természetesen íthatunk olyat, hogy "func :: (Foo a, Bar a, Eq b) => a -> Int -> [b] -> (a, b)".)

Megjegyzés: még egy ilyen típust sem kell feltétlenül megadnunk, a fordító még ennek a függvénynek a típusát is meg tudja határozni abból, hogy az egyes dolgokra milyen műveleteket használtunk.

18. További tudnivalók típusosztályokról
========================================

Egy típusosztály definíciójánál lehet alapértelmezett definíciókat megadni a metódusokra, hogy ha egy példányban nem definiáljuk a metódust, akkor az alapértelmezett definíció legyen használva. Ez pl. hasznos arra, hogy az Eq típusosztályban elég legyen (==) és (/=) közül az egyik működését megadni a példányokban.

Így az Eq típusosztály (teljes) definíciója

class Eq a where
    (==), (/=) :: a -> a -> Bool
    x /= y = not (x == y)
    x == y = not (x /= y)

Amikor egy típusosztályt definiálunk, megkövetelhetjük, hogy minden beletartozó típus tartozzon egy másik típusosztályba is; például ha rendezést definiálunk egy típusra, elvárhatjuk, hogy egyenlőségvizsgálat már legyen definiálva:

class  (Eq a) => Ord a  where
    compare              :: a -> a -> Ordering
    (<), (<=), (>=), (>) :: a -> a -> Bool
    max, min             :: a -> a -> a
    compare x y
         | x == y    =  EQ
         | x <= y    =  LT
         | otherwise =  GT
    x <= y           =  compare x y /= GT
    x <  y           =  compare x y == LT
    x >= y           =  compare x y /= LT
    x >  y           =  compare x y == GT
    max x y
         | x <= y    =  y
         | otherwise =  x
    min x y
         | x <= y    =  x
         | otherwise =  y

Itt az Ordering típus definíciója a következő:
data Ordering = LT | EQ | GT
értékeinek jelentése LT = less than, EQ = equal, GT = greater than.

Természetesen lehet egynél több ősosztály is, pl. "class (Foo a, Bar a) => Baz a where ...", viszont a metódusok típusában nem szabhatunk további típusosztályfeltételeket az épp definált típusosztály paraméterére (érdemes átgondolni, hogy miért), tehát a "good" metódus értelmes, "bad" viszont nem:
class Foobar a where
    good :: (Eq b) => a -> b -> Bool
    bad  :: (Eq a) => a -> b -> Bool

19. További hasznos beépített típusosztályok
============================================

class Bounded a where
    minBound         :: a
    maxBound         :: a
Korlátos típusoknál ez az osztály adja meg a legkisebb és legnagyobb értéket a típusban.

class Show a where
    show :: a -> String

(Ezen felül van még két metódus, amik nem fontosak, nagyon ritkán kell őket használni.) A show metódus egy értéket sztringgé alakít. A Show osztály példányosítva van rengeteg beépített típusra, így pl.
show 345 == "345"
show 'a' == "'a'"
show [1,2,4] == "[1,2,4]"
show "foobar\"\n" == "\"foobar\\\"\\n\""
show (1,2,3) == "(1,2,3)"
show pi = "3.141592653589793"
show (Maybe ()) == "Maybe ()"
(Vegyük észre, hogy String-ekre nem identitás a show függvény, idézőjelek közé teszi a sztringet és escape-el egyes karaktereket.)

Néha előfordul, hogy egy részkifejezés típusát nem tudja a fordító egyértelműen meghatározni (szinte mindig azért mert nem is lehet), pl. a "show minBound" kifejezés jellegzetes példa erre. Ilyenkor (meg máskor is) lehet egy részkifejezés típusát explicit jelölni a következő módon:
show (minBound :: Int)
Ez a ":: Típus" típusmegjelölés nagyon-nagyon gyengén köt, tehát pl. "show (myFunction $ 32 :: [Bool])" működik (pedig $ 0 kötéserősségű), de "func $ minBound :: Int"-ben nem minBound, hanem az egész kifejezés értékére mondtuk, hogy Int típusú.

A felsorolás jellegű típusokat az Enum típusosztállyal kezelhetjük:
class Enum a where
    succ, pred       :: a -> a
    toEnum           :: Int -> a
    fromEnum         :: a -> Int
    enumFrom         :: a -> [a]             -- [n..]
    enumFromThen     :: a -> a -> [a]        -- [n,n'..]
    enumFromTo       :: a -> a -> [a]        -- [n..m]
    enumFromThenTo   :: a -> a -> a -> [a]   -- [n,n'..m]
(toEnum-ból és fromEnum-ból vannak alapértelmezett definíciók a többire; legalább két kötőjel egymás után megjegyzést kezd, ami a sor végéig tart, de csak ha utána nem más speciális karakter van, tehát pl. "--!!" lehet egy operátor neve, de "-- !!"-ben a két kötőjel után megjegyzés kezdődik.)

Ehhez az osztályhoz kapcsolódik a számtani sorozat speciális szintaxist, mely szerint pl. ['a'..'z'] == "abcdefghijklmnopqrstuvwxyz";  [1..] a pozitív egészek végtelen listája, [1,3..] a páratlan pozitív egészek végtelen listája és ['a','c'..'z'] == "acegikmoqsuwy"; ezek az "enumFromXXX" metódusoknak felelnek meg.

Számos osztályt viszonylag unalmas kézzel példányosítani, ezért a data deklaráció után lehet írni egy deriving kulcsszót és utána zárójelben vesszőkkel elválasztva néhány példányosítandó osztályt, pl.

data Colour = Red | Green | Blue | Yellow | White | Black deriving (Eq, Ord, Show, Bounded, Enum)

és ezután a következők mind értelmesek és igaz értékűek:
Red <= Green == True
Blue > White == False
maxBound == Black
fromEnum Red == 0
succ Blue == Yellow
show Yellow == "Yellow"
[Green, Yellow..] == [Green, Yellow, Black]

Az itt használt 5 típusosztályon kívül alapból csak a Read-re (lásd később) alkalmazható a deriving szerkezet, a pontos szabályai (pl. pontosan mikor lehet?) le vannak írva pl. a "http://www.haskell.org/onlinereport/derived.html" weboldalon.

20. Numerikus típusosztályok
============================

A különféle számtípusokon végezhető műveletek jópár típusosztály metódusaiként vannak definiálva. Ez a típusosztályrendszer kb. 20 éve jött létre és azóta kb. változatlan, mert ha változtatva lenne, minden Haskell programot módosítani kéne. Ennek köszönhetően a rendszer nem teljesen logikus, gyakran vannak nem igazán szükséges feltételek, vagy olyan típusosztályok, amiknek egy-két metódusát ne tudjuk definiálni, de a többi jó lenne, ilyenkor a következő két beépített "függvényt" szokás használni a hiányzó metódusokra:
error :: String -> a
undefined :: a
Ha egy error által visszaadott érték, vagy undefined kiértékelődik, futásidejű hiba keletkezik, error esetében a hibaüzenet valószínűleg tartalmazni fogja a neki átadott hibaüzenetet.

Voltak/vannak javaslatok, hogy a típusosztályok olyanok legyenek mint félcsoport, csoport, gyűrű, test stb., de jelenleg a következőket kell elviselni:

===[ Num ]===========================

class  (Eq a, Show a) => Num a  where
    (+), (-), (*)    :: a -> a -> a
    negate           :: a -> a
    abs, signum      :: a -> a
    fromInteger      :: Integer -> a

        -- Minimal complete definition:
        --      All, except negate or (-)
    x - y            =  x + negate y
    negate x         =  0 - x

Ennek szinte minden numerikus típus példánya, ezek a példányok gyakran (Int, Double, ...) a gépi primitív műveleteket hívják, nem tiszta Haskell-ben vannak írva.

Például így lehet egy a kételemű testet megcsinálni:

data F2 = Zero | One deriving (Eq, Ord, Show, Enum, Bounded)

instance Num F2 where
    x + y = if x == y then Zero else One
    x * y = min x y
    negate = id

    -- ez a kettő nyugodtan lehetne undefined is, de inkább adok rájuk valamit
    abs = id
    signum _ = One

    fromInteger x = if even x then Zero else One

A Num osztály körüli fekete mágia, hogy ha azt írjuk a kódba, hogy 123, akkor az azt jelenti, hogy fromInteger (123 :: Integer) (leszámítva, hogy ez a definíció önhivatkozó), így pl. F2 elemeit is írhatjuk 0-nak meg 1-nek.

A polinomok (sőt, lényegében hatványsorok) lehető legrövidebb bevezetése - a polinomokat az együtthatóik listájaként definiáljuk:

instance (Num a) => Num [a] where
    [] + xs = xs
    xs + [] = xs
    (x:xs) + (y:ys) = (x+y):(xs+ys)
    (f:fs) * (g:gs) = f*g : [f]*gs + fs*(g:gs)
    _ * _ = []
    negate = map negate

    abs = undefined
    signum = undefined
    fromInteger n = [fromInteger n]
x :: (Num a) => [a]
x = [0, 1]

Itt map :: (a -> b) -> [a] -> [b] egy nagyon fontos beépített függvény, ami egy függvényt alkalmaz egy lista minden elemére. (Rekurzívan viszonylag könnyen definiálható, ezt tanulságos megpróbálni.)

Ezután pl. a következők igazak:
(2*x + 3)^5 :: [Int] == [243,810,1080,720,240,32]
(x + 1)^12 :: [F2] == [One,Zero,Zero,Zero,One,Zero,Zero,Zero,One,Zero,Zero,Zero,One]

(Itt (^) a hatványozás operátor.)

További, kevésbé fontos numerikus típusosztályok:

===[ Real ]===========================

class  (Num a, Ord a) => Real a  where
    toRational       ::  a -> Rational

Az elnevezések nem teljesen matematikailag pontosak, a Rational típusról annyit kell tudni, hogy tetszőleges pontosságú racionális számokat tárol (lényegében (Integer, Integer) pár) és az értékeit a (%) operátorral lehet képezni, egy racionális számot "számláló % nevező"-ként lehet írni.

===[ Integral ]===========================

class  (Real a, Enum a) => Integral a  where
    quot, rem        :: a -> a -> a
    div, mod         :: a -> a -> a
    quotRem, divMod  :: a -> a -> (a,a)
    toInteger        :: a -> Integer

        -- Minimal complete definition:
        --      quotRem, toInteger
    n `quot` d       =  q  where (q,r) = quotRem n d
    n `rem` d        =  r  where (q,r) = quotRem n d
    n `div` d        =  q  where (q,r) = divMod n d
    n `mod` d        =  r  where (q,r) = divMod n d
    divMod n d       =  if signum r == - signum d then (q-1, r+d) else qr
                        where qr@(q,r) = quotRem n d

Ez az osztás definiálja a maradékos osztást kétféleképpen is (sohasem jegyzem meg melyik melyik, a negatív számok kezelésében különböznek); továbbá az Integer-ré való konverziót.

Néhány fontos függvény típusában szerepel ez az osztály:

even, odd        :: (Integral a) => a -> Bool
gcd              :: (Integral a) => a -> a -> a
lcm              :: (Integral a) => a -> a -> a
(^)              :: (Num a, Integral b) => a -> b -> a

===[ Fractional ]===========================

class  (Num a) => Fractional a  where
    (/)              :: a -> a -> a
    recip            :: a -> a
    fromRational     :: Rational -> a

        -- Minimal complete definition:
        --      fromRational and (recip or (/))
    recip x          =  1 / x
    x / y            =  x * recip y

Viszonylag fontosabb osztály, definiálja a "valódi" osztás operátort.

===[ Floating, RealFrac, RealFloat ]===========================

Három típusosztály, amik lényegében függvénygyűjtemények a beépített lebegőpontos típusoknak:

class  (Fractional a) => Floating a  where
    pi                  :: a
    exp, log, sqrt      :: a -> a
    (**), logBase       :: a -> a -> a
    sin, cos, tan       :: a -> a
    asin, acos, atan    :: a -> a
    sinh, cosh, tanh    :: a -> a
    asinh, acosh, atanh :: a -> a


class  (Real a, Fractional a) => RealFrac a  where
    properFraction   :: (Integral b) => a -> (b,a)
    truncate, round  :: (Integral b) => a -> b
    ceiling, floor   :: (Integral b) => a -> b


class  (RealFrac a, Floating a) => RealFloat a  where
    floatRadix       :: a -> Integer
    floatDigits      :: a -> Int
    floatRange       :: a -> (Int,Int)
    decodeFloat      :: a -> (Integer,Int)
    encodeFloat      :: Integer -> Int -> a
    exponent         :: a -> Int
    significand      :: a -> a
    scaleFloat       :: Int -> a -> a
    isNaN, isInfinite, isDenormalized, isNegativeZero, isIEEE
                     :: a -> Bool
    atan2            :: a -> a -> a

 -- (3. előadás)

21. A Functor típusosztály
===========================

Egy típusosztály paramétere nem csak * fajtájú lehet, két fontos (Prelude-beli) típusosztály paramétere is * -> * fajtájú.

Az egyszerűbb a Functor típusosztály, aminek a következő a definíciója:

class Functor f where
    fmap :: (a -> b) -> f a -> f b

(Ez a két típusosztály kategóriaelméleti fogalmakról kapta a nevét. Sajnos nem tudok annyit kategóriaelméletről, hogy ezt kihasználva érthetőbbé tegyem a témát a kategóriaemélethez esetleg értők számára; mindenesetre a kategóriaelmélet ismerete egyáltalán nem szükséges ezen típusosztályok megértéséhez.)

Ha "f" egy a Functor típusosztályba tartozó típus, akkor egy "f a" típusú objektumot általában úgy lehet elképzelni, mint valamilyen struktúrát, amiben "a" típusú adattagok/részek vannak és "fmap func" az a függvény, ami változatlanul hagyva a struktúrát a benne ülő "a" típusú objektumokra alkalmazza a "func" függvényt.

A Functor osztály példányosításánál elvárás (ami nincs ellenőrizve, de a be nem tartása kb. olyan jelleggel okoz káoszt mintha pl. az (==) operátor nem lenne ekvivalenciareláció egy típusra) hogy fmap id = id és fmap (f . g) = fmap f . fmap g (az egyenlőségjeleket úgy értve, hogy a két függvény ugyanúgy viselkedik).

Példák Functor példányokra:

data Maybe a = Nothing | Just a
instance Functor Maybe where
    fmap _ Nothing = Nothing
    fmap f (Just x) = Just (f x)

Egy "Maybe a" típusú dologban vagy ül valami, vagy nem, ha ül, alkalmazzuk rá a függvény, ha nem, csak Nothing-ot tudjuk visszaadni (nem tudunk valamit csinálni a semmiből). Átgondolható, hogy nem lehet máshogy FUnktorrá tenni a Maybe típust (persze az error / undefined jellegű dolgokat leszámítva).

instance Functor [] where
    fmap = map
map _ [] = []
map f (x:xs) = f x : map f xs

A lista típus is Functor példánya; itt "[]" típuskörnyezetben szerepel, ahol nem az üres listát fogja jelenteni, hanem a listatípus konstruktorát, azaz azt a * -> * fajtájú típust, ami egy "a" típusból elkészíti az "[a]" típust (azaz "[] a" ugyanazt jelenti, mint "[a]", éljen a speciális szintaxis ...).

Az fmap függvény innen kapta a nevét, ami arra utal, hogy a map függvény tetszőleges funktorra való általánosítása. Ezt a példányt eléggé ritkán szokás használni, mivel helyette inkább direktben a map függvényt használják, hogy ne kelljen olyasmikat írni, hogy "fmap fmap fmap fmap fmap" ahol minden fmap más funktorban működik ...

instance Functor ((,) t) where
    fmap f (c, x) = (c, f x)
(,) a b = (a, b)
Még egy zseniális speciális szintaxis, "(,)" a rendezett pár típus * -> * -> * fajtájú konstruktora, így ((,) t) az * -> * fajtájú típus, ami egy "a" típusból a "(t, a)" típust készíti el. Azaz az fmap ezen speciális esetének a típusa
fmap :: (a -> b) -> (t, a) -> (t, b)

A Functor típusosztályt kétféleképpen lehet általánosítani: Az egyik az, amikor megkötéseket teszünk az (adat)szerkezet alakjára és pl. olyan műveleteket definiálunk, amikkel össze lehet gyűjteni a tartalmát. A másik a következő Monad típusosztály.

22. A Monad típusosztály
========================

A Functor típusosztálynál erősebb feltételt szab egy típusra a Monad típusosztály; aminek a definíciója a következő:
infixl 1 (>>), (>>=)
class Monad m where
    (>>=) :: m a -> (a -> m b) -> m b
    (>>) :: m a -> m b -> m b
    return :: a -> m a
    fail :: String -> m a

    x >> y = x >>= const y
    fail s = error s

Ha "m" egy a Monad típusosztályba tartozó típuskonstruktor, akkor egy "m a" típusú objektumot általában úgy lehet elképzelni, mint egy számítást, ami valamilyen módon "a" típusú érték(ek)kel tér vissza.

Minden monádra definiálható Functor példány, amiben "fmap f xs = xs >>= return . f", de történeti okokból nem "class (Functor m) => Monad m where ..."-ként van definiálva a Monad típusosztály.

A monádokra is vonatkozik néhány törvény, ezek a következők:
 (1) return a >>= k  =  k a
 (2) m >>= return  =  m
 (3) m >>= (\x -> k x >>= \y -> h y)  =  (m >>= \x -> k x) >>= \y -> h y
(plusz a "fmap f xs = xs >>= return . f" funktornak is jónak kell lennie).

A legkevésbé fontos metódus a fail, ami egy "döglött" számítást készít egy hibaüzenetből - ha a monádban nincs értelmes reprezentációja a hibának, az alapértelmezett definíciót szokás használni, ami a beépített error "függvényt" hívja, aminek a típusa "String -> a" és egy olyan érvénytelen értékkel tér vissza, amit ha ki akarunk értékelni (lusta kiértékelés!) leáll a program és kiíródik a megadott sztring. Ennek igaziból nagyon nem kéne a monád osztályban lennie, leginkább történeti okokból van itt.

A return metódus egy értékből létrehozza azt a számítást, ami nem csinál semmi mást (az (1) és (2) szabály szerint), csak visszatér az adott értékkel. Ez nem egy speciális utasítás, nem szakítja meg a függvény futását, nem ugrik sehová, nem lövi lábon a programozót stb. mint más programnyelvekben.

A (>>=) metódus lehetővé teszi, hogy egy számítást folytassunk egy, az első számítás eredményétől függő második számítással. (>>) ennek az a speciális esete, amikor a második számítás nem függ az első eredményétől. A (3) szabály lényegében ennek a (>>=) műveletnek egyfajta asszociativitását fogalmazza meg.

23. Néhány példa monádokra:
===========================

instance Monad Maybe where
    Nothing >>= _ = Nothing
    Just x >>= f = f x
    return = Just
    fail _ = Nothing


A Maybe monád olyan számításokat ad, amik lehet, hogy nem szolgáltatnak értéket -- ez az, amit pl. C-ben úgy szokás megoldani, hogy
int tmp = some_function(kwd);
if (tmp == -1)
    return -1;
else {
    char *tmp2 = second_function(tmp, "foobar");
    if (!tmp2)
        return -1;
    else {
        int tmp3 = parse(tmp2, 123.0, FALSE);
        // ...

itt pedig kb. úgy néz ki, hogy

someFunction kwd >>= secondFunction "foobar" >>= parse 123.0 False >>= ...

Ráadásul nem okoz gondot az sem, ha az adott típusnak nincs olyan értéke, mint például NULL vagy -1, ami nem értelmes eredmény, hiszen a "Maybe a" típusban mindig ott a Nothing hibaérték; valamint "Maybe a" és "a" különböző típusok, így nem fordulhat elő, hogy egy hibaértéket visszaadó függvénynél nem kezeljük a hibát.

instance Monad [] where
    [] >>= _ = []
    (x:xs) >>= f = f x ++ (xs >>= f)
    return x = [x]
    fail _ = []

A lista monád olyan számításokat ad, amik lehet, hogy egy értéket sem adnak vissza, de az is lehet, hogy többet; például

vertices graph >>= \v -> neighbours graph v >>= \v2 -> if test v v2 then return (v, v2) else []

jellegű kifejezéssel kereshetjük egy gráfban a megfelelő tulajdonságú éleket (gráf adatszerkezet nincs alapból importálva, a Data.Graph modulban van egy implementáció, de elég könnyű implementálni egyébként is).

24. Még egy igen fontos példa - az IO monád:
============================================

instance Monad IO where
    {- black magic -}

Az IO monád olyan számításokat ad, amik kölcsönhathatnak a külső világgal, írhatnak a képernyőre és a fájlokba, adatokat olvashatnak be stb. Az IO típus (IO fajtája * -> *, mint bármilyen más monádtípusnak) egy absztrakt adattípus, nincsenek publikus konstruktorai, de számos olyan függvény van (némelyik alapból importálódik, mások különféle modulokban vannak), amik különféle "a" típusokra "IO a" típusú számításokat állítanak elő. Míg más monádokhoz általában van olyan módszer, ami "lefuttatja" a számítást és megvizsgálja az eredményt (például a Maybe és lista mondádokban egy Maybe a vagy [a] típusú objektumot mintaillesztéssel szét lehet szedni; bonyolultabb monádokra gyakran van egy kb. "runMyMonad :: MyMonad a -> MyInputType -> MyInputType2 -> (SomeDataStructure a, OtherResult)" kinézetű függvény), az IO monádnál nincs erre lehetőség: Az egyetlen lehetőség IO számítások futtatására az, hogy a (>>=) és (>>) operátorokkal létrehozunk egyetlen "IO ()" típusú számítást, amit elnevezünk "main"-nek és a program futásakor ez a számítás fog végrehajtódni.

A következő gyakran használt IO számítások(at készítő függvények) alapból importálódnak:
putStr, putStrLn :: String -> IO ()
print :: (Show a) => a -> IO ()
print = putStr . show
getLine :: IO String
type FilePath = String
readFile :: FilePath -> IO String
writeFile :: FilePath -> String -> IO ()
appendFile :: FilePath -> String -> IO ()
Itt putStr és putStrLn kiír egy sztringet stdin-re, az utóbbi egy újsort is a végére rak; print sztringgé alakít egy értéket és megjeleníti; getLine beolvas egy sort stdin-ről; readFile, writeFile és appendFile egész fájlokat olvasnak/írnak -- lehet importálni számtalan más műveletet is, de ennyi kb. elég, ha tiszta (pl. matematikai) számításokat akarunk végezni és csak az eredményt/bemenetet akarjuk fájlokban tárolni.

Ezekkel például egy egyszerű program így néz ki:
main = putStrLn "Hello world!" >>
       putStr "Enter your name: " >>
       getLine >>= \name ->
       putStrLn ("Welcome, "++name++"!")

A nyelvbe szerepel a "do" szintaktikai cukor, amivel ezt a szokásos programnyelvekhez hasonlóbban írhatjuk:

main = do
   putStrLn "Hello world!"
   putStr "Enter your name: "
   ('a':name) <- getLine
   putStrLn $ "Welcome, "++name++"!"

Ez a do kulcsszó is layout-szabállyal irányított blokkot használ, mint szinte minden más hasonló dolog.

A do kifejezést a következők szerint lehet átfordítani a >> és >>= operátorokra:
 (1) "do {kifejezés}" ugyanaz, mint "kifejezés"
 (2) "do {kifejezés ; ...}" ugyanaz mint "kifejezés >> do { ... }"
 (3a) "do {változó <- kifejezés; ...}" ugyanaz mint "kifejezés >>= \változó -> do { ... }"
 (3) vagy általánosabban "do {minta <- kifejezés; ...}" ugyanaz, mint "kifejezés >>= \tmp -> case tmp of {minta -> do { ... }; _ -> fail <hibaüzenet>}", ahol "tmp" egy változó, ami egyébként nem szerepel a programban, <hibaüzenet> pedig egy automatikusan generált hibaüzenet a hiba helyéről stb.
 (4) "do {let {deklarációk}; ...}" ugyanaz mint "let {deklarációk} in do { ... }"

25. Fordítás, futtatás
======================

A következő módon lehet elérni, hogy ez a Haskell program lefusson:

 (0) Installáljuk a Haskell Platform elnevezésű programcsomagot, ami tartalmazza a GHC (Glasgow Haskell Compiler) fordítót és néhány gyakran használt, hasznos függvénykönyvtárat (letölthető a www.haskell.org weboldalról). Windows alatt tudtommal nem nehéz installálni, bár régóta nem próbáltam; Linux alatt kicsit trükkösebb, mert a GHC fordító Haskell-ben van írva és önmaga kell a forráskódból lefordításához, de a letöltés link mellett le van írva, hogy hogy kell csinálni -- ha valakinek nem megy, szívesen segítek.
 (1) Megírjuk a forráskódját és elmentjük mondjuk hello.hs néven (a "hello" rész mindegy, a ".hs" a haskell forráskódfájlok hagyományos kiterjesztése, a fordító szereti, ha így nevezzük a forráskódfájlokat, egyébként valami parancssori kapcsolóval lehet jelezni, hogy a pl. "baz.hs.old" fájlt is Haskell forráskódfájlnak kell tekinteni).
 (2a) Lefordítjuk  a kódot a GHC fordítóval: kiadjuk a "ghc --make hello.hs" parancsot parancssorban. (Megjegyzések: Windows alatt lehet, hogy ghc.exe-t kell írni és lehet, hogy a ghc.exe teljes elérési útvonalát, mivel nem biztos, hogy az a futtatható programok keresési útvonalára (PATH) eső mappába installálódik (korrekció: valószínűleg ezen problémák egyike sem fog fellépni); persze a hello.hs fájlnak az aktuális könyvtárban kell lennie, vagy meg kell adni az elérési útvonalát ("foo/bar/hello.hs")).
 (3a) Futtatjuk a létrejött "hello" (vagy  Windows alatt "hello.exe") nevű futtatható programot.

 (2b) Kiadjuk a "runghc hello.hs" parancsot, ekkor a kód csak interpretálva lesz (mint szkriptnyelveknél) és úgy hajtódik végre, nem keletkezik futtatató.

A GHC-nek számtalan parancssori paramétere van, ezek használatát a dokumentációjában lehet megnézni (Linux alatt ez alapból valami olyasmi helyre installálódik, hogy "/usr/local/share/doc/ghc/html/index.html"; Windows alatt talán elérhető a Start menüből)

26. GHCI
=========

Hasznos eszköz még a ghci nevű segédprogram ("ghc --interactive" néven is elérhető). Ezt a "ghci" paranccsal lehet elindítani (Windows alatt van egy nem terminálban futó variánsa is WinGHCI néven, mert Windows alatt kényelmetlen a terminál); ezután Haskell kódsorokat kér be, kb. olyan formátumban mintha azok "main = do" után lennének írva és rögtön végre is hajtja őket. Egy példa a futására:

nagdon@nagdon-Compaq-615:~$ ghci
GHCi, version 7.0.3: http://www.haskell.org/ghc/  :? for help
Loading package ghc-prim ... linking ... done.
Loading package integer-gmp ... linking ... done.
Loading package base ... linking ... done.
Loading package ffi-1.0 ... linking ... done.
Prelude> let a = 2; b = 3
Prelude> print $ a + b
5
Prelude> c <- getLine
foobar
Prelude> print $ reverse c
"raboof"
Prelude> :?
 Commands available from the prompt:
[...szép hosszú help szöveg kihagyva...]

(megjegyzés : a "Prelude>" prompt utáni részeket és a "foobar" szöveget én írtam, be a többi a program válasza). Számos hasznos parancsa van a ghci-nek, ezek a ":?"-hez hasonlóan kettősponttal kezdődnek és a ":?" által kidobott szövegben vannak leírva.

A ghci-nek bem lehet adni parancssori paraméterként egy Haskell forráskódfájlt és akkor az abból exportált változók/függvények elérhetőek/tesztelhetőek lesznek. Windows alatt tudtommal a .hs fájlokat a Haskell Platform installer automatikusan társítja a GHCI-vel.

Az idáig elmondott részével a nyelvnek már gyakorlatilag mindent meg lehet valósítani, a továbbiakban megemlítek néhány nem nélkülözhetetlen, de igen hasznos eszközt.

27. A Read típusosztály
========================

Ha a beolvasott értékeket értelmezni akarjuk, hasznos a Read (alapból importálódó) típusosztály, amiknek a legtöbb  beépített típus példánya. Nem éri meg kézzel példányosítani, mert összetettek a metódusai (amiket nem mondok el teljesen részletesen, mert nem fontosak), deriving utasítással viszont használhatjuk. Két fontos függvény kapcsolódik hozzá:

read :: (Read a) => String -> a

A read függvény megpróbál értelmezni egy értéket épp szükséges típusúként, és futásidejű hibát okoz, ha nem értelmezhetó az adott dolog, úgyhogy csak akkor szokás használt, ha pl. egy rövid szript számokat kér be az inputról.

reads :: (Read a) => String -> [(a, String)]

A reads függvény kicsit óvatosabban próbél meg értelmezni egy sztringet megfelelő típusúként, egy listát ad vissza "(lehetséges értelmezés, sztring kimaradó része)" párokból. Például
(reads "3267 orcs" :: [(Int, String)]) == [(3267, " orcs")]

28. Modulok
============

Mont szinte minden programnyelvben, Haskell-ben is lehetséges több fájlra tördelni a nagyobb programokat. Az egyes fájlokat a "module" és "import" deklarációkkal kapcsolhatjuk össze.

Minden Haskell program egy module deklarációval kell hogy kezdődjön, aminek az alakja
module Modulnév(exportált dolgok listája) where
és ha mégsem azzal kezdődik akkor odaértendő a kód legelejére, hogy
module Main where

A where kulcsszó után egy layout-szabály szerinit blokk következik, és ebben van a program egész kódja, azaz a globális (változó, típusmegadás, típus, típusosztály, stb.) deklarációk -- a globális deklarációkat elválasztó pontosvesszők így a layout szabály speciális eseteiként adódnak ki.
Foo.Bar.Baz11
A modulnév egy vagy több nagybetűvel kezdődő alfanumerikus karakterekből álló szóból áll, amiket pont (".") karakterek választanak el (ha jól tudom itt nem lehet whitespace a pontok köröl). A Main modul speciális olyan szempontból, hogy az ebben lévő "main" IO számítás fog végrehajtódni (más modulban nem speciális a main változónév).

Az exportált dolgok listájában lehetnek adattípusok mögöttük zárójelben az exportálandó konstruktoraikkal (pl. Maybe(Nothing, Just), MyEnum(..), AbstractType() ), típusosztályok zárójelben az exportálandó metódusaikkal (pl. Ord(..), MyClass(method1, method2)) ), globális változók (pl. myFunc, (++)), és egész importált modulok "module" kulcsszóval megjelölve (pl. module PrimitiveOps, module Foo.Bar.Baz); ez egyes exportálandó dolgokat vesszők választják el. A típusosztálypéldányosítások anonimak és automatikusan mindig exportálódnak.

Az import deklaráció általában használt alakjai
import Modulnév
import Modulnév(importálandó dolgok listája)
import Modulnév hiding(nem importálandó dolgok listája)
Az import deklarációknak a kód elején kell lenniük, a "module ... where" kezdés és egy import deklaráció között csak más import deklarációk lehetnek. Az importálandó dolgoknak természetesen a modul által exportált dolgok közül kell kikerülniük; ha a hiding változatot használjuk, minden exportált és nem tiltólistás dolgot importálunk. Az importált dologok alapból minden speciális pluszjelölés nélkül használhatóak a kódban.

A Prelude modul automatikusan importálódik az "import Prelude" deklarációval, hacsak nem importáljuk explicit egy másmilyen importtal (pl. "import Prelude hiding(succ)" ha valami mást akarunk "succ"-nak nevezni, mint az Enum típusosztály viszonylag ritkán használt metódusát). Ebben vannak az eddig alapból importálódóként/elérhetőként emlegetett dolgok definiálva.

Ha több modulból importáltunk ugyanolyan nevű objektumokat (de nem ugyanazt az objektumot!) akkor az alapból nem hiba, de nem hivatkozhatunk egyszerűen a közös néven egyikre sem, hanem a modulnévvel kell egyértelműsíteni a rájuk való hivatkozást, pl.

import MyModule1.Baz(someConstant, (<@>))
import MyModule2(someConstant, (<@>))

main = print $ MyModule1.Baz.someConstant MyModule2.<@> MyModule2.someConstant

Fontos, hogy itt a "modulnév.objektum" jelölésben a pont mellet nem lehet whitespace karakter; viszont ha modulnévnek nézhető (azaz nagybetűvel kezdődő) dologra és még valamire akarjuk alkalmazni a pont (függvény egymás után elvégzés) operátort, kell whitespace a pont mellett, tehát "Just.sqrt $ x" a Just modulbeli sqrt függvény értéke az x helyen, míg "Just . sqrt $ x" előbb az sqrt, majd a Just függvényt alkalmazza x-re.

Ezt az egyértelműsítésre kitalált jelölést akkor is használhatjuk, ha csak egyetlen modulból van importálva az adott dolog, bár ekkor felesleges, így ritkán szoktuk.

Ha hosszú a modul neve, egy alternatív modulnevet is létrehozhatunk, ami helyettesítheti az eredeti hosszú nevet (ami szintén használható marad):

import Data.ByteString.Lazy as L
main = ..blablabla.. L.map
       ..blablabla.. Data.ByteString.Lazy.map
       ..blablabla.. Prelude.map
       ..blablabla..

Ha két modul között névütközés van, a "qualified" kitétellel megtehetjük, hogy az egyikre előírjuk, hogy az onnan származó dolgokra mindig meg kelljen jelölni, hogy onnan származnak és ekkor a másikból származó dolgokat pluszjelölés nélkül használhatjuk. Ezt gyakran használjuk, ha egy modul Prelude-beli nevekkel ütközik. Például az előző példa módosítva:

import qualified Data.ByteString.Lazy as L
main = ..blablabla.. L.map
       ..blablabla.. Data.ByteString.Lazy.map
       ..blablabla.. map
       ..blablabla..

Ha egy több modulból álló programot fordítunk, akkor a "ghc --make"-nek a Main modult tartalmazó fájl nevét adjuk meg, és a többi modult vagy csomagokból szedi elő (a Haskell Platform-ban lévő modulokat így szedi elő, nem fontos tudni, hogy pontosan hogyan működik), vagy a modulnév által meghatározott fájlnévben keresi:
a "Foo.Bar.Baz" modult a "/path/to/main/Foo/Bar/Baz.hs" forráskódfájlban fogja keresni, ahol "/path/to/main" az a mappa, amiben a Main modult tartalmazó fájl van. Így a modulnév meghatározza a fájlnevet a Main modul kivételével mindig (lehet, hogy ezt felül lehet írni, de ez nem szokott kelleni).

Nagyon hasznos megnézni a Haskell Platform-hoz járó modulok dokumentációját, rengeteg hasznos függvény van bennük, amiknek a megírását így meg lehet spórolni. (Szerintem) különösen a Prelude, Data.List, Data.Map, Data.Maybe, Control.Monad, System.Random modulok nagyon hasznosak általában mindenhez. A Text.ParserCombinator.ReadP modul egy nagyon erős, de könnyen kezelhető eszköz szövegfeldolgozáshoz (olyasmi, mint a reguláris kifejezések, csak bonyolult dolgokat is le lehet bene írni nagy hekkelés nélkül és olvashatóbb). Ez a modul egy monádtípust és az azon való műveleteket definiálja, a monádok megértésében is hasznos lehet, mint nemtriviális példa.

29. Egy kis speciális szintaxis listákhoz
==========================================

A nyelvbe be van építve listák megadásának egy a matematikában szokásos halmazmegadáshoz hasonló módja. Ezt legegyszerűbb pár példával bemutatni:

ls :: [Int]
ls = [ show n | x <- [0..9], y <- [0..9], z <- [0..9], let n = 100*x+10*y+z, n/=0, n `mod` (x+y+z) == 0 ]

prod :: [a] -> [b] -> [(a, b)]
prod xs ys = [(x, y) | x <- xs, y <- ys]

A szerkezet alakja [ kifejezés | vesszővel elválasztott dolgok ], a dolgok itt lehetnek listákon végigfutó változók kijelölései, let-tel lokális változók definiálásai, és logikai feltételek. A szerkezet a következő módon fordítható a nyelv többi részére:

 (1)  "[ kifejezés | let változó = érték, ...]" ugyanaz, mint "let változó = érték in [ kifejezés | ...]"
 (1') "[ kifejezés | let változó = érték ]" ugyanaz, mint "let változó = érték in [ kifejezés ]"
 (2)  "[ kifejezés | minta <- lista, ...]" ugyanaz, mint "do { minta <- lista; [kifejezés | ...] }"
 (2') "[ kifejezés | minta <- lista ]" ugyanaz, mint "do { minta <- lista; [ kifejezés ] }"
 (3)  "[ kifejezés | feltétel, ...]" ugyanaz, mint "if feltétel then [kifejezés | ... ] else []"
 (3') "[ kifejezés | feltétel ]" ugyanaz, mint "if feltétel then [ kifejezés ] else []"

30. Megjegyzések
=================

Megjegyzéseket kétféleképpen írhatunk:
 * ha legalább két kötőjel mellett nem áll más "speciális" (nem whitespace nem alfanumerikus nem zárójel) karakter, akkor onnantól a sor végéig megjegyzés van. Tehát
foo = bar----- blah
megjegyzést kezd "bar" után, de
--* foobar
nem megjegyzés.
 * "{-" és "-}" (idézőjelek nélkül) között megjegyzés van, az ilyen megjegyzés tarthat több soron keresztül, és lehet benne beágyazott {-  -} pár, azaz "{- blah  {- foobar -} baz -}" végig megjegyzés, az első -} nem zárja le a megjegyzést.

 * "{-#" és "#-}" között pragmákat adhatunk meg, ezek közül amit nem árt tudni, az a {-# LANGUAGE nyelvi kiterjesztések neve vesszővel elválasztva #-} és a {-# OPTIONS_GHC extra parancssori paraméterek a GHC fordítónak #-}, például "{-# LANGUAGE ForeignFunctionInterface #-}" lehetővé teszi a C kóddal való kommunikációt biztosító "foreign import" és "foreign export" deklarációk használatát; "{-# OPTIONS_GHC -Wall -O2 #-}" bekapcsol szinte minden "gyanús" dologra vonatkozó figyelmeztetést és erős optimalizációt kér. A GHC dokumentációjában le vannak írva a nyelvi kiterjesztések hatásai, ezek sokszor tudnak nagyon hasznosak lenni.

31. Néhány apróság típusokhoz
==============================

Új típusok létrehozásának van egy további módja, ez a newtype deklaráció, ami abban különbözik a data deklarációtól, hogy "data" helyett azt kell írni, hogy "newtype", és a létrehozott típusnak csak egy konstruktora lehet és annak pontosan egy paraméterének kell lennie, tehát mindig
newtype Típusnév típusparaméterek = Konstruktornév <egy típuskifejezés, ami függhet a paraméterektől>
alakú. Ez után is lehet deriving-ot használni. A newtype deklarácót arra használjuk, hogy egy új típust hozzunk létre, aminek a fizikai reprezentációja megegyezik egy másik típussal, de pl. más típusosztályoknak példánya/máshogy vannak definiálva a metódusai.

A data és a newtype deklarációkat lehet bonyolítani a rekord szintaxissal, aminek a használata sajnos sokszor nem igazén kényelmes, de az egyetlen, sok paraméterrel rendelkező konstruktorral rendelkező típusok használatát gyakran áttekinthetőbbé teszi.
Például

data Person = Person {
    name :: String,
    age :: Int,
    adress :: Maybe String
} deriving (Eq, Show)

person1, person2, anon :: Person
person1 = Person {
            name = "John Doe",
            adress = Just "High street 33, Orkville",
            age = 26
         }
person2 = Person "John Doe" (Just "High street 33, Orkville") 26
anon = Person {name = "Anonymous", adress = Nothing }

afterBirthday :: Person -> Person
afterBirthday p = p {age = age p + 1}

main = do
    print $ person1 == person2  -- True
    print $ person1 == anon     -- False
    print $ adress person1      -- Just "High street 33, Orkville"
    print $ afterBirthday person2
-- Person {name = "John Doe", age =27, adress = Just "High street 33, Orkville"}
    print $ adress anon         -- Nothing
    print $ age anon            -- ***Exception: Prelude.undefined
                                -- futás idejű hiba

A pontos részletek megtalálhatóak a dokumentációban.

32. Egyéb dolgok
=================
A monomorfizmus korlátozás -- ha egy változót nem függvényként definiálunk, annak csak akkor szerepelhet típusosztály a típusában, ha explicit megadjuk a típusát. Ezt a fordítás számításigényének csökkentésére vezették be, ma már a nehezen felderíthető programhibákon kívül alig van haszna. Ki lehet kapcsolni, ha a kód legelején, még a module kulcsszó előtt elhelyezzük a "{-# LANGUAGE NoMonomorphismRestriction #-}" pragmát.

A default kulcsszó (ha jól tudom) ennek a korlátozásnak az enyhítésére lett bevezetve, nem tudom pontosan mit csinál, mert nem nagyon kellett még használnom.

A C-vel való kommunikáció meglepően egyszerűen megvalósítható, ha szükséges, megéri utánanézni.

Nem terminálból futó programok írásához szerintem a wxHaskell függvénykönyvtárnak kell utánanézni, de jópár más hasonló függvénykönyvtár van.

Vannak eszközök, amik "hivatalosan" nem léteznek -- ilyen a System.IO.Unsafe modulban található "unsafePerformIO :: IO a -> a" függvény és az Unsafe.Coerce modulban található "unsafeCoerce :: a -> b" függvény, ezeknek a fő alkalmazási területe az, hogy a programozó lábon lője magát velük.