• Nem Talált Eredményt

Haskellben nem használunk kapcsos zárójeleket műveletblokkok jelö-lésére, helyette a bal oldali margó alapján különbözteti meg a fordító az összetartozó kifejezéseket, éppen ezért a margózás, vagy angolul a layout rule, fontos szintaktikai elem.

4.3. feladat Írjuk meg az aritM Haskell-függvényt, amely két egész szám esetében meghatározza a számok összegét, szorzatát, különbségét, osztási hányadosát, osztási egészrészét és osztási maradékát. Az eredmény hatelemű tuple típusú érték legyen.

aritM :: Int -> Int -> (Int, Int, Int, Double, Int, Int) aritM x y = (r1, r2, r3, r4, r5, r6)

4.3. A margószabály 45 where

r1 = x + y r2 = x * y r3 = x - y

r4 = fromIntegral x / fromIntegral y r5 = div x y

r6 = mod x y

> aritM 11 7

(18, 77, 4, 1.5714285714285714, 1, 4)

A fenti kódsorban a margószabály betartása mellett arra is oda kell figyelni, hogy az osztási hányados meghatározásához típuskonverzióra van szükség, alkalmazni kell afromIntegral-t.

A függvényszignatúrát megadhatjuk típusváltozók segítségével, és amennyiben ezt nem szeretnénk egy sorba írni, akkor figyelembe kell ven-ni itt is a margószabályt. Több sorba tördelve természetesen olvashatóbb formát kapunk:

aritM ::

(Integral a, Fractional a1) =>

a -> a -> (a, a, a, a1, a, a)

Margózással különböző kifejezések, függvénydefiníciók láthatóságát is korlátozhatjuk, kiterjeszthetjük, ahogy azt a következő feladat mutatja.

4.4. feladat Írjunk egy Haskell-függvényt, amely meghatározza egy másod-fokú egyenlet valós gyökeit.

mEgyenlet :: (Floating a, Ord a)

=> a -> a -> a -> (a, a) mEgyenlet a b c

| delta < 0 = error "Komplex gyokok"

| otherwise = (x1, x2) where

x1 = (-b + sqrt delta) / tmp x2 = (-b - sqrt delta) / tmp delta = b * b - 4 * a * c tmp = 2 * a

delta :: Num a => a -> a delta a = 3 * a

A fenti kódsorokban kétdeltakifejezést is definiáltunk, felvetődik a kérdés, hogy az alábbi lekérdezésekben mikor melyikdeltakifejezés értékelődik ki?

> mEgyenlet 2 3 1

> delta 2

Az mEgyenlet a where kulcsszó alatt definiált delta kifejezést hasz-nálja, mert helyileg ez tartozik hozzá, a második lekérdezésben pedig a delta a = 3 * a kifejezés kerül kiértékelésre. Ha azonban rosszul törde-lünk, fordítási hibába ütközhetünk.

4.4. Rekurzió

A rekurzió a funkcionális nyelvek alap vezérlési szerkezete, ami azt jelen-ti, hogy a függvények hivatkozhatnak önmagukra és kölcsönösen egymásra.

Az első fejezetet egy Haskell-függvény megadásával kezdtük, a faktoriális függvény törzsét adtuk meg több módszerrel, most további függvényeket írunk, szintén rekurzívan.

4.5. feladat Írjunk egy-egy Haskell-függvényt, ahol az egyik kivonásos mód-szerrel, a másik Eukleidész módszerével határozza meg két egész szám legnagyobb közös osztóját.

Kivonásos módszer:

lnko :: (Ord a, Num a) => a -> a -> a lnko a b

| a < 0 || b < 0

= error "ez csak pozitiv szamokra mukodik"

| a > b = lnko (a - b) b

| a < b = lnko a (b - a)

| otherwise = a

Eukleidész módszere:

euclid :: Integral a => a -> a -> a euclid a b

| b == 0 = abs a

| otherwise = euclid b (mod a b) A lekérdezések és az eredmények pedig a következők:

4.4. Rekurzió 47

> lnko 24 204 12

