Felmerül a kérdés, hogy mégis mikor érdemes valamit tagfüggvényként implementálni egy osztályon belül, és mikor rajta kívül. Van, amikor nincs más választásunk, mint bent deklarálni (pl. amikor olyan adathoz fér hozzá a függvény, ami csak az osztályon belül látható - a láthatósági módosítókról is lesz később poszt). Mindenesetre, ha van az IntListünk, és egy alkalmazásban szeretnénk pl. egy függvényt, ami visszaad egy listát, amiben minden elem kétszerese az eredeti listabelinek (tehát pl. a (1,4,2,8,5,7) listán hívva a (2,8,4,16,10,14) új listát adja vissza), azt persze leimplementálhatjuk így is:
trait IntList {
def szorzasKettovel: IntList
}
case object Ures extends IntList {
override val szorzasKettovel = Ures
}
case class Nemures(head: Int, tail: IntList) {
override def szorzasKettovel = Nemures(head * 2, tail.szorzasKettovel)
}
de (remélhetőleg) azt érzi ilyenkor az ember valahol belül, hogy ez így nem lesz jó, mindenkinek, aki valamikor később használni fogja az IntList osztályunkat, ki lesz ajánlva egy szorzás kettővel függvény, amit jó eséllyel nem használna, mert csak egy bizonyos alkalmazásban egy bizonyos célra volt rá szükség. Pláne, ha egy másik alkalmazásban épp kilenccel kell szorozni az elemeket, egy másikban hármat hozzáadni... és egy nagyon furcsa abomination kezd kifejlődni, ha ezt az utat követjük:
és ez az a pont, ahol egy kicsit messzebbről szemlélve a kódot valami "általánosabb" mintát láthatunk benne, amivel elég csak egy függvényt implementálni: mivel funkcionális programozásban próbálunk gondolkodni, észrevehetjük, hogy itt valójában két dolog szerepel inputként: a lista és egy művelet, ami a lista elemeit transzformálja valahogy egyenként, függetlenül. Az első esetben a kettővel szorzás függvény, a másodikban a kilenccel szorzás, a harmadikban a hármat hozzáadás. Ezt így is meg lehet csinálni:
Az alsó három példában láthatjuk, hogy így miért is tudtuk egy füst alatt lekezelni a (valószínűleg) alkalmazásfüggő "szorozd konstanssal"-like függvényeket: egyszerűen csak a transzformációt kell átadjuk argumentumként. Névtelen, lambda-függvényként átadni pl. úgy lehet, ahogy a fenti példában látjuk. Ha ugyanazt a transzformációt használjuk sok helyen a kódban, akkor persze érdemes kimenteni egy (mondjuk) val mezőbe, és akkor látszik is, hogy miért is csináljuk, amit -- és ha változtatni kell a függvényt, akkor elég lesz egy helyen piszkálni a kódot.
trait IntList {
def szorzasKettovel: IntList
def szorzasKilenccel: IntList
def pluszHarom: IntList
}
case object Ures extends IntList {
override def szorzasKettovel = Ures
override def szorzasKilenccel = Ures
override def pluszHarom = Ures
}
case class Nemures(head: Int, tail: IntList) {
override def szorzasKettovel = Nemures(head * 2, tail.szorzasKettovel)
override def szorzasKilenccel = Nemures(head * 9, tail.szorzasKilenccel)
override def pluszHarom = Nemures(head + 3, tail.pluszHarom)
}
és ez az a pont, ahol egy kicsit messzebbről szemlélve a kódot valami "általánosabb" mintát láthatunk benne, amivel elég csak egy függvényt implementálni: mivel funkcionális programozásban próbálunk gondolkodni, észrevehetjük, hogy itt valójában két dolog szerepel inputként: a lista és egy művelet, ami a lista elemeit transzformálja valahogy egyenként, függetlenül. Az első esetben a kettővel szorzás függvény, a másodikban a kilenccel szorzás, a harmadikban a hármat hozzáadás. Ezt így is meg lehet csinálni:
trait IntList {
def map(f: Int=>Int): IntList
}
case object Ures extends IntList {
override def map(f: Int=>Int) = Ures
}
case class Nemures(head: Int, tail: IntList) extends IntList {
override def map(f: Int=>Int) = Nemures(f(head), tail.map(f))
}
val list = Nemures(1,Nemures(4,Nemures(2,Ures)))
println(list.map({x: Int => 2*x})) //prints Nemures(2,Nemures(8,Nemures(4,Ures)))
println(list.map { x => 9*x }) //prints Nemures(9,Nemures(36,Nemures(18,Ures)))
println(list.map { _+3 }) //prints Nemures(4,Nemures(7,Nemures(5,Ures)))
val myFavFunc: Int=>Int = { x => 7*x }
println(list.map(myFavFunc)) //prints Nemures(7,Nemures(28,Nemures(14,Ures)))
Az alsó három példában láthatjuk, hogy így miért is tudtuk egy füst alatt lekezelni a (valószínűleg) alkalmazásfüggő "szorozd konstanssal"-like függvényeket: egyszerűen csak a transzformációt kell átadjuk argumentumként. Névtelen, lambda-függvényként átadni pl. úgy lehet, ahogy a fenti példában látjuk. Ha ugyanazt a transzformációt használjuk sok helyen a kódban, akkor persze érdemes kimenteni egy (mondjuk) val mezőbe, és akkor látszik is, hogy miért is csináljuk, amit -- és ha változtatni kell a függvényt, akkor elég lesz egy helyen piszkálni a kódot.
Egy másik gyakran használt listaművelet a filter: pl. ha az IntListünkben gondolkodunk, akkor lehet az egy feladat, hogy legyűjtsük a pozitív értékeket egy listába, vagy a páratlan számokat:
trait IntList {
def pozitivak: IntList
def paratlanok: IntList
}
case object Ures extends IntList {
override def pozitivak = Ures
override def paratlanok = Ures
}
case class Nemures(head: Int, tail: IntList) extends IntList {
override def pozitivak = if (head>0) Nemures(head, tail.pozitivak) else tail.pozitivak
override def paratlanok = if (head%2==1) Nemures(head, tail.paratlanok) else tail.paratlanok
}
és ugyanúgy, mint az előbb, rájöhetünk arra is, hogy ez egy általános minta, amit egy predikátum (az objektumból, mint most az Int, Booleanba képző függvényeket hívják így) átadásával tudunk egységesen kezelni:
trait IntList {
def filter(p: Int=>Boolean): IntList
}
case object Ures extends Intlist {
override def filter(p: Int=>Boolean) = Ures
}
case class Nemures(head: Int, tail: IntList) extends IntList {
override def filter(p: Int=>Boolean) = if (p(head)) Nemures(head, tail.filter(p)) else tail.filter(p)
}
val list = Nemures(1,Nemures(4,Nemures(2,Ures)))
println(list.filter { _%2==1 }) //prints Nemures(1,Ures)
Már csak az üthet szöget a fejünkbe, hogy mi van, ha nem csak Inteket tároló listákkal akarunk dolgozni, ez a következő poszt témája
folytköv
No comments:
Post a Comment