Funkcionális nyelvek (egyetemi jegyzet) Páli Gábor János

Hasonló dokumentumok
2019, Funkcionális programozás. 2. el adás. MÁRTON Gyöngyvér

FUNKCIONÁLIS PROGRAMOZÁS ELŐADÁS JEGYZET

FUNKCIONÁLIS PROGRAMOZÁS GYAKORLAT JEGYZET

Alapok. tisztán funkcionális nyelv, minden függvény (a konstansok is) nincsenek hagyományos változók, az első értékadás után nem módosíthatók

2016, Funkcionális programozás

Funkcionális és logikai programozás. { Márton Gyöngyvér, 2012} { Sapientia, Erdélyi Magyar Tudományegyetem }

BASH script programozás II. Vezérlési szerkezetek

2018, Funkcionális programozás

Kifejezések. Kozsik Tamás. December 11, 2016

1. Alapok. #!/bin/bash

Kifejezések. Kozsik Tamás. December 11, 2016

FUNKCIONÁLIS PROGRAMOZÁS

Amortizációs költségelemzés

2019, Funkcionális programozás. 5. el adás. MÁRTON Gyöngyvér

AWK programozás, minták, vezérlési szerkezetek

A függvény kód szekvenciáját kapcsos zárójelek közt definiáljuk, a { } -ek közti részt a Bash héj kód blokknak (code block) nevezi.

Vezérlési szerkezetek

Rekurzió. Dr. Iványi Péter

C++ referencia. Izsó Tamás február 17. A C++ nyelvben nagyon sok félreértés van a referenciával kapcsolatban. A Legyakoribb hibák:

First Prev Next Last Go Back Full Screen Close Quit. Matematika I

2019, Funkcionális programozás. 4. el adás. MÁRTON Gyöngyvér

Már megismert fogalmak áttekintése

Occam 1. Készítette: Szabó Éva

Programozási nyelvek (ADA)

2018, Funkcionális programozás

Webprogramozás szakkör

Programozás alapjai gyakorlat. 2. gyakorlat C alapok

Java II. I A Java programozási nyelv alapelemei

Programozás C és C++ -ban

1. Egyszerű (primitív) típusok. 2. Referencia típusok

SZÁMÍTÁSOK A TÁBLÁZATBAN

6. Függvények. 1. Az alábbi függvények közül melyik szigorúan monoton növekvő a 0;1 intervallumban?

2010. október 12. Dr. Vincze Szilvia

KOVÁCS BÉLA, MATEMATIKA I.

Komputeralgebra rendszerek

AWK programozás Bevezetés

Brósch Zoltán (Debreceni Egyetem Kossuth Lajos Gyakorló Gimnáziuma) Számelmélet I.

Komputeralgebra rendszerek

Programozás BMEKOKAA146. Dr. Bécsi Tamás 2. előadás

Java programozási nyelv

2018, Funkcionális programozás

KOVÁCS BÉLA, MATEMATIKA I.

ALGORITMIKUS SZERKEZETEK ELÁGAZÁSOK, CIKLUSOK, FÜGGVÉNYEK

5. SOR. Üres: S Sorba: S E S Sorból: S S E Első: S E

Oktatási segédlet 2014

KOVÁCS BÉLA, MATEMATIKA I.

Funkcionális Nyelvek 2 (MSc)

Szkriptnyelvek. 1. UNIX shell

Formális nyelvek és automaták

f(x) vagy f(x) a (x x 0 )-t használjuk. lim melyekre Mivel itt ɛ > 0 tetszőlegesen kicsi, így a a = 0, a = a, ami ellentmondás, bizonyítva

Alkalmazott modul: Programozás 4. előadás. Procedurális programozás: iteratív és rekurzív alprogramok. Alprogramok. Alprogramok.

Tömbök kezelése. Példa: Vonalkód ellenőrzőjegyének kiszámítása

HORVÁTH ZSÓFIA 1. Beadandó feladat (HOZSAAI.ELTE) ápr 7. 8-as csoport

Interfészek. PPT 2007/2008 tavasz.

Programozás alapjai gyakorlat. 4. gyakorlat Konstansok, tömbök, stringek

Programozás alapjai (ANSI C)

OOP. Alapelvek Elek Tibor

Készítette: Nagy Tibor István

GENERIKUS PROGRAMOZÁS Osztálysablonok, Általános felépítésű függvények, Függvénynevek túlterhelése és. Függvénysablonok

2018, Diszkrét matematika

S2-01 Funkcionális nyelvek alapfogalmai

Adatszerkezetek Tömb, sor, verem. Dr. Iványi Péter

Általános algoritmustervezési módszerek

AWK programozás, minták, vezérlési szerkezetek

PHP. Telepítése: Indítás/újraindítás/leállítás: Beállítások: A PHP nyelv

Segédanyagok. Formális nyelvek a gyakorlatban. Szintaktikai helyesség. Fordítóprogramok. Formális nyelvek, 1. gyakorlat

Kiterjesztések sek szemantikája

Változók. Mennyiség, érték (v. objektum) szimbolikus jelölése, jelentése Tulajdonságai (attribútumai):

Mindent olyan egyszerűvé kell tenni, amennyire csak lehet, de nem egyszerűbbé. (Albert Einstein) Halmazok 1

Halmazelmélet. 1. előadás. Farkas István. DE ATC Gazdaságelemzési és Statisztikai Tanszék. Halmazelmélet p. 1/1

sallang avagy Fordítótervezés dióhéjban Sallai Gyula

Operációs rendszerek. 9. gyakorlat. Reguláris kifejezések - alapok, BASH UNIVERSITAS SCIENTIARUM SZEGEDIENSIS UNIVERSITY OF SZEGED

Programozás II. 2. gyakorlat Áttérés C-ről C++-ra

Python tanfolyam Python bevezető I. rész

A szemantikus elemzés elmélete. Szemantikus elemzés (attribútum fordítási grammatikák) A szemantikus elemzés elmélete. A szemantikus elemzés elmélete

Amit a törtekről tudni kell Minimum követelményszint

Információk. Ismétlés II. Ismétlés. Ismétlés III. A PROGRAMOZÁS ALAPJAI 2. Készítette: Vénné Meskó Katalin. Algoritmus. Algoritmus ábrázolása

9. előadás. Programozás-elmélet. Programozási tételek Elemi prog. Sorozatszámítás Eldöntés Kiválasztás Lin. keresés Megszámolás Maximum.

Mindent olyan egyszerűvé kell tenni, amennyire csak lehet, de nem egyszerűbbé.

A fordítóprogramok szerkezete. Kódoptimalizálás. A kódoptimalizálás célja. A szintézis menete valójában. Kódoptimalizálási lépések osztályozása

van neve lehetnek bemeneti paraméterei (argumentumai) lehet visszatérési értéke a függvényt úgy használjuk, hogy meghívjuk

I. Egyenlet fogalma, algebrai megoldása

Leképezések. Leképezések tulajdonságai. Számosságok.

A szemantikus elemzés helye. A szemantikus elemzés feladatai. A szemantikus elemzés feladatai. Deklarációk és láthatósági szabályok

Bevezetés a programozásba

Szoftvertervezés és -fejlesztés I.

Excel 2010 függvények

NAGYPONTOSSÁGÚ RACIONÁLIS-ARITMETIKA EXCEL VISUAL BASIC KÖRNYEZETBEN TARTALOM

az Excel for Windows programban

Komputeralgebra Rendszerek

Bevezetés a programozásba II. 5. Előadás: Másoló konstruktor, túlterhelés, operátorok

Programozási segédlet

A PiFast program használata. Nagy Lajos

Lekérdezések az SQL SELECT utasítással

BASH SCRIPT SHELL JEGYZETEK

Programozás alapjai. 7. előadás

Logika es sz am ıt aselm elet I. r esz Logika 1/36

C programozási nyelv

Aritmetikai kifejezések lengyelformára hozása

Algoritmusok és adatszerkezetek 2.

Átírás:

Funkcionális nyelvek (egyetemi jegyzet) Páli Gábor János <pgj@elte.hu> Budapest, 2016

