Kihagyás

9. gyakorlat

Tuple adatstruktúra

A Tuple lehetővé teszi különböző típusú adatok csoportosítását egyetlen objektumban, így több értékkel tud visszatérni egy függvény vagy akár egyetlen paraméterben több értéket fogadhat is. Ugyanazon Tuple struktúra elemeinek típusa eltérhet, maximális mérete pedig 22 lehet, azonban a nagy elemszámú Tuple-k kezelése bonyolult és nehezen kezelhető kódot eredményezhet, ezért használata nem ajánlott. Az adatstuktúra elemeit a következő szintaxissal érhetjük el: ._1, ._2, ... ._22.

1
2
3
4
5
6
7
def interval(num: Double): (Double, Double) = {
  (math.floor(num), math.ceil(num))
}

def pair(pair: (Int, Int) ): Int = {
  math.abs(pair._2 - pair._1)
}

A Tuple-k egyik kényelmes használati módja a destrukturálás, ami lehetővé teszi, hogy a tuple elemeit közvetlenül, esetünkben immutable(!) változókhoz rendeljük. Ez különösen hasznos mintaillesztés alkalmazása esetén.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
def which(tuple1: (String, Double), tuple2: (String, Double) ): (String, Double) = {
  val (name1, weight1) = tuple1
  val (name2, weight2) = tuple2

  (weight1, weight2) match {
    case (0, 0) => ("", 0)
    case (a, b) if a > b => (name1, weight1)
    case _ => (name2, weight2)
  }
}

Option

Mivel tisztán funkcionális programozásban nem használunk kivételeket (hiszen ez mellékhatása lenne a programnak), ezért a hibakezelés típus szinten történik. Az egyik alapvető eszköz erre az Option típus, amely két lehetséges értéket vehet fel:

  • Some(value): ha létezik eredmény

  • None: ha nincs értelmezhető eredmény

Az Option segítségével a függvények biztonságosan jelezhetik, hogy lehet, hogy nem tudnak érvényes eredményt visszaadni, például egy keresés sikertelen volt, vagy egy számnak nincs értelmezhető négyzetgyöke, ahogyan az alábbi példában is történik. Ebben az esetben az Option egy Some(Tuple)-lel fog visszatérni ha van eredmény vagy None-nal, ha nincs.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
object Main {

  def quadratic(abc: (Double, Double, Double) ): Option[(Double, Double)] = {
    val (a, b, c) = abc
    val d = b * b - 4 * a * c

    if (d < 0)
      None
    else
      val x1 = (-b + Math.sqrt(d)) / (2 * a)
      val x2 = (-b - Math.sqrt(d)) / (2 * a)
      Some((x1, x2))
  }

  def main(args: Array[String]): Unit = {
    quadratic(11, -5, 6) match {
      case Some((x1, x2)) => println("Roots: " + x1 + " " + x2)
      case None           => println("No real roots")
    }
  }
}

A case class

Többek között az olvashatóság és a típusellenőrzés érdekében a Tuple helyett használhatunk case class-t is. Ez egy speciális osztály Scala-ban, amely automatikusan generál számos hasznos metódust (pl. toString, equals, hashCode, copy).

A case class adattagjai alapértelmezés szerint immutable (val), így a példányok létrehozás után nem módosíthatók. Emiatt az osztály definíciójában nem szükséges külön kiírni a val kulcsszót (ahogyan azt tettük a sima OOP class esében). A case class különösen jól használható mintaillesztésre is.

A case class használatának másik előnye, hogy a példányosítás is egyszerűbb: nem szükséges a new kulcsszó. Ezen kívül, míg egy Tuple csak pozíció alapján azonosítja az adatokat, a case class explicit mezőnevekkel rendelkezik, ami olvashatóbbá és karbantarthatóbbá teszi a kódot. Emellett a case class-ok típusellenőrzés szempontjából is erősebbek, mivel lehetőség van minden mező külön típusának meghatározására.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
case class Point(zh1: Int, zh2: Int, zh3: Int) {

  def score(): Int = {
    if (zh1 == 0 || zh2 == 0 || zh3 == 0)
      0
    else
      zh1 + zh2 + zh3
  }
}

object Main {

  def score(point: Point): Int = {
    point match {
      case Point(0, _, _) => 0
      case Point(_, 0, _) => 0
      case Point(_, _, 0) => 0
      case Point(a, b, c) => a + b + c
    }
  }

  def main(args: Array[String]): Unit = {
    val point = Point(3, 2, 1)
    println(point.score())
    println(score(point))
  }
}

A lista típus

Az immutabilis kollekciók használata funkcionális programozásban lehetővé teszi a biztonságosabb kódot, mivel ezek a kollekciók megakadályozzák az adatok nem szándékos módosítását (pl. egy hibás indexelést követően). A List az egyik leggyakrabban használt immutable kollekció Scala-ban, mert támogatja a rekurziót, a mintaillesztést és a láncolható műveleteket. Imperatív megközelítéssel a lista bejárása tipikusan for vagy foreach ciklus történik. Esetünkben ez most nem áll rendelkezésre, hiszen nem egy indexelhető adatstruktúráról beszélünk. Így ahhoz, hogy a listát bejárjuk (és az elemeken/elemekkel valamilyen műveletet végezzünk) szükség van a lista úgynevezett head-tail felbontására. A cél az, hogy egy listát az első elemére (head) és a maradék részére (tail) bontsunk.

