08 - Vektor3D andThen compose

Láttuk előadáson, hogy mik is azok a case classok, most ezt fogjuk gyakorolni.

a feladat

Készítsünk egy Vektor3D case classt, mely egy 3-dimenziós Double vektort (mármint egy 3D irányvektort) tárol!

  • A mezők nevei legyenek x, y, z!
  • Írjunk olyan függvényt, mely kiszámítja a bejövő vektor hosszát!
  • Írjunk olyan függvényt, mely inputként kap egy v vektort és egy c számot és visszaadja a c * v vektort!
  • Írjunk olyan függvényt, mely a két bejövő argumentum vektornak az összegét számítja ki!
  • Írjunk olyan függvényt, mely ,,normalizál'' egy vektort (azaz visszaad egy ugyanolyan irányú, egység hosszú vektort)! Ha a vektor hossza túl kicsi, akkor legyen belőle nullvektor.
  • Írjunk skalár szorzó függvényt, mely két bejövő vektorból elkészíti a skalárszorzatukat!

a megoldás

Vegyük végig a részfeladatokat egyesével, és minden részfeladat után futtassuk le a teszteket, hogy jól gondolkodtunk-e!

Hogyan készítünk el egy ilyen case classt?

1
case class Vektor3D( x: Double, y: Double, z: Double )

Válasz mutatása

A vektor hossza koordinátáinak négyzetösszege a gyök alatt. Ehhez hasznos lehet a Math objektum sqrt függvénye. Implementáljuk!

1
def length( v: Vektor3D ) = Math.sqrt( v.x * v.x + v.y * v.y + v.z * v.z )
Válasz mutatása

Persze ebben a nyelvben is a szorzás erősebb művelet, mint az összeadás, ezért nem kell kitegyünk zárójeleket.

Itt érdemes megismerjük az import egy újabb használatát:

  • a forrásfile elején egy import Math.sqrt sorral elérjük, hogy az sqrt függvény elé ne kelljen kiírni, hogy Math (ugyanakkor pl. a Math.abs elé továbbra is ki kell ekkor),
  • egy import Math.* sornak pedig az lesz a hatása, hogy a Math objektumon belüli minden függvényt elég csak a nevén nevezni, Math és egyéb prefixek nélkül.

Persze ez nem csak a Mathra, hanem bármelyik csomag bármelyik objektumára igaz. Ezt persze olyankor érdemes megtenni, ha standard nevű a függvény és/vagy többször használjuk, úgy nem rontja az olvashatóságot mások számára, viszont rövidebb lesz, könnyebben feldolgozható emberi szemmel.

A számmal szorzás koordinátánként kell megszorozza a vektort a megadott számmal és visszaadni azt az új vektort, amit így kapunk. Hogyan implementáljuk le?

1
def mult( v: Vektor3D, c: Double ) = Vektor3D( v.x * c, v.y * c, v.z * c )

Azaz, ha új példányt akarunk létrehozni case classból, akkor pl. így kell hívjuk a konstruktorát.

Válasz mutatása

Két vektort koordinátánként adunk össze. Hogyan tegyük ezt?

1
2
def add( u: Vektor3D, v: Vektor3D ) =
  Vektor3D( u.x+v.x, u.y+v.y, u.z+v.z )
Továbbra is: mivel a fordító ki tudja következtetni, hogy a kifejezés értéke egy Vektor3D lesz és ez nekünk megfelel visszatérési értéknek, nem kell explicit kiírjuk a függvény típusát. Válasz mutatása

A normalizáló függvényt úgy írjuk meg, hogy ha a Vektor hossza 1e-10 vagy kisebb, akkor nullvektort adjunk vissza, egyébként pedig osszuk le a vektor hosszával. Érdemes ehhez a vektor hosszát kirakni egy értékbe. Használjuk fel a számmal szorzó és a hosszt számító függvényeket!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// fent, package deklaráció után
import Math.*

// valahol az App object belsejében

def normalize( u: Vektor3D ) = {
  val len = length( u )
  if( abs(len) < 1e-10 ) Vektor3D(0.0, 0.0, 0.0)
  else {
    val inverseLen = 1.0 / len
    mult( u, inverseLen )
  }
}  

A fordító ennek is ki tudja következtetni a típusát: az utolsó kifejezés egy if-else szerkezet, melynek az if részében egy Vektor3D lesz az érték típusa (a frissen létrehozott nullvektor), az else része pedig egy blokk kifejezésblokk, aminek az utolsó kifejezése szintén Vektor3D, hiszen ezzel tér vissza a mult függvény. Tehát az if mindkét ágán Vektor3D a típus, így biztos, hogy a kifejezés és így a függvény típusa is Vektor3D lesz.

Válasz mutatása

Végül ebben a feladatblokkban utolsóként, írjunk skalár szorzó függvényt: két vektor skalárszorzatát úgy kapjuk, hogy összeszorozzuk páronként az azonos koordinátákat és ezt a (most három) szorzatot összeadjuk.

1
2
def scalarProduct( u: Vektor3D, v: Vektor3D ) =
  u.x * v.x + u.y * v.y + u.z * v.z
Válasz mutatása

kompozíció