Tartalomjegyzék 1. Rövid áttekintés 1 1.1. Bevezetés............................. 1 1.2. Elágazások függvényekben.................... 4 1.3. Programozás listákkal....................... 7 1.4. A rekurzió............................. 9 1.5. Rekurzió listák felett: hajtogatás................ 14 1.6. Függvények függvényei...................... 16 1.7. Részlegesen alkalmazott függvények............... 21 1.8. Típusok.............................. 22 1.9. λ-függvények........................... 26 1.10. Függvények kompozíciója.................... 30 1.11. Listák mint halmazok....................... 33 1.12. Lokális definíciók......................... 36 1.13. Interakció a külvilággal...................... 38 1.14. Összefoglalás........................... 43 2. Típusok 44 2.1. Bevezetés............................. 44 2.2. Nevesített típusok......................... 45 2.3. Származtatott típusok...................... 46 2.4. Algebrai adattípusok....................... 50 2.4.1. Típusok összeadása.................... 50 2.4.2. Típusok szorzása..................... 51 2.4.3. Rekurzív típusok..................... 52 2.4.4. Műveletek algebrai típusok elemeivel.......... 54 2.4.5. Struktúraszerű használat................. 54 2.4.6. Példák........................... 56 2.5. Üres értékek............................ 58 2.6. Fajták............................... 58 2.7. Típusosztályok.......................... 59 2.7.1. Típusosztályok mint szótárak.............. 60 i

2.7.2. Osztályok definíciója................... 61 2.7.3. Példányok definíciója................... 64 2.7.4. Többértelmű nevek.................... 65 2.7.5. Átfedő példányok..................... 66 2.7.6. Típuskonstruktor-osztályok............... 68 2.8. Összefoglalás........................... 69 A. Magyar-angol szótár 70 B. A Glasgow Haskell Compiler 73 B.1. Haskell Platform......................... 73 B.2. Nyelvi kiterjesztések....................... 74 B.2.1. A kiterjesztések engedélyezése és tiltása......... 74 B.2.2. A jegyezetben alkalmazott kiterjesztések........ 75 ii

1. fejezet Rövid áttekintés Ebben a fejezetben szeretnénk egy rövid áttekintést adni a tisztán funkcionális programozás alapvető fogalmairól. A téma mélysége és a könnyű olvashatóság fenntartása miatt itt most szándékosan nem törekedtünk a teljes részletességre. Ezért a most következő összefoglalás egyaránt alkalmas lehet arra, hogy adjon egy átfogó, vázlatos képet a témával elsőként találkozóknak, illetve mindazok számára, akik korábban már foglalkoztak vele, de szeretnék ismereteiket újra feleleveníteni. A későbbi fejezetekben az itt említett fogalmakat, megoldásokat újra elő fogjuk venni és majd kimerítőbben tanulmányozzuk. 1.1. Bevezetés A funkcionális programozás a deklaratív programozási paradigma egyik ága. Ekkor programjainkat mint függvény- és típusdefiníciók vegyes sorozatát adjuk meg. Ezek a definíciók aztán dominók sorozatához hasonlóan gyakran egy hosszú láncot alkotnak, melyet az egyik végéről meglökve a programunk végül lefut, ahogy az egymásra támaszkodó elemek eldőltik egymást. Ez a folyamat egészen addig tart, amíg akad az, olykor sokfelé ágazó, láncban ledönthető dominó. A funkcionális nyelvi programok esetében a lánc eldőlthető végét start kifejezésnek nevezzük, ez lesz lényegében a főprogramunk. A dominók eldőlése a definíciók, legtöbbször függvények értékének meghatározását, kiértékelését jelenti, és amikor egy összes ledönthető dominó végül eldőlt, megkapjuk a programunk végeredményét. Azonban érdemes megjegyezni, hogy a funkcionális programok számos tekintetben eltérnek a dominók sorozatától, gyakran képesek a valós világban lehetetlen trükköket végrehajtani. Például bizonyos elemeknek neveket adhatunk, amelyekre később minden olyan helyről tetszőleges mennyiségben 1

hivatkozhatunk, ahova beilleszthetőek anélkül, hogy több példányt gyártanánk belőlük. Vagy készíthetünk olyan sorozatot, ahol a dominók egy része újra feláll, és ezzel ismét eldönthetővé válnak. Ezáltal kapunk egy olyan sorozatot, ahol a dominók sosem fognak véglegesen eldőlni. Ez utóbbiak felelnek meg a végtelen sokáig futó programoknak. Másrészt a funkcionális programokban, ellentétben a népszerűbb, ún. imperatív programozási nyelvi programokkal, a szerző nem azt írja le, hogy az egyes részek pontosan mikor és milyen sorrendben dőljenek el. Helyette mindössze csak szabályokat ad meg erre vonatkozóan, de a tényleges folyamatot már nem irányítja közvetlenül. Ezért nevezzük tehát ezt a programozási stílust deklaratív, vagy leíró jellegű programozásnak. A deklaratív gondolkodási mód a fentiek révén nagy szabadságot ad a programozónak, amelyek segíti abban, hogy a megoldandó problémákat egy viszonylag absztrakt és matematikai oldaláról fogalmazza meg, egyfajta képletek, összefüggések alkalmazásával. Ezek így gyakran hozzájárulnak ahhoz, hogy a megoldásban kialakuljon egy rendszer, és hogy a fejlesztés során ne vesszünk el olyan könnyen a részletekben. Maga a képletszerű megfogalmazás egyébként rögvest függvénydefiníciók formájában jelenik meg, például így: negate x = 0 - x Ez a függvény egy szám előjelét fordítja meg úgy, hogy kivonja nullából. A neve annak angol nyelvű elnevezéséből fakadóan most negate, valamint egyetlen paramétere van, ez az x. A függvény nevét és paraméterét annak definíciójától, vagyis törzsétől egy = (egyenlőségjel) választja el, így ezzel lényegében egy egyenletet kapunk. Ezeket az egyenleteket fogjuk arra felhasználni, hogy eljussunk a programunk végeredményéhez. Ennek során tulajdonképpen nem kell mást csinálnunk, mint helyettesítenünk az egyenlet bal oldalán szereplő programrészletet annak jobb oldalán szereplő megfelelőjével úgy, hogy közben értéket adunk a paramétereknek. Ekkor azt is állíthatjuk, hogy a függvényünk eredménye csak és kizárólag annak bemenetétől függ, ilyen függvényeket szokhattunk meg a matematikában is. Ezeket tiszta függvényeknek nevezzük, ezek következetes alkalmazásából származik a funkcionális programozás egyik ága, a tisztán funkcionális programozás. Az iménti függvénydefiníciót úgy tudjuk felhasználni, ha elhelyezzük egy szöveges állományba, amelyet most nevezzük úgy, hogy Tutorial.hs. A.hs kiterjesztés utal arra, hogy ez egy Haskell nyelvű forrásprogram. Ezt például a Glasgow Haskell Compiler (GHC) interaktív felületén keresztül tudjuk megszólítani, ha betöltjük értelmezésre a ghci nevű programba. Windows 2

rendszerek esetében ehhez elegendő csak duplán kattintani az állomány nevére. Így a következő eredményt kell kapnunk: $ ghci Tutorial.hs GHCi, version 7.10.3: http://www.haskell.org/ghc/ :? for help [1 of 1] Compiling Main ( Tutorial.hs, interpreted ) Ok, modules loaded: Main. A GHC telepítéséről a B. függelékben olvashatunk. Miután az állomány sikeresen betöltésre került, az értelmező lehetővé teszi számunkra, hogy egyenként hivatkozzunk az általunk definiált függvényekre, vagy akár kombináljuk ezeket. Az egész működését úgy kell elképzelünk, mint egy programozható számológépet, ahol kifejezéseket tudunk kiértékelni, és ehhez újabb és újabb függvényeket készíthetünk. A program a kiértékelendő kifejezéseket a sikeres betöltést követően egy Main*> kezdetű sorban, egy promptban várja. Itt maga a Main annak a függvénytárnak, modulnak a neve, amely a saját definícióinkat tartalmazza. Mivel nem adtunk neki külön nevet a forráskódban, az értelmező alapértelmezés szerint így nevezi el, függetlenül az állomány nevétől. A továbbiakban az értelmező promptját egyszerűen csak úgy jelöljük, hogy GHCi>. GHCi> negate 1-1 GHCi> negate (negate 1) 1 GHCi> negate -1 <interactive>:3:1: Non type-variable argument in the constraint: Num (a -> a) (Use FlexibleContexts to permit this) When checking that it has the inferred type it :: forall a. (Num a, Num (a -> a)) => a -> a Ezt az értelmezővel folytatott párbeszédet Read-Eval-Print (mint beolvasni, kiértékelni, kiírni, röviden REP) ciklusnak hívják. Először az értelmező beolvassa és értelmezi a bemenetként kapott kifejezést, majd, amennyiben ez sikeres volt, kiértékeli, vagyis kiszámolja az eredményét, melyet végül kiír. Sajnos, ahogy a fenti példában látható, az utolsó kifejezés valamiért nem volt elfogadható. Ez arra mutat egy példát, amikor a fordító valamilyen oknál fogva nem képes megérteni a beolvasott szöveget, miközben az a nyelv helyesírási, másnéven szintaktikai szabályainak látszólag teljesen megfelel. A fordító ugyanis nem egyszerűen csak ezen szabályok szerint ellenőrzi a bemenetet, hanem elvégez egy ún. típusellenőrzési lépést is, amely tulajdonképpen 3