> euclid 33989590165734525335 42391251154308366336 13

Algoritmikailag mindkét függvény jól ismert, az lnko esetében azt az el-gondolást követjük, hogy a nagyobbik paraméterből ki kell vonni a kisebbik paraméter értékét, mígnem két egyforma értéket nem kapunk, ami a kezdeti bemenetek legnagyobb közös osztója lesz. Az euclidfüggvény esetében, a rekurzív függvényhíváskor az első paraméter ablesz, a második pedig aza ésbosztási maradéka. A rekurzív függvénytörzset tehát úgy is értelmezhet-jük, hogy ha bnulla, akkor azaés blegnagyobb közös osztójaa, ellenkező esetben megegyezik a bés mod a blegnagyobb közös osztójával.

Rekurzív függvényhívások esetében különösen oda kell figyelni arra, hogy a sajátos, a triviális eseteket külön feltételben kezeljük, és amikor önmagát hívja egy függvény, akkor úgy válasszuk meg az új paramétereket, hogy az ne eredményezzen végtelen számítási folyamatot.

A Haskell gcd(greatest common divisor) függvénye az euklideszi algo-ritmus alapján határozza meg két szám legnagyobb közös osztóját:

> gcd 3792853 187589173 47

4.6. feladat Írjunk egy Haskell-függvényt, amely meghatározza az n-edik Fibonacci-számot.

Definíció szerint az első Fibonacci-szám a0, a második az 1, a számso-rozatn-edik tagját pedig úgy határozzuk meg, hogy összeadjuk azn-1-edik, illetven-2-edik tagot.

A kitűzött példát háromféleképpen is megoldjuk. Az első módszer so-rán a fenti definíció alapján fogjuk a rekurzív függvényhívásokat megadni, amely esetben azt fogjuk tapasztalni, hogy a 25-ödik Fibonacci-szám meg-határozására már várnunk kell.

fibonacciE :: (Ord a, Num a, Num b) => a -> b fibonacciE n

| n < 0 = error "hibas bemenet"

| n == 0 = 0

| n == 1 = 1

| otherwise = fibonacciE (n - 1) + fibonacciE (n - 2) Ahhoz, hogy lássuk, hogy mennyi idő szükséges a kiértékeléshez, kapcsoljuk be azidőmérést:

> :set +s

> fibonacciE 25 75025

(0.45 secs, 77,318,704 bytes)

A fenti algoritmus futási ideje exponenciális, azaz lassú, és ezt a lassúságot nem a rekurzió okozza. Ahhoz, hogy az algoritmus futási idején javíthassunk, például lineáris futásidejű algoritmust találjunk, másképp kell gondolkod-nunk. Ehhez két függvényt fogunk írni. A fibonaccifüggvény szerepe az lesz, hogy kezelni tudjuk a hibás bemenetet, illetve inicializálhassuk a tulaj-donképpeni számításokat végzőauxFibfüggvény paramétereit. AzauxFib első paramétere az nértékét (a hátralevő lépések számát), a második, illet-ve a harmadik paramétere pedig azi-edik, illetvei+1-edik Fibonacci-szám értékét fogja jelölni. A kezdeti meghíváskor, a helyes eredmény meghatáro-zásához, a nulladik és az első Fibonacci-szám értékét kell megadni.

fibonacci :: (Ord a, Num a, Num b) => a -> b fibonacci n

| n < 0 = error "hibas bemenet"

| otherwise = auxFib n 0 1 where

auxFib :: (Eq a, Num a, Num b) => a -> b -> b -> b auxFib n a b

| n == 0 = a

| otherwise = auxFib (n - 1) b (a + b)

> fibonacci 25 75025

(0.00 secs, 59,088 bytes)

A kapott futási idő is azt mutatja, hogy lényegesen hatékonyabb a má-sodik változat. A gyakorlatban létezik logaritmikus futási idejű algoritmus is, amelyről a fejezet végén található kitűzött feladatoknál lesz szó.

