Skalárny súčin vektorov v Scale

Ú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 Listov Doubleov . Funkcia pre skalárny súčin (dot product) má na vstupe dva Listy 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 zipnúť 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 yielde.

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

Pridaj komentár

Vaša e-mailová adresa nebude zverejnená. Vyžadované polia sú označené *