a kifejezés nyelvhelyességi, azaz szemantikai vizsgálatára utal. A konkrét hiba részleteit itt most nem fejtjük ki, röviden csak annyit jegyzünk meg, hogy mindez azért számít Haskellben szemantikai hibának, mert a - (mínusz) szimbólumot egyszerre lehet a kivonás bináris, és az előjelváltás unáris operátorának tekinteni. Ezért ez a megfogalmazás kisebb fejtörést tud okozni a fordító számára, így mint ökölszabály, mindig javasolt a programokban a negatív számokat külön zárójelbe tenni: GHCi> negate (-1) 1 Az értelmező használata során arra mindig ügyelnünk kell, hogy minden esetben, amikor a programot megváltoztatjuk, azt újra be kell töltenünk. Ezt az értelmezőn belül egy paranccsal tudjuk elvégezni, ennek a neve :reload. Figyeljük meg, hogy a parancs a : (kettőspont) szimbólummal kezdődik, ezzel tudja az értelmező megkülönböztetni a feldolgozandó kifejezéseket a parancsoktól. GHCi> :reload [1 of 1] Compiling Main ( Tutorial.hs, interpreted ) Ok, modules loaded: Main. A parancs rövidítéseként azt is írhatjuk, hogy :r, illetve maga a : (üres parancs) alkalmas arra, hogy az utolsóként kiadott parancsot megismételtessük. Érdemes tudni, hogy a :? segítségével az összes elérhető parancsot mindig lekérdezhetjük. 1.2. Elágazások függvényekben A függvénydefiníciók sokszor azonban nem ennyire egyszerűek, mivel alkalmanként el is kell ágazniuk. Például tegyük fel, hogy készíteni szeretnénk egy olyan függvényt, amelyik egy szám abszolút értékét képes kiszámítani. Ebben az esetben a szám értékétől függően, amennyiben az negatív, előjelet váltunk vagy változatlanul hagyjuk. Ez utóbbi kapcsolatban megjegyezzük, hogy a tisztán funkcionális programok esetében az értékek nem változhatnak meg a program futása során. Vagyis egy név mindig ugyanazt a hozzárendelt értéket hivatkozza, függetlenül attól, hogy a programban merre található. Ezt hivatkozási helyfüggetlenségnek nevezzük. Továbbá ehhez kapcsolódóan azt mondjuk, hogy a programbeli változóink egyszer kapnak csak értéket. Ennek megfelelően tehát a függvényünk majd nem fogja módosítani a kapott paraméter értékét, hanem helyette egy új, de már módosított értéket fog visszaadni. 4

Haskell programokban elágazásokat többek közt ún. őrfeltételek hozzáadásával képezhetünk. Az őrfeltételek olyan logikai értékű kifejezések, amelyeket a függvénydefiníció bal oldalán, a függvény nevét és a paraméterlistát követően helyezhetünk el a (függőleges vonal) szimbólummal elválasztva. Ennek szemléltetéséhez most vegyük az osztási művelet egy olyan speciális változatát, ahol a számítást magát csak abban az esetben végezzük el, amikor az osztó nem egyenlő nullával: x./. y y /= 0 = x / y Ekkor a függvény törzsét (az egyenlet jobb oldalát) csak az őrfeltétel teljesülésekor fogja csak a program futtatását végző rendszer kiértékelni. Minden más esetben az adott törzs feldolgozása kimarad és a futtató rendszer folytatja a következő, teljesíthető őrfeltétellel rendelkező ágat, miközben fentről lefelé halad. Amennyiben nem adtuk meg további lehetséges törzseket, ún. függvényalternatívákat, futási idejű hiba, kivétel keletkezik. Ezt az értelmező közvetlenül meg is jeleníti, mivel ilyen esetekben nem képes választ adni a korábban feltett kérdésünkre: GHCi> 42./. 0 *** Exception: Tutorial.hs:5:3-26: Non-exhaustive patterns in function./. Ezt a matematikából kiindulva parciális függvénynek nevezzük. Ezek olyan függvények, amelyek nem minden értékhez rendelnek másikat, bizonyos pontjaikban nem értelmezettek. Ahogy láthattuk, ezek alkalmazása viszont futási hibához vezet, amely egyúttal a teljes programunk kiértékelésének leállását jelenti. Ennek okán a gyakorlatban az ilyen függvények írása nem javasolt, a későbbiekben ennek elkerülésére látni is fogunk megoldásokat. A parciális függvény ellentéte a totális függvény, amely fogalmát szintén a matematikából kölcsönöztük, ahol minden értékhez rendelünk valamit. Most adjuk meg az abs, vagyis az abszolútérték-függvény definícióját őrfeltételek használatával! Vigyázzunk azonban a tördelésre! Ugyanis ez már egy többsoros definíció lesz, ahol a fordító a behúzások alapján képes megérteni, az egyes sorok miként kapcsolódnak egymáshoz. Ezt margószabálynak hívják, amelynek lényege, hogy a hosszabb definíciókat eltörhetjük és több sorban írhatjuk. Ilyenkor az első sor után következőket mindig bentebb kell húznunk szóközökkel, és ezáltal egy margót képzünk: abs x x < 0 = negate x otherwise = x 5

Ebben a definícióban tehát két eltérő ágat is felfedezhetünk. Az egyik fogalmazza meg a kivételes esetekre, vagyis a negatív számokra vonatkozó szabályt, és alkalmazza rájuk a korábban már definiált negate függvényünket. A másik pedig az összes többi esetet kezeli. Ez egyben egy olyan őrfeltételre is mutat példát, amely igazából nem akadályozza meg az adott törzs kiértékelését. Ebben ugyanis valójában az otherwise konstans szerepel, amely az azonosan igaz logikai értéket fedi: GHCi> otherwise True Az eddigiek alapján megfigyelhetjük, hogy az egyes alternatívák őrfeltételeit tehát a kiértékelés során fentről lefelé vizsgáljuk, és közülük végül azt a törzset választjuk, amely először értékelődik igazra. Vegyük észre, hogy ennek megfelelően, ha az otherwise feltétel szerepel először, akkor az minden más ágnál hamabb, tőlük lényegében függetlenül válik igazzá, így azok sosem fognak szóba kerülni. Ha viszont először a kivételeket soroljuk fel, képes az összes többi esetre választ adni, egyfajta védőhálóként megakadályozza, hogy a függvény véletlenül parciálissá váljon. Haskellben létezik továbbá az imperatív nyelvekben ismert if ( ha... akkor... utasítás) megfelelője is. Ez azonban, a nyelv és paradigma jellegéből fakadóan, nem utasítás, hanem függvény. A tisztán funkcionális nyelvekben ugyanis csak és kizárólag függvények vannak, nincsenek utasítások. Emiatt a programjaink sem lesznek többek, mint hatalmas kifejezések, amelynek egyszerűen csak ki akarjuk számítani az értékét. Másrészt ennélfogva már a legkisebb kifejezések is programnak számítanak, és az értelmező a kiértékeléssel ezeket a programokat futtatja. Ennek következményeképpen, bár az if kulcsszónak számít, teljesen egy háromparaméteres függvényként kell rá gondolnunk. Ezért mindig, minden paraméterét meg kell adnunk: az elágaztatás logikai feltételét, valamint az igaz és hamis értékeknek megfelelő kifejezések. GHCi> if True then 1 else 0 1 Ezen keresztül az abs függvény így lenne megadható az if használatával: abs x = if (x < 0) then (negate x) else x Azonban az ágak számának növekedésével az őrfeltételek sokkal jobb választásnak bizonyulnak, mivel így a programjaink rövidebbek maradnak. Ezért a funkcionális nyelvi programokban inkább ezeket szoktuk alkalmazni. 6