A harmadik változat nem fog javítani az algoritmus hatékonyságán, összehasonlításképpen adjuk meg. Az auxFib függvény a számításokat másképp végzi, akkor fog számolni, amikor jön vissza a rekurzióból, ellen-tétben az előzővel, amely akkor számol, amikor megy be a rekurzióba. A fibonacci_ visszatérési értéke pedig az auxFibáltal meghatározott két-elemű tuple első eleme lesz.

fibonacci_ :: (Eq a, Num a, Num b) => a -> b fibonacci_ n = t1

where

4.5. Mintaillesztés 49 (t1, t2) = auxFib n

auxFib :: (Eq a, Num a, Num b) => a -> (b, b) auxFib n

| n == 0 = (0, 1)

| otherwise = (b, a + b) where

(a, b) = auxFib (n - 1)

Fontosnak tartjuk megjegyezni, hogy egy hs állományon belül ugyanolyan nevű függvényeket akkor használhatunk, ha azok láthatósági zónája nincs átfedésben. A fenti kódsorokban azauxFibfüggvények láthatóságát a tör-delés segítségével korlátoztuk, ezért a hívó függvények tudni fogják, melyiket értékeljék ki.

4.5. Mintaillesztés

Az argumentumok mintaillesztése, apattern matchingazt jelenti, hogy a függvény értékét az a kifejezés határozza meg, amelyre a függvényparaméte-rek egy megadott minta alapján illeszkednek. A mintaillesztést és a feltételek megadását lehet együttesen is alkalmazni. Mintaillesztést használva egy függvénytörzset egyszerűbbé, olvashatóbbá lehet tenni, ezek használata kü-lönösen jellemző a funkcionális programozási nyelvek körében.

4.7. feladat Írjunk egy Haskell-függvényt, amely meghatározza egy szám számjegyeinek összegét.

szOsszeg :: Integral a => a -> a szOsszeg 0 = 0

szOsszeg x = mod x 10 + szOsszeg (div x 10)

> szOsszeg 1234 10

4.8. feladat Írjunk egy Haskell-függvényt, amely meghatározza egy szám számjegyeinek szorzatát.

szSzorzat :: Integral a => a -> a szSzorzat 0 = 0

szSzorzat x = auxSz 1 x where

auxSz res 0 = res

auxSz res x = auxSz (res * mod x 10) (div x 10)

> szSzorzat 1234 24

Mindkét függvény mintaillesztést alkalmaz, és két-két esetet vizsgál. A függ-vénytörzsek első sorai azt az esetet kezelik, amikor a bemenet nulla, a második sorok pedig általános esetben adják meg, hogy mit kell kiértékelni.

AszOsszegfüggvény a számjegyek összegzését akkor számítja ki, ami-korjön vissza a rekurzióból, ezért a triviális esetben a függvény visszatérési értéke 0. A szSzorzat másképp jár el, akkor szoroz, amikor megy be a rekurzióba. A szSzorzat, amikor a bemenete nem nulla, a lokálisan meg-adott auxSz függvényt fogja kiértékelni. A tulajdonképpeni számításokat tehát az auxSzvégzi. Az auxSz úgy kerül meghívásra, hogy az első para-méterének értékét1-re inicializáltuk, amely minden egyes rekurzív híváskor aktualizálódik, és a részszorzat aktuális értékét fogja jelölni. Az eredmény tehát a rekurzió legalsó szintjén, az auxSz első argumentumában meg lesz határozva, így a triviális esetben ezt az értéket fogja visszaadni az auxSz.

Vegyük észre, hogy a függvények nem kezelik a negatív bemeneti érté-ket, az olvasóra bízzuk, hogy módosítsa úgy a függvénytörzseérté-ket, hogy azok ne eredményezzenek futási hibát negatív bemenetre.

Megjegyezzük, hogy azszOsszegfüggvény azszSzorzatfüggvénynél alkalmazott technikával is megoldható, illetve a szSzorzatesetében is al-kalmazhattuk volna azt a technikát, hogy akkor számolunk, amikor jövünk vissza a rekurzióból. Ezekben az esetekben sincs hatékonyságbeli különbség a két módszer között. Gyakorlás végett írjuk meg őket.