Scala kogumite kasutamine
Tutvume nüüd tööd Scala Collections raamistikuga. Nende kohta saab pikemalt lugeda dokumentatsioonist. Lugeda tasub õpiku 9. peatükk, eriti jaotis 9.4, kus on palju näited listide kõrgemat järku funktsioonide kohta. Soovitame nii palju kui võimalik harjutada sellist stiili, mida te mujal ei ole kasutanud.
Scalal on põhimõtteliselt igast asjast muudetavad ja muutmatud versioonid, vastavalt pakettides scala.collection.mutable ja scala.collection.immutable. Neil on ühised liidesed pakettides scala.collection. On kolm peamist liiki andmestruktuure:
Ja nad kõik implementeerivad Iterable liidese meetodi iterator, mille kaudu on defineeritud kõik meile juba tuttavad kõrgemat järku funktsioonid (map, filter, fold,...).
Soojendus: Seq liidese kasutamine
Selle kursuse jaoks piisab kasutada lihtsalt ülemliidesed Seq, Set ja Map. Kui peaks midagi muud vaja minema anname otse teada. Kui on väga suur soov midagi muud kasutada, siis peab ise selle kohta uurida. Ülemklasside liidese kasutamine tähendab, et kood töötab kõikide konkreetsete implementatsioonide jaoks. Esimese asjana võiks proovida defineerida selline FizzBuzz implementatsioon, mis töötab kõikidel Scala erinevatel järjenditel:
def main(args: Array[String]): Unit = { fizzbuzz(List(0,1,2,3,4,5)) fizzbuzz(Array(1,2,3,4)) fizzbuzz(0 to 100) }
For-avaldised
Näiteülesanne
Scala for-avaldised ei ole ainult Unit tüüpi, vaid võivad väärtust genereerida märksõnaga yield:
case class Suusataja(nimi: String, pikkus: Int) { val suusakepiPikkus: Int = (0.85 * pikkus).toInt } val suusatajad = Seq( Suusataja("Andrus", 182), Suusataja("Anna", 168), Suusataja("Donald", 190), Suusataja("Jüri", 164), Suusataja("John", 176), Suusataja("Hillary", 165), Suusataja("Kristiina", 168), Suusataja("Mari", 90) )
Kirjutage funktsioon eligibleForJuniorContest(maxPikkus: Int): Seq[String]
, mis tagastab kõikide suusatajate nimed (üleval defineeritud järjendist), kes võivad võistelda võistlustel, kus osavõtt on piiratud suusakepi pikkuse järgi.
Harjutusülesanne 1
Järgmisena väike raamatu klass, kus raamatul on pealkiri ja autorid.
case class Book(title: String, authors: Seq[String]) val books: Seq[Book] = Seq( Book("Structure and Interpretation of Computer Programs", Seq("Abelson, Harold", "Sussman, Gerald J.")), Book("Principles of Compiler Design", Seq("Aho, Alfred", "Ullman, Jeffrey")), Book("Programming in Modula2", Seq("Wirth, Niklaus")), Book("Introduction to Functional Programming", Seq("Bird, Richard", "Wadler, Phil")), Book("The Java Language Specification", Seq("Gosling, James", "Joy, Bill", "Steele, Guy", "Bracha, Gilad")), Book("Effective Java", Seq("Bloch, Joshua")), Book("Java Puzzlers", Seq("Bloch, Joshua", "Gafter, Neal")), Book("Programming in Scala", Seq("Odersky, Martin", "Spoon, Lex", "Venners, Bill")) )
Defineerige meetod allBooksBy(name: String): Seq[String]
, millega saab järjendist books üles otsida etteantud autori kõigi teoste pealkirjad.
Proovi defineerida ka variant, mis lubab otsida nime osa ("Bloch") või/ja case insensitive ("bloch"). Selleks vihje: exists
.
Harjutusülesanne 2
Meil on nüüd lihtsalt selline case klass:
case class Person(name: String, interest: String)
Defineerige järgmise signatuuriga funktsioon:
matches(g1: Seq[Person], g2: Seq[Person]): Seq[String]
Funktsioon peaks väljastama sõnena kõik paarid, kus üks inimene on rühmast g1 ja teine inimene rühmast g2 ning nendel inimestel on sama huvi.
Näiteks:
val group1: Seq[Person] = Seq( Person("Peeter", "singing"), Person("Julia", "football"), Person("Pelle", "dancing") ) val group2: Seq[Person] = Seq( Person("Anna", "football"), Person("Elsa", "dancing"), Person("Carmen", "dancing"), Person("Ivan", "singing"), ) matches(group1, group2)
Tulemus peaks olema võrdne järgmise järjendiga:
Seq("Peeter and Ivan", "Julia and Anna", "Pelle and Elsa", "Pelle and Carmen")
Harjutusülesanne 3
Aadu tahab avada restorani, kus pakutakse päevapraena Õiget Eesti Toitu! Õige toit koosneb teadupärast kolmest komponendist: liha, kartul ja toorsalat. Aita Aadut menüü genereerimisel. Defineerige järgmise signatuuriga funktsioon:
kombineeri(lihaollus: Seq[String], salatid: Seq[String]): Seq[String]
Funktsioon peaks väljastama kõik võimalikud kombinatsioonid järgmise mustri järgi (sõnena): “<liha> + <kartul> + <salat>”. Liha ja salati valik on antud meetodi parameetrina ning kartul on alati kas "praekartul" või "keedukartul".
Näiteks:
val salatid = Seq("porgandisalat", "kapsasalat", "vinegrett") val lihaollus = Seq("kanakoib", "kotlett")
puhul
kombineeri(lihaollus, salatid)
tulemus peaks olema võrdne (aga elementide järjekord) järgmise järjendiga:
Seq("kanakoib + praekartul + porgandisalat", "kanakoib + praekartul + kapsasalat", "kanakoib + praekartul + vinegrett", "kanakoib + keedukartul + porgandisalat", "kanakoib + keedukartul + kapsasalat", "kanakoib + keedukartul + vinegrett", "kotlett + praekartul + porgandisalat", "kotlett + praekartul + kapsasalat", "kotlett + praekartul + vinegrett", "kotlett + keedukartul + porgandisalat", "kotlett + keedukartul + kapsasalat", "kotlett + keedukartul + vinegrett")
Harjutusülesanne 4
Defineerige järgmise signatuuriga funktsioon:
addnext(xs: Set[Int]): Seq[Int]
Funktsioon peaks väljastama täisarvude järjendi, kus iga hulga xs elemendi x kohta on tagastatavas järjendis arv x ning arv x+1.
Näiteks:
addnext(Set()) == Seq() addnext(Set(1)) == Seq(1, 2) addnext(Set(1, 7, 15)) == Seq(1, 2, 7, 8, 15, 16) addnext(Set(2, 3)) == Seq(2, 3, 3, 4)
Hulgad
Eeldatavasti teate, mis on hulga ja järjendi erinevus, aga Scala puhul on peamine tähele panna, et enamus kõrgemat järku funktsioonid säilitavad andmestruktuuri. Kas saad ennustada järgmise programmi väljundi?
def sumSquare(coll: Iterable[Int]): Int = coll.map(x => x * x).sum println(sumSquare(Set(-2, 1, 2))) println(sumSquare(List(-2, 1, 2)))
Defineeri nüüd meetod uniqueChars(sisend: String): Seq[Char]
, mis võtab sõne sisse ja tagastab tema kõik tähed (isLetter). Neid võiks teisendada väikseteks tähtedeks (toLower), kordusi võiks eemaldada ja elemendid võiksid esineda tähestikulises järjekorras. (Vihje: Siin on mõned funktsioonid, mida võite proovida kasutada: filter, map, toSet, to(SortedSet), toSeq, distinct, sorted (töötab ainult järjendil), isLetter, toLower...)
Ja nüüd saab lahendada selle aine kullafondi ülesanne. Sõbralik ettevõtja Kersti võttis suveürituste jaoks tööle viis koolilast, kes on kõvasti vaeva näinud. Aga kokku on lepitud nii, et palk makstakse välja hoopis laste vanematele --- niisiis tuleb ühe pere laste palgad liita. Kersti on jõudnud laste palgad juba üles kirjutada, kuid tal on vaja veel implementeerida meetod 'palgaSumma', mis teise argumendis olevate nimede järgi just nende laste palkade summa leiab. Ta on implementeerinud sellise suurepärase meetodi:
def palgaSumma(palgaRaamat: Map[String, Int], pereLapsed: Set[String]): Int = pereLapsed.map(palgaRaamat).sum
Kas selle meetodiga on kõik korras? Kui mitte, siis paranda ära, et järgmine väljakutse annaks õige tulemuse:
def main(args: Array[String]): Unit = { val palgad = Map( "Jüri" -> 140, "Peeter" -> 30, "Jaana" -> 90, "Mirjam" -> 180, "Paul" -> 70, "Agnes" -> 30) println(palgaSumma(palgad, Set("Paul", "Peeter", "Agnes"))) }
Map ja GroupBy
Üks hästi kasulik funktsioon Scala (ja andmebaaside, Java Stream, MS LINQ, jne) kogumiklassides on groupBy. Ta võtab rühmitamise funktsiooni ja tagastab Map rühma esindaja ja rühma liikmete vahel. Oletame, et meil on sõnade list, kus esineb erineva kirjapildiga sõnad, näiteks val input = Seq("LiNux", "Latex", "LaTex", "GNU", "LaTeX", "LinuX", "LaTeX", "Gnu", "Latex", "GNU")
. Järgmine meetod loob sellise funktsiooni, mis iga sõna jaoks tagastab kõiki tema vorme ilma kordusteta:
def findForms(list: Seq[String]): String => Set[String] = { val index = list.toSet[String].groupBy(_.toLowerCase) s => index(s.toLowerCase) }
Me oleks ka saanud lihtsalt index tagastada, aga antud lahendus on selles suhtes robustsem, et töötab näiteks ka järgmise sisendi korral:
val input = Seq("LiNux", "Latex", "LaTex", "GNU", "LaTeX", "LinuX", "LaTeX", "Gnu", "Latex", "GNU") def main(args: Array[String]): Unit = { val formMap = findForms(input) println(formMap("GNU")) }
Moodustame nüüd siis sõnade listist Map, kus seame igale tähele vastavusse temaga algavad sõned, näiteks Set("kala", "koer", "jänes")
puhul on tulemus Map('k' -> Set("kala", "koer"), 'j' -> Set("jänes"))
.
def groupByFirstLetter(strings: Set[String]): Map[Char, Set[String]] = ???
Proovi nüüd teha selline meetod, mis näitab mitu korda iga täht esineb sõnes. Suuri ja väikseid tähti võiks mitte eristada. Siin võiks teha samasugune teisendus nagu üleval formMap puhul, et ka sisendile rakendada toLowerCase.
def freqs(s: String): Char => Int = ???
Esialgu on okei, kui kasutad Scala 2.13-s deprecated meetodeid. Seejärel võid proovi hoiatustest lahti saada kasutades IntelliJ kuvatavaid vihjeid. Lõpuks proovi lahendada kasutades Scala 2.13 groupMapReduce
meetodit, mis on loetavam ja ka efektiivsem.