1.3. Programozás listákkal Az egész számok mellett Haskellben sok más típus is használható. Például, ahogy az otherwise esetében is láthattuk előzőleg, tudunk logikai értékekkel is dolgozni. Sőt, ahogy majd később (2. fejezet) láthatjuk, a nyelv tetszőleges típus definiálását lehetővé teszi. A funkcionális programokban másik gyakran előforduló típus a lista, amelynek történelme egészen az első funkcionális nyelvig, a LISP (mint "list processing") programozási nyelvig visszanyúlik. A listák értékek egyszerű tárolóiként használhatóak. A legegyszerűbb lista az üres lista. GHCi> [] [] De ennél bonyolultabb listákat is létre tudunk hozni az elemeik felsorolásával, vesszők között felsorolva: GHCi> [1,2,3,4,5] [1,2,3,4,5] Vagy akár magát az értelmezőt is megkérhetjük, hogy magától állítsa elő ugyanezt a listát. Ehhez mindössze a felsorolás alsó és felső korlátját kell megadnunk, köztük két ponttal: GHCi> [1..5] [1,2,3,4,5] Listákat nem csak így lehet építeni. Ezeket valójában az előbb bemutatott üres lista és egy ún. építő operátor segítségével lehet készíteni, amelynek jele a : (kettőspont). A : operátornak egy listaelemre és listára van szüksége, melyekből képes egy újabb listát készíteni. Ez egy jobbasszociatív művelet, vagyis ha többször egymásba ágyazva alkalmazzuk, akkor mindig a jobb oldali példányt kell először kiértékelni. GHCi> 1 : (2 : (3 : (4 : (5 : [])))) [1,2,3,4,5] Érdemes tudni, hogy az egyszerűség kedvéért a fordító mindig ebben a formában tárolja a listákat. Az imént bemutatott többi lehetőség csupán ennek egy-egy másik felírási módja, amely a könnyebb olvashatóságot hívatott elősegíteni. Ezt nyelvi ízesítésnek szoktuk nevezni, amely egy gyakran alkalmazott technika a funkcionális nyelvek definíciójában. 7

A listák eredeti ábrázolási módjával azért is fontos tisztában lennünk, mert így listákat feldolgozó függvényeket is lehetőségünk nyílik írni. Erre példaként tekintsünk egy olyan függvényt, amely az == operátor (egyenlőségvizsgálat) segítségével ellenőrzi, hogy egy adott lista üres vagy sem: isempty xs = xs == [] amelyet a következőképpen próbálhatunk ki az értelmezőben: GHCi> isempty [] True GHCi> isempty [1..5] False Megjegyezzük, hogy ilyen függvényeket lehetőségünk van egy másik eszközzel, ún. mintaillesztéssel megfogalmazni. A mintaillesztés során a függvény által kapott értéket annak szerkezete szerint különböző mintákkal vetjük össze és annak alapján választjuk meg a függvény eredményét. Ezen keresztül, az őrfeltételek használatához hasonlóan, függvényalternatívák sorozatát tudjuk felsorolni, melyek mindegyikéhez egy-egy minta fog tartozni. A függvény értékének kiszámítása során ezeket a mintákat fogjuk azok megadásának sorrendjében (fentről lefelé) megpróbálni illeszteni a függvény paramétereire, és a kiértékelést azzal a függvénytörzssel folytatjuk, ahol azok a szerkezeti szabályok szerint megfelelnek. Az összes többi törzs ekkor feldolgozatlan marad. Például annak eldöntése, hogy egy lista üres vagy sem, ezzel a módszerrel a következő módon fogalmazható meg: null [] = True null _ = False Itt az első alternatíva csak és kizárólag az üres lista konstans esetén fog illeszkedni, és ekkor a függvény a True (igaz) értéket veszi fel. Az összes többi lehetőséget egy ún. joker mintával fedjük le, amely tetszőleges értékre illeszkedik. Ennek kapcsán hozzátesszük, hogy a joker minta alkalmazásakor az adott paraméter értékehez sem tudunk hozzáférni (hiszen nem neveztük el), ezért ilyet akkor írunk, ha attól függetlenül akarjuk képezni a függvény értékét. Emiatt a joker mintát úgy is nevezhetjük, hogy névtelen változó. Ennél összetettebb listákat úgy tudunk feldolgozni függvényekkel, ha a listák felépítésének inverzében gondolkodunk. Vagyis a : szimbólummal most nem egy elemet és egy listát építünk össze listává, hanem egy listát bontunk fel ennek segítségével egy elemre és egy listára. Ezeket, a szintén LISP-től 8

származó örökség részeként, a lista fejének és törzsének nevezzük. A nekik megfelelő függvények már csak mintaillesztés segítségével fogalmazhatóak meg: head (x:_) = x head _ = error "head: empty list" tail (_:xs) = xs tail _ = error "tail: empty list" Innen talán jobban kiderül, hogy a mintaillesztés valójában kicsivel több, mint mezei egyenlőségvizsgálat. Észrevehetjük ugyanis, hogy egy minta tartalmazhat lyukakat, amelyeket változónevekkel tudunk lefedni. Az illesztés során ezeket a lyukakat fogjuk majd kitölteni az adott paraméter mintának megfelelő, egyező részeivel, melyekre aztán így később a függvény törzsében akár külön-külön is hivatkozni tudunk. Mellette azt is megfigyelhetük, hogy a listákra vonatkozó mintákat zárójelbe kellett tennünk, mivel ott egyetlen értéket akarunk több részre felbontani. A head definíciójában egy lista csak akkor fog illeszkedni az első alternatívához tartozó mintára, amennyiben azt annak idején a : szimbólummal hoztuk létre. Minden más esetben egy hibaüzenetet hozunk létre, amely lényegében a [], azaz az üres lista esete. Erre azért van szükség, mert ha az üres listának nincs feje, vagyis nem tudunk belőle elölről leválasztani elemet. Így a head emiatt egy parciális függvény lesz. Ugyanez a gondolatmenet érvényes a tail esetében is. GHCi> head [] *** Exception: head: empty list GHCi> head [1..5] 1 GHCi> tail [1..5] [2,3,4,5] 1.4. A rekurzió A listák esetében gyakorta előfordul, hogy a függvénnyel a teljes listát végig kell járnunk. Például elegendő csak arra gondolnunk, amikor el akarjuk dönteni, hogy két lista egyenlő-e. Ilyenkor, mivel nem tudjuk pontosan a program írásakor, az egyes listák milyen hosszúak lehetnek, valamilyen, az egyes elemek esetében azonos módon elvégezhető, általánosított lépéssorozat ismétlésére, iterálására van szükségünk. 9