Ha megfigyeljük, a skaláris szorzatból és a gyökvonásból elő lehet állítani a hossz kiszámítását: önmagával kell skalárszorozni a vektort, majd gyököt vonni az eredményből. Újraírhatjuk ez alapján akár a hossz függvényt. Hogyan?

1
2
def length( u: Vektor3D ) =
  sqrt( skalarSzorzat(u,u) )

Válasz mutatása

Ez, mikor is több függvény egymásba helyettesítésével, végül a ,,legalsó'' függvénybe az argumentumok beírásával áll elő az eredmény, fontos elem funkcionális programozásban is; egy imperatív nyelven vélhetően kb. így csinálnánk. Azonban a funkcionális paradigmában, így pl. Scalában is, nem csak a ,,built-in'' primitív típusok, mint pl. az Intek közt vannak előre definiálva műveletek, mint pl. az Intek közti összeadás, szorzás, hanem a függvények között is. Egy ilyen az andThen: ha f: T => U egy függvény és g: U => V egy másik függvény (tehát az a fontos, hogy f output típusa ugyanaz legyen - vagy szűkebb -, mint g input típusa), akkor f andThen g lesz az a T => V függvény, ami az input x-re elébb kiszámítja f(x) értékét, majd az eredményt behelyettesíti g-be, vagyis (f andThen g)(x) = g(f(x)). Vannak, akiknek az andThen felírás könnyebben értelmezhető, mert így vizuálisan is kijön, hogy előbb f-et, majd utána g-t számítjuk ki.

Most ez nem könnyít meg nagyon semmit, mert az első függvényünk, f, nem az u-t várja, hanem u,u-t, így nem tudjuk azt írni, hogy skalarSzorzat andThen sqrt: sajnos az andThen csak egyváltozós függvényekre van értelmezve, így a skalarSzorzat andThen részre már problémázik a fordító, hogy nem tudja értelmezni, amit szeretnénk tőle.

Egy megoldás az, hogy készítünk egy olyan függvényt, ami egy bejövő vektort vár, és önmagával skalárszorozza azt (így kimenete egy Double lesz), majd ezzel kapcsoljuk össze andThen-nel a sqrt függvényt. Sikerül forduló kódot írjunk, ami ezt csinálja?

Így pl. lefordul:

1
2
def length( u: Vektor3D ) =
  { x => skalarSzorzat(x,x) } andThen sqrt

Egyelőre nem látszik az előnye az andThen konstrukciónak, de ha már az első függvényünk is eleve létezne valami megnevezett függvényként a névtérben, akkor látszana, hogy kényelmes a szintaxis.

Ha megfigyeljük, az u paramétert nem is használjuk a függvény törzsében így - ez az, amit, ha ki sem írjuk a paramétert, Tacit programmingnak vagy point-free style programmingnak is neveznek:

1
def length = { x => skalarSzorzat(x,x) } andThen sqrt

Így is fordul, és ha begépeljük, láthatjuk is, hogy az idea a helyes típust infereli hozzá, tudja, hogy ez egy Vektor3D => Double függvény kell legyen:

so what

Válasz mutatása

Jobb viszont, ha már ismerjük az andThen függvényt, ha ismerjük a compose függvényt is: lényegében ez egy fordított sorrendű andThen, f andThen g ugyanaz, mint g compose f. Hogy miért is van így ebből kettő.. truth is, még a matematikában sem konzisztens az, hogy egy ,,f kör g'' mint függvénykompozíció jelölés most a kettő közül melyiket is jelentse.

Ha csak megfordítjuk a két függvény sorrendjét és compose-t írunk andThen helyett, ezt kapjuk:

1
def length = sqrt compose { x => skalarSzorzat(x,x) }

Figyeljük meg: az idea nem húzza alá, nem jelez fordítási hibát! Azonban ha futtatni próbáljuk a kódot, akkor ezt megelőzően ténylegesen le is próbálja fordítani a fordító, és nem sikerül neki:

compose bad

Értjük a hibaüzenetből, hogy mi a probléma?

Bizonyos esetekben a Scala típuskövetkeztetője nem tudja feloldani, mikor egy függvény nevét írjuk be, hogy most ezt a függvényt meghívni akarjuk, vagy mint függvénnyel akarunk vele csinálni valamit. Ilyenkor, ha egyértelműsíteni akarjuk, hogy most mint függvényt szeretnénk kezelni, amivel össze akarunk komponálni egy másikat, egy megoldás kitenni az underscoret, ez történt az egyik előadáson is a println függvény esetében:

1
def length = sqrt _ compose { x => skalarSzorzat(x,x) }

Így már fordul, és úgy is fut, ahogy szeretnénk.

A tanulság talán az, hogy ne mindig hagyatkozzunk az idea beépített fordító motorjára, időnként, főleg ha ritkábban használt nyelvi feature-öket vetünk be, fordítsuk újra a projektet a build menüben, pl. a Build Project entry kiválasztásával (vagy ha van futtathatónk vagy tesztünk, futtassuk azt), kijöhetnek olyan fordítási hibák, amiket az idea nem detektál.

Válasz mutatása


Utolsó frissítés: 2021-02-07 23:06:47