Kihagyás

10. gyakorlat

Generikus függvények

A polimorfizmus lehetővé teszi, hogy ugyanaz a kód több különböző adattípuson is működjön. Scala támogatja a polimorf típusokat, amelyek segítenek az általánosítható, újrafelhasználható és típusbiztos kód írásában. A nyelvben a generikus típusok a polimorfizmus egy formáját képviselik, ahol a típusokat paraméterként adjuk meg, így egyetlen osztály vagy függvény különböző adattípusokkal is működhet anélkül, hogy új implementációra lenne szükség.

Azaz ha minden típushoz külön osztályt vagy függvényt kellene írni, az rengeteg felesleges kódot eredményezne. Generikus típusokkal egyetlen kódbázis kezel különböző adattípusokat.

1
2
3
4
5
6
  def genericListSize[T](list : List[T]): Int = {
    list match {
      case Nil => 0
      case _ :: t => 1 + genericListSize(t)
    }
  }

Ebben a lista méretét meghatározó példában a függvény nem végez semmilyen műveletet a T típusú elemekkel - csupán megszámolja őket. A típus tehát itt nem fontos, mert a függvény kizárólag a lista struktúráját használja, nem az elemek viselkedését. Viszont:

A típusparaméter nem garantál viselkedést

Ha egy függvény A típusú paramétert kap, nem feltételezhetjük, hogy azon bármilyen művelet (pl. +, ==) elérhető lesz. Ha például szeretnénk a + jellegű műveletet használni az A típuson, akkor a megfelelő típusosztály hozzáadására is szükség van.