Funkcionális nyelvekben erre a rekurzió fogalmát használják fel. Egy függvényt rekurzívnak nevezünk minden olyan esetben, amikor a definíciójában saját magára hivatkozik. Ezért gyakran a rekurziót úgy is szokták tréfásan definiálni, hogy Rekurzió: ld. rekurzió. A tisztán funkcionális nyelvekben nagyon óvatosan kell bánnunk az imperatív nyelvekből ismert, teljesen ártatlan megoldásokkal, mint amilyen a következő is: x = x + 1 Ha imperatív módon tekintünk a fenti egyenletre, akkor ezt úgy kellene értelmeznük, mint az x értékének növelését eggyel. Ez viszont a Haskell működési elvei szerint egy végtelen rekurzió eredményez, amely kiértékelése során az értelmező sosem lesz képes befejezni az eredmény meghatározását. Ez azért történik így, mert ebben az esetben definiálni akarjuk az x függvényt, amely tulajdonképpen egy konstans (mivel nincs paramétere, tehát mindig ugyanazt az értéket fogja képviselni), és ennek meghatározásához szükségünk lenne annak korábbi értékére. Ezt az értéket viszont éppen akkor szeretnénk kiszámítani, amely utána folyamatosan, a végtelenségig magára hivatkozik. Az imperatív nyelvekben ez a szerkezet azért működőképes, mert az x értékének van egy állapota, és amikor a fenti értékadást értelmezzük, akkor annak bal oldalán mindig az új, a jobb oldalán pedig a régi állapotáról beszélünk. Tisztán funkcionális nyelvekben azonban a hivatkozási helyfüggetlenség miatt végig ugyanarról értékről beszélünk, innen adódik az eltérő viselkedés. Ha valamiért egy ilyen végtelen ciklusba keverednénk a számítások során, a Ctrl+C billentyűkombinációval ezeket bármikor megszakíthatjuk: GHCi> x ^CInterrupted. Ennek ellenére, meglepő módon, Haskellben az ilyen végtelen eredményt gyártó függvények esetenként lehetnek hasznosak, mindössze csak a megfelelő típusban kell gondolkodnunk. Például tekintsünk az alábbi függvény definícióját: repeat x = x : (repeat x) Ezt a következő módon tudjuk használni az értelmezőn keresztül: GHCi> repeat 1 [1,1,1,1,1,1,1,1,1,1,1^CInterrupted. 10

Habár láthattuk, hogy a függvény kiszámítása ebben az esetben sem fog önmagától befejeződni, az értelmező mégis képes volt valamilyen köztes eredményt, egyesek véget nem érő folyamát, megjeleníteni. Ennek megértéséhez észre kell vennünk, hogy a repeat függvény definícióját nem feltétlenül úgy kell olvasnunk, ahogy azt az imperatív nyelvek esetében megszokhattuk. Annak megfelelően elsőként ugyanis a zárójeles részt, a repeat x részkifejezést kellene kiszámítanunk, és ehhez kellene a : alkalmazásán keresztül az x értékéből még egy elemet az így elkészített lista elé fűzni. Ezt a kiértélési módszert mohónak nevezzük, ahol a függvények alkalmazásához először mindig ki akarjuk számítani a paraméterek értékeit, és csak utána velük a függvény értékét. Azonban létezik egy másik megközelítési mód, ahol elsőként mindig a függvényt számítjuk ki, és a paramétereivel kizárólag csak akkor foglalkozunk, amennyiben arra szükségünk van. Emiatt elképzelhető az is, hogy egyáltalán nem fogjuk valamelyik paramétert kiértékelni. Ezt lusta kiértékelésnek nevezzük, és Haskellben is ezt alkalmazzák. Ennek megfelelően a repeat értékének kiszámítása során először a felsőbb szinten szereplő operátort, vagyis a : műveletét végezzük el először, és így a még el nem készült lista elejére illesztjük vele az x értékét. Ezt folytatjuk a repeat rekurzív kiszámításával, amellyel mögé kerül egy másik x érték, és az egész folyamat egészen a végtelenségig halad. A lustaságnak és annak köszönhetően, hogy a részkifejezéseket így csak akkor értékeljük ki, amennyiben ténylegesen szükségünk van az értékükre, az alábbi kifejezés viszont már véges idő alatt kiszámítható lesz: GHCi> head (repeat 1) 1 A kapott eredmény fenti gondolatmenetet követve teljesen logikusnak tűnik: miért bontanánk ki a teljes listát, miközben a válaszadáshoz elegendő csak az első elemének meghatározása? A lustaságnak köszönhetően Haskellben lehetőségünk van végtelen adatszerkezetekkel dolgozni, amely az algoritmusok leírását is meglepően elegánssá és tömörré tudja tenni. Fontos megjegyeznünk, hogy nem minden funkcionális nyelv esetében igaz, mivel a mohó kiértékelés azokban is sokáig az uralkodó kiértékelési stratégia volt, tehát például a LISP is ilyen. Természetesen a repeat függvény a véges változatát is el tudjuk készíteni, ha korlátozzuk a listába illeszthető elemek számát. Tisztán funkcionális programozás esetében, mivel nincsenek állapottal rendelkező nevek, vagyis a hagyományos értelemben vett programváltozók, ezt úgy tudjuk megoldani, ha felveszük a függvénynek egy újabb paramétert. Ez a paraméter egy 11

számláló szerepét fogja játszani, amelyet a korlát adott értékétől visszafele indulva lépésenkét mindig eggyel csökkentünk egészen addig, amíg az pozitív. Így lényegében két esetet fogunk kapni. Egy ún. induktív esetet, amikor a rekurzív hivatkozással megadjuk, hogy miként építjük a listát, miközben csökkentjük a számlálót, és egy ún. alapesetet, amikor a függvény rekurzió segítsége nélkül képes lesz közvetlenül meghatározni az értékét. Az induktív eset felel meg a ciklus lépéseinek, magjának újbóli végrehajtásának, az alapeset pedig a ciklusból kilépés, vagyis az ismétlés megállításának feltételeit írja le. Ezeket az eseteket függvényalternatívák felhasználásával adhatjuk meg. Így a repeat véges változata, a replicate a következőképpen áll elő: replicate n x n <= 0 = [] otherwise = x : (replicate (n - 1) x) Megfigyelhető, hogy a replicate első paramétere, vagyis a számláló, rekurzívan mindig az előzőnél eggyel kisebb értékkel hívódik meg. Ellenkező esetben a rekurzió sosem fejeződne be. Ezért fontos törekednünk arra, hogy az induktív esetek megfogalmazása során mindig igyekezzünk elérni az alapesetekben leírt leállási feltételeket. Ezt úgy tehetjük meg, hogy a rekurzív hívás során legalább az egyik paramétert megváltoztatjuk. Ez a replicate tehát esetében azt jelenti, hogy a számláló értéke lépésenként csökkent és az ismétlés megáll, amikor nullára vagy az alá érünk. GHCi> replicate 0 1 [] GHCi> replicate 3 1 [1,1,1] GHCi> replicate (-3) 1 [] Nem csak számok, de más típusú értékek is csökkenthetőek, ezáltal rekurzív stílusban feldolgozhatóak. Ilyenek többek között a listák, ahol csupán a lista törzsének a továbbadása elegendő lehet egy alapeset eléréséhez, amely ekkor általában az üres lista. Emiatt listák esetében a törzs kiszámítása lényegében az eggyel csökkentésnek, az üres lista pedig a nulla konstansnak feleltethető meg. Mindezekre példaként tekintsünk most a take függvény definícióját, amelynek az a feladata, hogy egy lista elejéről adott számú elemet adjon vissza, amennyiben ez lehetséges. Ha nem, akkor adja vissza az eredeti listát változatlanul. 12

take _ [] = [] take n _ n <= 0 = [] take n (x:xs) = x : take (n - 1) xs Észrevehetjük, hogy ebben a definícióban mintaillesztést és őrfeltételeket is alkalmaztunk vegyesen, így fejeztünk ki két lehetséges alapesetet. Az első sor értelmében ha a feldolgozandó lista üres, akkor az első paraméter (a számláló) értékétől függetlenül egyszerűen csak adjunk vissza egy üres listát. A második sor szerint ha a kivenni kívánt elemek számát jelző számláló már nem pozitív, akkor ismét adjunk vissza üres listát, hiszen már nem kell belőle többet kivennünk. Végezetül a harmadik sorban azt olvashatjuk, hogy minden más esetben vegyük a lista első elemét és illesszük hozzá a : operátorral ahhoz a listához, amelyet úgy nyerünk, hogy egy eggyel rövidebben listából veszünk ki, a take rekurzív alkalmazásával, eggyel kevesebb elemet. Ehhez az esethez a mintaillesztés és az őrfeltételek viselkedésének megfelelően mindig akkor jutunk, ha az előtte szereplő alapesetek feltételei közül egyik sem teljesült még. A take függvény is lusta lesz, így képes akár végtelen listákkal is dolgozni. Például olyanokkal, amelyeket a repeat felhasználásával állítottunk korábban elő: GHCi> take 3 (repeat 1) [1,1,1] Ennek mikéntjének pontosabb megértéséhez tekintsük át, miként is fog az adott kifejezés kiértékelése végbemenni: take 3 (repeat 1) ----------------- < illeszkedés a take 3. sorára, ahol n = 3, x = 1, xs = repeat 1 > 1 : take (3-1) (repeat 1) 1 : take 2 (repeat 1) ----------------- < illeszkedés a take 3. sorára, ahol n = 2, x = 1, xs = repeat 1 > 1 : 1 : take (2-1) (repeat 1) 1 : 1 : take 1 (repeat 1) ----------------- < illeszkedés a take 3. sorára, ahol n = 1, x = 1, xs = repeat 1 > 1 : 1 : 1 : take (1-1) (repeat 1) 1 : 1 : 1 : take 0 (repeat 1) ----------------- 13

< illeszkedés a take 2. sorára, ahol n = 0 > 1 : 1 : 1 : [] [1,1,1] Emellett még az is tetten érhető, hogy a replicate tulajdonképpen a take és a repeat kompozíciójaként is megfogalmazható, amely remekül összeegyeztethető a moduláris programozás elveivel: replicate n x = take n (repeat x) 1.5. Rekurzió listák felett: hajtogatás A listák felett szervezett rekurzió kapcsán érdemes foglalkozni valamennyit a mögötte megbúvó sémával. Ez egy jól általánosítható séma, amelyet hajtogatásnak nevezzük. Ez arról szól, hogy folyamatosan vesszük egy lista elemeit és belőlük egy kétparaméteres függvény felhasználásával lépésenként egy görgetett, hajtogatott értéket építünk fel, amely aztán az így képzett számítás végeredménye lesz. Az elemek bejárásának, így a hajtogatásnak az iránya lehet balról jobbra: vagy jobbról balra: (... ((((z x 0 ) x 1 ) x 2 ) x 3 )...) x n x 0 (x 1 (x 2 (x 3 (... (x n z)...)))) ahol a kétparaméteres függvény, az x 0, x 1,..., x n értékek a összehajtogatni kívánt lista adott indexű elemei, valamint a z a hajtogatás során képzett, göngyölt eredmény kezdőértéke. A függvénynek attól függően, hogy balról vagy jobbra hajtogatunk vele, bal- illetve jobbasszociatívnak kell lennie. Értelemszerűen asszociatív függvények mind a két esetben alkalmazhatóak. Például tekintsük az elemek összeadását! Ekkor az operátor az összeadás (+) lesz, a hozzá tartozó kezdőérték a 0, és a hajtogatás során kiszámított érték pedig a listában szereplő elemek összege. Annak alapján, amit eddig láthattunk a listák felett értelmezett rekurzióról, nem is annyira nehéz megadni egy ilyen függvény definícióját: sum [] = 0 sum (x:xs) = x + sum xs Ha az egyes szabályokat valamilyen lista esetében értelmezni kezdjük, akkor láthatjuk, hogy a függvény alkalmazásával a következő transzformációt végezzük el: 14

sum [1,2,3] --> 1 + sum [2,3] --> 1 + (2 + sum [3]) --> 1 + (2 + (3 + sum [])) --> 1 + (2 + (3 + 0)) A funkcionális programozásban ez a séma megfogható egy alkalmas függvényfajtával, amelyet magasabbrendű függvényeknek nevezünk. Magasabbrendűnek nevezünk minden olyan függvényt, amely vagy függvényt kap paraméterül, vagy pedig függvényt hoz létre eredményként. Ez a lehetőség abból származik, hogy funkcionális nyelveken a függvények az értékekhez, például számokhoz hasonlóan feldolgozhatóak és képezhetőek. Itt és most csak az első esetet használjuk ki, vagyis amikor a függvényünk egy másik függvényt kap paraméterül, de később majd kitérünk a másik esetből következő lehetőségekre is. Kezdésképpen azonban még ne az imént megismert sémát próbáljuk meg ezen a módon kifejezni, hanem helyette tekintsünk egy egyszerű, de érdekes példát a magasabbrendű függvényekre, a map függvényt. Ez a függvény egy függvényt és egy listát kap paraméterül, majd visszaad egy olyan listát, ahol a lista összes elemére alkalmazza a függvényt. Az angol matematikai szaknyelvben ugyanis a "map" leképezést jelent, vagyis amikor vesszük egy halmaz összes elemét, és azt teljes egészében egy másik halmazba képezzük át. GHCi> map negate [1,-1,2,-2,3,-3] [-1,1,-2,2,-3,3] Megfigyelhetjük, hogy a negate függvényt látszólag mindenféle paraméter nélkül hívtuk meg. Ez annak a következménye, hogy a függvényt értékként fogtuk meg, és ilyenkor csak a nevére van szükségünk. Minden mást meg fog kapni a map függvénytől, ahogy annak a definíciójából is kiderül: map _ [] = [] map f (x:xs) = (f x) : (map f xs) Látható tehát, hogy a negate itt az f paraméter értékeként jelenik, és a definícióban ennek adjuk át folyamatosan a listában egymás után kivett értékeket. A függvényt egyszerűen csak a map egyik paramétereként vettük fel és úgy használjuk, mintha egy korábban már definiált függvényről lenne szó. 15

1.6. Függvények függvényei Most, miután láttuk, hogy miként lehet rekurzív és magasabbrendű függvényeket készíteni, fogalmazzuk meg a korábban említett hajtogatási sémát! Az egyszerűség kedvéért itt csak a jobbról hajtogatással, vagyis a foldr ("fold right") függvény definíciójával foglalkozunk, de ehhez hasonlóan a balra hajtogatás, vagyis a foldl ("fold left") is megadható: foldr _ z [] = z foldr f z (x:xs) = f x (foldr f z xs) Tekintsük a definíció egyes részeit! Amikor a lista üres, a függvény értéke legyen a hajtogatás eredményének kezdőértéke, mivel ebben az esetben befejeződik a rekurzió. A listában tárolt számok összegzésénél ez lényegében azt jelentette, hogy az üres listához az összeadás egységelemét, a nullát rendeltük. Amennyiben a lista nem üres, vegyük a lista soron következő, vagyis első elemét és a függvény felhasználásával azt kombináljuk össze azzal az értékkel, amelyet úgy nyertünk, hogy a lista fennmaradó részére (törzsére) alkalmaztuk a hajtogatást (jobbasszociatív) módon. Ezzel a sémával most már képesek vagyunk a sum függvényt akár egyetlen sorban összefoglalhatóan leírni: sum xs = foldr (+) 0 xs Itt láthatjuk, hogy a számok összegzéséhez a + függvényt és a 0 kezdőértéket adtuk át a foldr függvénynek. Nem nehéz ellenőrizni, hogy ha ezeket a paramétereket közvetlenül behelyettesítjük a foldr definíciójába, akkor visszakapjuk belőle a sum definícióját. Ezért is tekinthetőek a magasabbrendű függvények a hagyományos függvényeknél erősebbeknek: az általuk megfogalmazott függvényosztályban minden függvény kifejezhetően csupán annyival, hogy a megfelelő törzset helyettesítjük a definíciójukba. A fenti példában a + műveletet ismét paraméterek nélkül adtuk át. Illetve, mivel operátor, és ezért a nyelv helyesírási szabályai szerint szimbólumokból kell, hogy álljon, zárójelbe is kellett tennünk. Természetesen a foldr függvénynek bármilyen más függvény is átadható, akár olyanok is, amelyeket mi magunk írtunk. Velük szemben az egyedüli igazi követelmény, hogy a típusuk megfelelő legyen. Noha a típusok kérdéskörét eddig ügyesen sikerült elkerülnünk, tagadhatatlan, hogy a Haskell programok írásának ez egy másik létfontosságú, azonban sokszor láthatatlan része. Haskellben minden függvénynek van típusa annak ellenére, hogy ezek közül eddig egyetlen egyet sem említettünk. Az értelmezőben az egyes kifejezések típusát a :type (vagy röviden :t) paranccsal kérdezhetjük le. Nézzük meg például meg, hogy mi a foldr típusa: 16

GHCi> :type foldr foldr :: (a -> b -> b) -> b -> [a] -> b A válasz megértéséhez elsőként megjegyezzük, hogy Haskellben a nevekhez a típusra vonatkozó információt a :: (dupla kettőspont) szimbólummal kapcsoljuk. Továbbá a függvények típusaiban a -> (nyíl) szimbólum szerepel, amely lényegében egy típusok felett értelmezett operátor, amelyet arra használunk, hogy leírjuk vele a függvény értelmezési tartománya és annak értékkészlete közötti összefüggést: f :: A -> B Ekkor tehát az f egy olyan függvény lesz, amelyik az A típusból (mint értelmezési tartományból) a B típusba (mint értékkészletbe) képez. Többparaméteres függvények esetében alkalmazhatnánk a matematikában megszokott jelölést, vagyis az értelmezési tartományok Descartes-szorzatát. Például úgy, hogy a létezik egy másik típusszintű operátor, az x, amely két típus szorzatát írja le: f :: A x B -> C Ilyen lehetőség létezik Haskellben is, azonban ezt a, (vessző) operátorral jelöljük: f :: (A, B) -> C Azonban technikai okból, melyekről majd a későbbiekben lesz részletesebben szó, a nyelv tervezői ehelyett mindenhol csak a nyíl operátor alkalmazása mellett döntöttek: f :: A -> B -> C Ezáltal a függvények típusát a következő ökölszabály szerint lehet könnyen értelmezni. A nyilakkal elválasztott részek közül az első (n 1) darab tag a rendre paraméterek típusainak feleltethető meg, míg az utolsó, vagyis n. tag a függvény értékének típusát adja meg. Ennek a magyarázatnak megfelelően kiderül, hogy a foldr függvénynek három paramétere van: a -> b -> b b [a] 17

Itt a függvények típusa mellett egy másik típusoperátort is felfedezhetünk, ez a [] avagy a lista szimbóluma. Ez szintén egy típusok felett értelmezett függvény, azonban ennek csak egyetlen paramétere van, a listában szereplő elemek típusa. Ez tehát, hasonlóan a -> viselkedéséhez, egy típusból egy másik típust képez, az adott típusú elemeket tartalmazó lista típusát. A rövid alapozás után most menjünk egyesével végig a foldr paraméterein: az első egy függvény, a második valamilyen érték, a harmadik pedig egy lista. Ami azonban feltűnő lehet, hogy nem látunk bennük egyetlen konkrét típust sem, helyettük csak az a és b betűk szerepelnek. Ezek a típusokban szereplő változók, vagyis típusváltozók, amelyeken keresztül az ún. parametrikus polimorfizmus jelenik meg. Ez az jelenti, hogy a foldr függvény nem csak adott típusokra alkalmazható, hanem automatikusan képes szinte tetszőleges típusú értékekkel dolgozni. A változókat arra használjuk a típus leírásában, hogy az egyes paraméterek, illetve a visszatérési érték egymáshoz való logikai viszonyait kifejezzük. Így, ha ezeket a típusváltozókat tényleges típusokkal helyettesítjük, ennek az egyébként absztrakt típusnak kapjuk meg végtelen sok konkretizált változatát. Például tekintsük azt az esetet, amikor az a értéke Integer, a b értéke pedig Bool, ahol az Integer egész számot, a Bool logikai értéket jelent: foldr :: (Integer -> Bool -> Bool) -> Bool -> [Integer] -> Bool Azonban a helyettesítéskkel kapcsolatban van egy nagyon fontos szabály. Az egyes típusváltozók értékét mindig csak ugyanarra a típusra cserélhetjük. Tehát a foldr alábbi konkretizálása helytelen, ezzel a paraméterezéssel már nem használható: foldr :: (Bool -> Bool -> Bool) -> Integer -> [Integer] -> Integer Az eddigi függvényeink típusát ún. típuskikövetkeztetésen keresztül kaptuk meg, ezért nem volt szükséges nekünk kitalálni és megadni. A típuskikövetkeztetés egy algoritmus, amely mindig megpróbálja a függvény legáltalánosabb, vagy más néven principális típusát meghatározni. A foldr esetében az imént megadott, típusváltozókat tartalmazó típus az összes lehetséges közül a legáltalánosabb, mivel a függvény leírása során: Mivel a függvény harmadik paraméterére egyszer az üres listát ([]), máskor pedig az : szimbólumon keresztül a listák felbontását lehetővé tevő mintát illesztettük, feltételezhető, hogy ez valamilyen értékeket tartalmazó, vagyis egy a típusú lista. 18

A második paraméterrel nem csináltunk semmi különlegeset, ezért nem lehetünk biztosak benne, hogy a listákban található értékek típusával azonos típusú lesz, így egy új, eddig nem használt típusváltozót rendelünk hozzá, ez legyen a b. Végezetül, az első paramétert úgy használtuk, mint egy függvényt, amely a listából kivett (a típusú) elemet kapja első paramétereként, majd a a foldr rekurzív hívásával számolt (b típusú) értéket második parmétereként és ugyanolyan típusút is ad vissza, annak típusának végül annak kell lennie, hogy a -> b -> b. Ez utóbbira bizonyítékot a függvény definíciójának első sorában láthatunk, amikor a második paramétert változtatás nélkül visszaadjuk, amelyről már tudjuk, hogy b típusú. Amikor a fordító nem képes ez egy ehhoz hasonló gondolatmenettel magától kikövetkeztetni a típust, hibát jelez és nem fordítja le a programunkat. Ezért ezt a folyamatot típusellenőrzésnek is nevezik. A típusok kikövetkeztetése nem csak a típusokra vonatkozó információk megadásától mentesít minket, hanem arra is alkalmas, hogy ellenőrizzük az adott definíció értelmét. Tehát ez lényegében a programunk egyfajta szemantikai értelmezéseként is szolgál. Képzeljük el a foldr definíciójának egy látszólag helyes, viszont valójában egy elrontott változatát: foldr _ z [] = z foldr f z (x:xs) = f x (foldr z f xs) Ha ezt a programot most be akarnánk tölteni az értelmezőbe, akkor válaszul egy típusra vonatkozó hibaüzenetet fogunk kapni, amely tájékoztat arról, hogy nincs minden rendben: GHCi> :load Fail.hs [1 of 1] Compiling Main ( Fail.hs, interpreted ) Fail.hs:4:33: Occurs check: cannot construct the infinite type: t1 ~ t -> t1 -> t1 Relevant bindings include xs :: [t] (bound at Fail.hs:4:14) x :: t (bound at Fail.hs:4:12) z :: t1 (bound at Fail.hs:4:9) f :: t -> t1 -> t1 (bound at Fail.hs:4:7) 19

foldr :: (t -> t1 -> t1) -> t1 -> [t] -> t1 (bound at Fail.hs:3:1) In the first argument of foldr, namely z In the second argument of f, namely (foldr z f xs) Failed, modules loaded: none. Ehhez azonban érdemes hozzátenni, a foldr több különböző változata lehet típusosan helyes annak ellenére, hogy nem felel a fentebb megfogalmazott specifikációnknak. Ezért, bár a típusleírást nem minden esetben kötelező megadnunk, jó programozási gyakorlatnak számít, ha a fontosabb függvények definíciójának részeként annak típusát is megadjuk. Ez segít többek között észrevenni, ha a függvény típusa véletlenül idő közben mégis megváltozna. Például az utolsó sorban az x és f változókat felcseréljük: foldr :: (a -> b -> b) -> b -> [a] -> b foldr _ z [] = z foldr f z (x:xs) = x f (foldr f z xs) Ez explicit típusdeklarációnak köszönhetően azonban a fordító képes rámutatni erre a figyelmetlenségre: [1 of 1] Compiling Main ( Fail.hs, interpreted ) Fail.hs:5:20: Couldn t match expected type (a -> b -> b) -> b -> b with actual type a a is a rigid type variable bound by the type signature for foldr :: (a -> b -> b) -> b -> [a] -> b at Fail.hs:3:10 Relevant bindings include xs :: [a] (bound at Fail.hs:5:14) x :: a (bound at Fail.hs:5:12) z :: b (bound at Fail.hs:5:9) f :: a -> b -> b (bound at Fail.hs:5:7) foldr :: (a -> b -> b) -> b -> [a] -> b (bound at Fail.hs:4:1) The function x is applied to two arguments, but its type a has none In the expression: x f (foldr f z xs) In an equation for foldr : foldr f z (x : xs) = x f (foldr f z xs) Failed, modules loaded: none. 20

1.7. Részlegesen alkalmazott függvények Ha visszaemlékszünk, volt egy másik magasabbrendű függvényünk is, a map. Ennek a típusát is le lehet kérdezni: GHCi> :type map map :: (a -> b) -> [a] -> [b] A definíció alapján a korábban informálisan bemutatott gondolkodásmód alapján a foldr függvényhez hasonlóan ennek a legáltalánosabb típusa is kikövetkeztethető. Amikor viszont a példában a negate függvénnyel használtuk, a típusban szereplő (a és b) típusváltozókat az Integer típusra szűkítettük: GHCi> map negate [1,-1,2,-2,3,-3] [-1,1,-2,2,-3,3] Azonban érdemes megjegyezni, hogy a map függvénynek nem csak egyparaméteres függvényeket tudunk átadni. Haskellben, mivel a többparaméteres függvények értelmezési tartományait nem Descartes-szorzatokkal ábrázoljuk, lehetőségünk nyílik arra, hogy a függvényeket részlegesen, vagyis az eredeti paraméterszámánál kevesebb paraméterrel alkalmazzuk. Ennek megértéséhez tekintsük a const függvény definícióját: const :: a -> b -> a const x _ = x Ez a függvény két paramétert vár, amelyek közül csak az elsőt adja vissza. GHCi> const 1 2 1 GHCi> const True False True Ahogy eddig is láthattuk a (szöveges névvel rendelkező) függvények esetében, az alkalmazásukhoz előre a függvény nevét írjuk, majd az után szóközökkel elválasztva felsoroljuk a paramétereit. Most viszont hozzátesszük, hogy a függvényalkalmazás valójában egy balasszociatív bináris operátor, maga a szóköz, egyik oldalán a függvénnyel, másik oldalán annak paraméterével. Ezt láthatóvá tudjuk tenni úgy, ha az előbbi kifejezésben az összes függvényalkalmazást bezárójelezünk: 21

GHCi> ((const True) False) True Mindemellett a const True kifejezés típusát is le tudjuk kérdezni: GHCi> :type const True const True :: b -> Bool Ebből kiderül, hogy az egyik paraméter elhagyásával egyszerűen létrehoztunk egy másik függvényt. Ez egyben meg tudja magyarázni, hogy a nyelv kialakításakor miért így kezdték el ábrázolni a függvényeket. Megjegyezzük, ennek következményeként a -> típusoperátor jobbasszociatív lesz, így a const típusa a következőképpen lesz leírható: const :: a -> (b -> a) Ez a tulajdonság hasznosnak tud bizonyulni a magasabbrendű függvények alkalmazása során is. Vegyük például a map függvényt, ahol az első paraméternek a const függvény is megadható, amennyiben annak első paraméterét rögzítjük a korábbi módon: GHCi> map (const True) [1..5] [True,True,True,True,True] Ekkor a listában szereplő számok egyike sem megjelenni az eredményként keletkező listában, egyedül a True konstans fog egymás után a számuknak, vagyis a lista hosszának megfelelően ismétlődni. 1.8. Típusok Minden, eddig szerepelt függvénynek természetesen ugyanúgy van típusa, például a negate esetében: negate x = 0 - x a típusa a következő: GHCi> :type negate negate :: Num a => a -> a 22

Ez szinte teljesen tud illeszkedni ahhoz a naiv sejtésünkhöz, mely szerint ez egy egyparaméteres függvény, amely a paraméterével azonos típusú értéket hoz létre. Egyedüli eltérés ettől a típusban a Num a => előtag megjelenése. Ezt a típusváltozóra vonatkozó megszorításnak vagy röviden csak megszorításnak hívják, és a - operátor alkalmazásából kaptuk: GHCi> :type (-) (-) :: Num a => a -> a -> a Azt már tudhatjuk, hogy a típusváltozók jelenléte polimorf függvényekre utal. Ezért azt mondhatjuk, hogy a - több különböző típusú értékre is felhasználható. A típusváltozók általában tetszőleges Haskellbeli típusra kicserélhetőek. Viszont ha a típus leírásához rájuk vonatkozóan egy megszorítás is megjelenik, akkor azzal a behelyettesíthető típusokat lényegében leszűkítjük egy adott csoport elemeire. A Num is egy ilyen csoport, pontosabban osztály, amely azon típusok osztályát jelenti, amelyek számokat ábrázolnak. Haskellben ilyen típusok például az Integer vagy Double, így a - művelet mind a két esetben alkalmazható, miközben az adott típusra jellemző változatot használjuk a háttérben. Ez úgy is felfogható, mint a függvénynevek túlterhelése, amikor ugyanaz a név több típus esetén is újrahasznosítható. Vagy megfordítva, ugyanaz a függvény a típustól függően többféleképpen képes viselkedni. Ezt eseti, vagy ad hoc polimorfizmusnak szokták nevezni. GHCi> -- (-) :: Double -> Double -> Double GHCi> 0.0-1.0-1.0 GHCi> -- (-) :: Integer -> Integer -> Integer GHCi> 0-1 -1 De a Num osztály nem csak a - operátort tartalmazza, hanem benne megtalálható minden, számokkal kapcsolatos általános művelet. Erről bővebb tájékoztatást az értelmezőn keresztül az :info vagy röviden :i paranccsal kérhetünk: GHCi> :info Num class Num a where (+) :: a -> a -> a (-) :: a -> a -> a (*) :: a -> a -> a negate :: a -> a 23

abs :: a -> a signum :: a -> a frominteger :: Integer -> a -- Defined in GHC.Num instance Num Word -- Defined in GHC.Num instance Num Integer -- Defined in GHC.Num instance Num Int -- Defined in GHC.Num instance Num Float -- Defined in GHC.Float instance Num Double -- Defined in GHC.Float A class kulcsszó egy ilyen típusosztályt vezet be, amely után annak neve szerepel, ebben az esetben a Num, majd egy típusváltozó. Ezt a változót tudjuk az osztály leírásában arra használni, hogy megadjuk az összes olyan függvényt, amelynek rendelkeznie kell valamilyen definícióval az összes olyan típus esetében, amelyek ennek az osztálynak az elemei. A típusosztályokat ún. példányok megadásával a forráskód bármelyik részén bővíthetjük. Ehhez mindössze annyit kell tennünk, hogy az instance kulcsszóval bevezetve megadjuk az osztály nevét és a típusváltozó helyére kitöltjük azt a típust, amelyre vonatkozóan az osztályban szereplő függvényeket meg akarjuk adni. Fontos azonban éles különbséget tennünk a típusosztályok és az objektumorientált paradigmában megjelenő osztályok fogalmai között. Habár valóban vonható köztük valamilyen szinten párhuzam, a típusosztályok az objektumorientált terminológia szerint inkább absztrakt interfészeknek tekinthetőek. Meglepő módon azonban nem csak függvények, hanem konstansok (amelyek tulajdonképpen paraméter nélküli függvények) is lehetnek esetlegesen polimorfak. Erre példaként tekintsük az 1 konstanst: GHCi> :type 1 1 :: Num a => a Miért is pontosítanánk az 1 szimbólum típusát annál jobban, mint hogy azt mondjuk róla, az egy szám? Tekinthetnénk egész vagy lebegőpontos számnak egyaránt, a tényleges ábrázolásáról ez az írásmód nem ad információt. Az ábrázolás a használat során fog (lusta módon) egyre pontosabbá válni. Nézzük meg erre példaként a következő kifejezést: GHCi> :type 1 / 3 1 / 3 :: Fractional a => a Itt egy másik típusmegszorítás kerül elő, amely azt állítja, hogy az 1 / 3 nem egyszerűen valamilyen szám, hanem törtszám. A / (osztás) használata miatt kizártuk az egész számokat. A Fractional osztály a Num egy alosztálya, amely további megkötéseket tartalmaz a típusváltozóra: 24

GHCi> :info Fractional class Num a => Fractional a where (/) :: a -> a -> a recip :: a -> a fromrational :: Rational -> a -- Defined in GHC.Real instance Fractional Float -- Defined in GHC.Float instance Fractional Double -- Defined in GHC.Float Hozzátesszük, hogy amikor az értelmezőben ki akarjuk egy ilyen kifejezés értékét számítani, az eredmény meghatározásához végül kénytelenek leszünk az osztályból valamelyik konkrét típust kiválasztani: GHCi> 1 / 3 0.3333333333333333 Ez az értelmező erre a célra beépített mechanizmusán keresztül történik, ahol a törtszámok esetében a Double típusra szűkített le. A :: használatával azonban lehetőségünk van a kifejezésekhez típusinformációt fűzni, és ezzel mi magunk is szabályozni tudjuk, hogy az adott polimorf típusból milyen konkrét, ún. monomorf típust hozzuk létre. GHCi> 1 / 3 :: Float 0.33333334 Ezt viszont nem szabad összetévesztenünk esetleg más programozási nyelvekből ismert típuskényszerítés fogalmával. A Haskell egy szigorúan típusos nyelv, ezért a típusrendszere nem engedi meg azt, hogy anélkül megváltoztassuk egy kifejezés típusát, hogy ne adnánk meg a régi és új típus közti konverziót végző függvényt (a kifejezés részeként). Ennek az elvnek az elhagyásával a rendszer nem lenne képes a programok helyességére garanciákat nyújtani. GHCi> (1 / 3 :: Float) :: Double <interactive>:2:2: Couldn t match expected type Double with actual type Float In the expression: (1 / 3 :: Float) :: Double In an equation for it : it = (1 / 3 :: Float) :: Double Szerencsére a számok különböző típusai között számos konverziós függvény elérhető. Ebben az esetben realtofrac használható a kívánt hatás elérésére: 25