Először nézzük meg, hogyan tudunk listákat létrehozni. A lista típusos, tehát minden egyes lista ugyanolyan típusú elemeket tartalmaz. A Nil egy előre definiált konstans, ami az üres listát reprezentálja (azaz List.empty). A listát elemenként is meg tudjuk építeni a :: (ez az úgynevezett cons) operátorral (az elem hozzáadására szolgál egy lista elejére). Listák konkatenálását (összefűzését) a ::: operátor végzi.

1
2
3
val list1 = List("alma", "banan", "citrom")
val list2 = List(1, 2, 3)
val list3 = 4 :: 5 :: Nil

A listák manuális készítése helyett alkalmazhatunk beépített listageneráló függvényeket:

  • List.fill(n)(value): n-szer ismételt érték

  • List.range(start, end): számtartomány

  • List.range(start, end, step): számtartomány lépésközzel

Egy lista a következő beépített tulajdonságokkal rendelkezik:

  • head: visszaadja a lista első elemét

  • tail: visszaadja a listát az első elem kivételével

  • last: visszaadja a lista utolsó elemét

  • init: visszaadja a listát az utolsó elem kivételével

  • isEmpty: igazzal tér vissza, ha üres a lista

  • length: visszaadja a lista hosszát

  • reverse: megfordítja a listát

További számos beépített listakezelő függvényt is meg fogunk ismerni a későbbiekben, viszont fontos, hogy a listákat a beépített tulajdonságok és függvények nélkül is képesek legyünk kezelni. Ehhez listabejárásra van szükség a lista head-tail felbontásával, illetve mintaillesztés alkalmazásával.

1
2
3
4
5
6
def listSize(list: List[Int]): Int = {
  list match {
    case Nil => 0
    case head :: tail => 1 + listSize(tail)
  }
}

Mivel a listákat rekurzívan járjuk be, így itt is alkalmazhatunk tail-recursive optimalizálást.

1
2
3
4
5
6
7
@tailrec
def listSum(list: List[Int], res: Int): Int = {
  list match{
    case Nil => res
    case head :: tail => listSum(tail, head + res)
  }
}

Alkalmazzunk case class-t annak érdekében, hogy két listát páronként összefűzzük.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
case class Pair(first: Int, second: Int) {
}

def listPair(list1: List[Int], list2: List[Int]): List[Pair] = {
  (list1, list2) match {
    case (h1 :: t1, h2 :: t2) => Pair(h1, h2) :: listPair(t1, t2)
    case (Nil, _) => List.empty
    case (_, Nil) => List.empty
  }
}

Szintén alkalmazhatunk TCO-t:

1
2
3
4
5
6
7
8
@tailrec
def listPairRec(list1: List[Int], list2: List[Int], res:List[Pair]): List[Pair] = {
  (list1, list2) match {
    case (h1 :: t1, h2 :: t2) => listPairRec(t1, t2, Pair(h1, h2) :: res)
    case (Nil, _) => res
    case (_, Nil) => res
  }
}

Gyakorló feladatok

Feladat

Készíts egy pure függvényt, amely egy egész számokat tartalmazó listát megfordít. Valósítsd meg a megoldást tail-recursive módon is. A megoldásodban ne használj beépített listakezelő függvényeket vagy tulajdonságokat. 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ámokat tartalmazó listából előállít egy olyan listát, amely csak a lista pozitív elemeit tárolja. Valósítsd meg a megoldást tail-recursive módon is. A megoldásodban ne használj beépített listakezelő függvényeket vagy tulajdonságokat. 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 kiszámolja egy lebegőpontos számokat tartalmazó lista elemeinek az átlagát. A megoldásodban ne használj beépített listakezelő függvényeket vagy tulajdonságokat. Üres lista esetén térjünk vissza None-nal, egyébként Some-mal, amely az átlagot tárolja. A megoldáshoz használhatsz segédfüggvény(eke)t, ha szükséges.

Feladat

Készíts egy case class-t, amely komplex számokat reprezentál (valós számokból álló pár). Készítsd el az alábbi függvényeket (case class-ban és object-ben külön-külön, tehát összesen 4 pure függvény):

  • osszead - két komplex számot vár paraméterül, és az összegükkel tér vissza: (r_1, i_1) + (r_2, i_2) = (r_1 + r_2, i_1 + i_2)

  • szoroz - két komplex számot vár paraméterül, és a szorzatukkal tér vissza: (r_1, i_1) * (r_2, i_2) = (r_1 * r_2 - i_1 * i_2, r_1 * i_2 + r_2 * i_1)

Case class helyett Tuple segítségével is valósítsd meg a statikus metódusokat.


Utolsó frissítés: 2025-04-23 15:54:56