Parciální funkce ve Scale a dvojité vyhodnocování
Parciální funkce je taková, která není definována pro všechny vstupy. Ve Scale
je reprezentována typem PartialFunction.
trait PartialFunction[-A, +B] extends (A => B) { def apply(x: A): B def isDefinedAt(x: A): Boolean }
Dlouho jsem dumal nad jednou věcí: nedochází při použití parciálních funkcí v některých případech ke dvojitému vyhodnocování? Abych to osvětlil, uvedu příklad:
val regex = """(\d+)-(\d+)-(\d+)""".r val f: PartialFunction[String, Int] = { case regex(y, m, d) => y.toInt*10000 + m.toInt*100 + d.toInt } val strings: Seq[String] = ??? strings.collect(f) // chová se stejně strings.filter(f.isDefinedAt).map(f)
f je parciální funkce definovaná jen pro stringy odpovídající určitému
regexu. Pokud je použita jako argument metody collect, ta nejdřív zjistí,
zdali je funkce definovaná pro konkrétní element (isDefinedAt) a pokud tomu
tak je, je tento aplikován. Z API je patrné, že musí nejdřív volat
isDefinedAt, které musí jednou vyhodnotit regex a pak volat apply, které
opět vyhodnotí ten samý regex. A regexy nejsou úplně zadarmo.
Tohle byla otázka, která mi ležela v hlavě.
Naštěstí tomu tak není. Kompilátor pro každou parciální funkci vygeneruje
metodu applyOrElse kombinující isDefinedAt a apply. Pro
PartialFunction o pár odstavců výše jde o přibližně toho (přibližně, protože
jsem to musel vykoukat z bytekódu):
def applyOrElse(str: String, default: String => Int): Int = { // tady se provede regex, pokud je úspěšný unapplySeq vrátí Some val opt: Option[Seq[String]] = regex.unapplySeq(str) if (opt.isEmpty || opt.get == null) { return default(str) } val seq = opt.get.asInstanceOf[scala.collection.LinearSeqOptimized] if (seq.lengthCompare(3) != 0) { return default(str) } // tělo parciální funkce seq(0).toInt*10000 + seq(1).toInt*100 + seq(2).toInt }
Samotná metoda applyOrElse ale nestačí k efektivní implementaci collect,
protože akce default se provede jen pro prvky, které se nenachází v doméně
funkce. Ale my potřebujeme opak – odlišit ty, které se v ní nacházejí, tedy
všechny pro které isDefinedAt vrátí true. Proto PartialFunction definuje
ještě metodu runWith, která na applyOrElse staví.
def runWith[U](action: B => U): A => Boolean = { x => val z = applyOrElse(x, _ => MARKER) if (z ne MARKER) { action(z); true } else false }
Nejde o přesnou implementaci runWith, ale zachycuje jejího ducha. MARKER je
unikátní objekt, který indikuje, že parciální funkce není pro daný vstup
definována.
Metoda collect interně používá právě runWith:
def collect[B](f: PartialFunction[A, B]): Seq[B] = { val b = newBuilder foreach(f.runWith(b += _)) b.result }
A to znamená, že při použití parciálních funkcí nedochází ke dvojitému
vyhodnocování a collect může být z důvodu snížených alokací dočasných kolekcí
efektivnější než odpovídající kombinace filter a map.
+1: Toto chování je zdokumentováno ve zdrojácích Scaly.