Tehát ahhoz, hogy egy generikus lista elemeit összegezzük - itt konkrétan A típusú elemeket szeretnénk összeadni - szükségünk van a Numeric típusosztályra, ami definiálja milyen műveletet végezhetünk A-val. Ez a típusosztály (https://www.scala-lang.org/api/3.x/scala/math/Numeric.html) biztosít számos függvényt és tulajdonságot, amit fel tudunk használni a polimorf függvényeinknél, többek között:

  • zero: A: a típushoz tartozó "nulla" érték

  • plus(x: A, y: A): A: két A típusú érték összeadása

  • times(x: A, y: A): A: két A típusú érték szorzása

Ahhoz, hogy ezeket használni tudjuk a függvényünk scope-jában, szükség van egy explicit típusosztály-példányra, ezt a summon[Numeric[A]] segítségével tudjuk előhívni.

1
2
3
4
5
6
7
  def genericListSum[A: Numeric](list: List[A]): A = {
    val numeric = summon[Numeric[A]]
    list match {
      case Nil => numeric.zero
      case h :: t => numeric.plus(h, genericListSum(t))
    }
  }

További fontos típusosztályok:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
  def maxElement[A: Ordering](list: List[A]): Option[A] = {
    val ordering = summon[Ordering[A]]

    @tailrec
    def helper(currentList: List[A], currentMax: A): A = {
      currentList match {
        case Nil => currentMax
        case currentHead :: currentTail =>
          val nextMax =
            if (ordering.gt(currentHead, currentMax))
              currentHead
            else
              currentMax
          helper(currentTail, nextMax)
      }
    }

    list match {
      case Nil => None
      case head :: tail => Some(helper(tail, head))
    }
  }

Egy függvény több generikus típussal is dolgozhat. Például az alábbi függvény két generikus típusú listát vár, (T1 és T2 eltérő típus is lehet), majd visszatér párokkal, amennyiben a két lista hosssza megegyezik.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
  def createPairs[T1, T2](list1: List[T1], list2: List[T2]): Option[List[(T2, T1)]] = {

    def loop(l1: List[T1], l2: List[T2]): List[(T2, T1)] = {
      (l1, l2) match {
        case (Nil, Nil) => Nil
        case (h1 :: t1, h2 :: t2) => (h2, h1) :: loop(t1, t2)
        case _ => Nil
      }
    }
      val res = loop(list1, list2)
      res match {
        case Nil => None
        case _ => Some(res)
      }
  }

Magasabb rendű függvények

A magasabb rendű függvények (higher-order functions) olyan függvények, amelyek egy vagy több függvényt fogadnak bemenetként, vagy egy függvényt adnak vissza kimenetként. A funkcionális programozás egyik alapvető jellemzője, hogy a függvények nemcsak adatokkal dolgozhatnak, hanem más függvényeket is "kezelnek". Az alábbi függvények iterálható típusokon alkalmazhatóak, mint például amilyen a lista típus is.

Mivel a magasabb rendű függvények alkalmazásakor anonim (lambda) függvényeket használunk, ezért nézzük meg hogyan tudunk ilyen függvényt írni. Ehhez a => operátorra lesz szükség, pl.:

  • val multiply1: (Int, Int) => Int = (x,y) => {x * y}, de egy kifejezés esetén a {} elhagyható, és tovább is egyszerűsíthetjük

  • val multiply2 = (x: Int, y: Int) => x * y, amelyeket aztán később meghívhatunk: multiply1(2,3) és multiply2(2,3)

Ahhoz, hogy egy saját, magasabb rendű függvényt írjunk, a függvény fejlécében kell meghatároznunk annak a függvénynek a szignatúráját, amelyet paraméterként át szeretnénk adni:

1
2
3
4
5
6
  def applyFunc(func: Int => Int, list: List[Int]): List[Int] = {
    list match {
      case Nil => List.empty
      case head :: tail => func(head) :: applyFunc(func, tail)
    }
  }

Ezt követően a függvény meghívható a következő módon:

1
2
  val add = (x: Int) => x + 1
  println(applyFunc(add, List(1, 2, 3)))

Tekintsünk át néhány magasabb rendű függényt, amit listakezeléshez használhatunk:

  • map: minden egyes elemre alkalmaz egy adott függvényt, és az eredményt egy új adatstruktúrában adja vissza, pszeudokóddal:

  • filter: kiválasztja azokat az elemeket, amelyek megfelelnek egy adott feltételnek, és egy új adatstruktúrát ad vissza, amely csak a feltételt teljesítő elemeket tartalmazza

  • fold: egy kezdőértékkel és egy összegző művelettel "összevonja" az adatstruktúra elemeit, és az eredményként egyetlen értéket ad vissza

  • reduce: nincs kezdőérték, és az első két elemet a művelettel összevonja, majd ezután az eredmény a következő elemre kerül alkalmazásra, és így tovább egészen a lista végéig

  • zip: két adatstruktúrát kombinál és egy olyan új adatstruktúrát ad vissza, amely párokat (tuple-öket) tartalmaz, ahol az azonos indexű elemek állnak párba

  • zipWithIndex: egy adatstruktúra elemeihez indexet rendel, és egy új, párokból álló adatstruktúrát ad vissza, ahol az első komponens az eredeti elem, a második pedig az index

  • exists: ellenőrzi, hogy legalább egy elem megfelel-e a megadott feltételnek

  • forall: ellenőrzi, hogy minden egyes elem megfelel-e a megadott feltételnek

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
  val list = List(1, 2, 3, 4, 5)
  println( list.map(elem => elem * elem) )
  println( list.filter(elem => elem % 2 == 0) )
  println( list.fold(100)((accumulator, elem) => accumulator + elem) )
  println( list.reduce((accumulator, elem) => accumulator + elem) )

  val list2 = List('A', 'B', 'C', 'D')
  println( list2.zip(list) )
  println( list2.zipWithIndex )
  println( list.exists( elem => elem > 3) )
  println( list.forall(elem => elem > 3) )

Vector, Set és a Map

A List-en kívül további immutable kollekciók is elérhetőek a nyelvben. A nyelv érdekessége, hogy a kollekciók mutable verzióban is elérhetőek.

  • Vector: List-szerű adattárolást biztosít, de indexelhető, így kifejezetten hasznos ha sokszor történik egy adott indexről az olvasás. Fontos, hogy mivel immutable kollekcióról beszélünk, ezért az updated(index, value) művelet egy új vektort hoz lére. A :+ művelettel a kollekció végére, a +: művelettel a kollekció elejére tudunk új elemet beszúrni - de ezek szintén egy új vektor létrejöttét eredményezik.
1
2
3
4
5
6
7
8
9
  val v1 = Vector(10, 20, 30)
  val v2 = v1.updated(0, 111)
  val v3 = v1 :+ 40
  val v4 = 0 +: v1
  println(v1(0))
  println(v2)
  println(v3)
  println(v4)
  println(v1)
  • Set: Egy immutable halmaz, ami csak egyedi elemeket tárol, így egy elem beillesztéskor eldobja a duplikációkat (ha van). A beszúrás, kivétel művelet szintén egy új halmaz létrejöttét eredményezik.
1
2
3
4
5
6
7
  val s1 = Set(1, 2, 3, 3)
  val s2 = s1 + 4 
  val s3 = s1 - 1
  println(s1.contains(2))
  println(s2)
  println(s3)
  println(s1)
  • Map: egy kulcs–érték párokból álló immutable gyűjtemény, azaz asszociatív tömb vagy dictionary. A kulcs egyedi, és ennek alapján lehet frissíteni, törölni, hozzáadni - szintén egy új asszociatív tömb jön létre az adott műveletnél. A lekérdezés egy Option-nel tér vissza, mivel előfordulhat, hogy az adott kulcsot nem tartalmazza a kollekció.
1
2
3
4
5
6
7
8
9
  val m1 = Map("One" -> 1, "Two" -> 2)
  val m2 = m1.updated("Two", "One")
  val m3 = m1 + ("Three" -> 3)
  val m4 = m1 - "Two"
  println(m1.get("One"))
  println(m2)
  println(m3)
  println(m4)
  println(m1)

Gyakorló feladatok

Feladat

Készíts egy pure generikus függvényt, amely egy listát és egy beszúrandó elemet vár. A függvény szúrja be az elemet a lista minden második elemét követően (tehát pl. [1, 2, 3, 4] és 5-ös érték esetén: [1, 2, 5, 3, 4, 5]). A megoldáshoz használhatsz segédfüggvény(eke)t, ha szükséges.

Feladat

Készíts egy pure függvényt, amely egy egész számról eldönti, hogy prímszám-e. Ennek megállapításához ne használj beépített függvényeket. A függvényt úgy írd meg, hogy átadható legyen a filter magasabb rendű függvénynek. A megoldáshoz használhatsz segédfüggvény(eke)t, ha szükséges.

Feladat

Készíts egy pure függvényt, amely a forall magasabb rendű függvény működését valósítja meg, de specializálva van egész számokra. Azaz a függvény döntse el, hogy az adott feltételnek a paraméterben átadott össszes elem eleget tesz-e. A megoldáshoz ne használj beépített függvényeket, de használhatsz segédfüggvény(eke)t, ha szükséges.

Feladat

Készíts egy pure függvényt, amely egy szöveget vár bemenetként, és visszadja a benne szereplő angol ABC karaktereinek előfordulási gyakoriságát. Az eredményt egy immutable Map-ban tárold el. A megoldáshoz használhatsz segédfüggvény(eke)t, ha szükséges.


Utolsó frissítés: 2025-05-03 15:51:15