Úvod
Scala vám radostne umožní programovať gulášom paradigiem: jednu vec môžete napísať imperatívne ako starý Javák, alebo zvoliť čisto funkcionálny prístup, alebo ich podľa potreby kombinovať.
Zoberme si akademický algebraický príklad scalarneho, pardón, skalárneho súčinu dvoch vektorov.
Spomienka zo strednej školy: ak máme dva vektory, skalárny súčin získame vynásobením prislúchajúcich zložiek, ktoré následne sčítame. Na príklade:
v = (1, 2)
w = (3, 4)
v * w = (1 * 3) + (2 * 4) = 11
Načo je to dobré? Napr. môžeme zistiť, či sú dva vektory na seba kolmé (vtedy a len vtedy majú skalárny súčin rovný nule).
Ako na to v Scale?
Vektory zareprezentujme jednoduchými zoznamami reálnych čísiel, teda List
ov Double
ov . Funkcia pre skalárny súčin (dot product) má na vstupe dva List
y a vráti jedno reálne číslo:
def dotProduct(v : List[Double], w : List[Double] ) : Double = {
...
}
Testovať to môžeme v jednoduchom objekte
object VectorTest {
def dotProduct(v : List[Double], w : List[Double] ) : Double = {
...
}
def main(args: Array[String]) {
println(dotProduct(List(1, 2), List(3, 4)))
}
}
Verzia 1 – javistická
Javisti, pascalisti a céčkaristi a iná imperatívna chasa by uvažovala nasledovne:
- vytvorím pomocnú premennú, do ktorej budem nasčítavať jednotlivé súčiny
- vo
for
cykle preleziem od 0 po dĺžku vektora (predpokladám, že vektory majú rovnakú dĺžku, inak skalárny súčin nemá zmysel) - vytiahnem
i
-te položky, vynásobím ich a prirátam do pomocnej premennej - výsledkom je hodnota pomocnej premennej
V Scale:
def dotProduct(v : List[Double], w : List[Double] ) : Double = {
var sum = 0
for (i <- 0 until v.length) {
sum += v(i) * w(i)
}
return sum
}
Vylepšiť to môžeme o dva aspekty:
- v Scale je
return
nepovinný: výsledkom funkcie je vyhodnotenie posledného výrazu - mali by sme overiť, či sú vektory naozaj rovnakej dĺžky. Na to využijeme zabudovanú funkciu
require()
, ktorá overí splnenie vstupných podmienok a v prípade zlyhania vyhodí výnimku.
Výsledok:
def dotProduct(v : List[Double], w : List[Double] ) : Double = {
require(v.length == v.length)
var sum = 0
for (i <- 0 until v.length) {
sum += v(i) * w(i)
}
sum
}
Verzia 2 – zoznamová
Úvaha pôjde nasledovným smerom:
- zoznam v Scale má metódu pre sčítavanie prvkov
- tie prvky sú v1 + w1, v2 + w2, …, vn + wn
- získame ich tým, že vyrobíme zoznam dvojíc prvkov…
- … a tie následne po dvojiciach ponásobíme
Príklad:
val v = List(1, 2)
val w = List(3, 4)
V Scale je možné zobrať dve kolekcie a zip
núť ich: to znamená vytvoriť zoznam, ktorý obsahuje dvojicu prvých prvkov, potom dvojicu druhých prvkov atď.
v.zip(w)
Výsledkom jest zoznam tíc (tuples):
List((1, 3), (2, 4))
Z neho potrebujeme zoznam:
List(1 * 3, 2 * 4)
Môžeme na to využiť for
cyklus!
Podivný for
cyklus
Na rozdiel od klasických imperatívnych jazykov nie je for
cyklus len spôsob, ako opakovať n krát daný kus kódu. V Scale môže byť for
výrazom, ktorý vracia hodnotu!
Skvele funguje na prípady, keď chcete vytvoriť zoznam pomocou predpisu. Pythonisti poznajú list comprehensions a toto je v Scale ich analógia.
def patParnychCisiel = for (i <- 1 to 5) yield i * 2
Výsledkom je:
scala.collection.immutable.IndexedSeq[Int] = Vector(2, 4, 6, 8, 10)
Inak povedané, zoznam čísiel od 2 po 10.
Príkaz yield
vypľuje ďalší prvok do výsledného zoznamu na základe predpisu. V tomto prípade sa postupne do zoznamu _yield_uje 1 * 2
, 2 * 2
, 3 * 2
atď.
V našom skalárnom sučine budeme _yield_ovať súčin dvoch prvkov v_i
a w_i
. Inými slovami, potrebujeme urobiť prevod:
List((1, 3), (2, 4)) -------> List(1 * 3, 2 * 4)
S využitím for
/yield
je to prosté:
val z = for (ntica <- v.zip(w)) yield ntica._1 * ntica._2
Postupne prechádzame cez zipnutý zoznam, a každý prvok postupne priradíme do premennej ntica
, ktorá je typu tuple, čo je scalácka špeciálna štruktúra pre … _n_tice. K prvému prvku sa dopracujeme cez vlastnosť _1
a k druhému cez _2
.
Na zozname z
môžeme teraz rovno zavolať metódu sum
a máme výsledok.
z.sum
Celý kód
Celý kód môže vyzerať takto:
def dotProduct(v : List[Double], w : List[Double] ) : Double = {
val z = for (ntica <- v.zip(w)) yield ntica._1 * ntica._2
z.sum
}
Alternatívne môžeme použiť jednoriadkovú verziu:
def dotProduct(v : List[Double], w : List[Double] ) : Double = {
( for (ntica <- v.zip(w)) yield ntica._1 * ntica._2 ).sum
}
Tuple (tice) môžeme vo for
cykle použiť aj na odbaľovanie prvkov:
( for ( (e1, e2) <- v.zip(w)) yield e1 * e2 ).sum
Všimnime si, že namiesto premennej ntica
vieme použiť priamy zápis tice tvorenej z dvoch prvkov e1
a e2
, na ktoré sa potom vieme priamo odkazovať v yield
e.
Verzia 3 – funkcionálne zoznamová
Skúsme si celú transformáciu napísať ešte raz:
List(1, 2) List(3, 4)
=============================
|
| zip
|
\=/
List((1, 3), (2, 4))
|
| ponásob dvojice
|
\=/
List(3, 8)
|
| sum
|
\=/
11
Krok “ponásob dvojice” je transformácia zoznamu, kde každý prvok pôvodného zoznamu (typu tuple
) zmeníme na jeden Double
tým, že vynásobíme prvý prvok dvojice druhým prvkom dvojice. Inými slovami, každú ticu pôvodného zoznamu namapujeme na číslo v novom zozname.
Mapovanie zoznamu môžeme zapísať v Scale pomocou funkcie map
, kde uvedieme predpis:
zipnutyZoznam.map( prvok => prvok._1 * prvok._2 )
Kompletnú transformáciu vieme napísať nádherne:
(v.zip(w)).map( prvok => prvok._1 * prvok._2 ).sum
---------- ----------------------------- ---
zipuj ponásob dvojice |-sčítaj
Kompletný kód:
def dotProduct(v : List[Double], w : List[Double] ) : Double = {
(v.zip(w)).map( prvok => prvok._1 * prvok._2 ).sum
}
Pretekári v súťaži o najkrajtší kód sa môžu spoľahnúť na fakt, že:
- metódu môžeme volať aj bez bodky,
- ak má metóda len jeden parameter, môžeme vynechať zátvorky,
- návratový typ metódy možno odvodiť z výsledku
Výsledný kód potom bude ultimátna skrátenina:
def dotProduct(v : List[Double], w : List[Double] ) = v zip w map( e => e._1 * e._2 ) sum