Scala - Booleovská kompozice funkcí

V projektu Chanminer jsem měl nějaké funkce f
,
g
, h
typu A => Boolean
a potřeboval jsem je skládat jako kdyby šlo o Boolean hodnoty, např: f && (g || h)
. Bylo by pěkné, kdyby to šlo
takhle přímo, ale bohužel FunctionN nemá žádnou z metod &&
, !!
, unary_!
nebo ^
.
Klasické řešení je vytvořit větší funkci, v níž zavolám
f
, g
a h
a zkombinuji jejich výsledky:
val x = { (a: A, b: B, c: C) => f(a, b, c) && (g(a, b, c) || h(a, b, c)) }
I když je to funkční řešení, je hned vidět jaké má nevýhody: musím opakovat všechny argumenty, což člověka začne velice rychle štvát. V ukázce jsou jména argumentů jenom jednopísmenné, kdyby měla popisné názvy, v záplavě písmen by se ztratil záměr kódu.
Přeskočím a začnu rovnou tím, jak to udělat v nové Scale 3 (jinak byste se
museli prokousat přes klikyháky ScalaZ a koho zajímá tohle monstrum). Třetí
verze jazyka usnadňuje rozšiřování existujících typů pomocí mechanismu
extension
. Jím můžu přímo vyjádřit, že chci přidat několik metod na typ (T =>
Boolean)
a hotovo. Není třeba ztrácet čas s implicitními konverzemi a pomocnými třídami.
extension[T] (f: T => Boolean) { inline def && (g: T => Boolean) = (a: T) => f(a) && g(a) inline def || (g: T => Boolean) = (a: T) => f(a) || g(a) inline def ^ (g: T => Boolean) = (a: T) => f(a) ^ g(a) inline def unary_! = (a: T) => !f(a) } val f: Int => Boolean = ??? val g: Int => Boolean = ??? val h: Int => Boolean = ??? val c: Int => Boolean = !f && g || h
Za zmínku stojí ještě klíčové slovo inline
, které způsobí, že triviální
kombinátory budou vždy plně inlinované a nikdy se nebude volat pomocná metody
&&
a další. To odstraní určitou úroveň abstrakce a JIT má o něco málo
jednodušší práci. Ale jako vždy s inlinováním platí, že se to nesmí přehánět.
JIT preferuje malé kompaktní metody.
Pomoci může ScalaZ a monoidy, které se dají
k podobnému účelu použít. Monoid je struktura s jednou asociativní binární operací,
nulovým prvkem a několika axiomy. Monoid je například definován pro celá čísla, kde operace je +
a nulový prvek je 0. Stejně tak je definován i pro funkce A => Boolean
, kde
operace je logické nebo a nulový prvek je false
(zjednodušeně řečeno).
Takže když bych chtěl mezi sebou zorovat několik funkcí, můžu napsat:
val x = f |+| g |+| h
Jak vidno, není to příliš flexibilní řešení.
Stejně jako je na celých číslech definován další monoid, kde operace je násobení a nulový prvek je 1 existuje i na bool funkcích ještě jeden monoid pro and a true, ale aby se použil tenhle musely by naše predikáty vracet speciální typ BooleanConjunction a i tak není možné jednoduše míchat tyhle dva monoidy. Takže nic, tohle mi práci neulehčí.
ScalaZ ale nabízí další užitečnou věc: aplikativní buildery. Pomocí nich můžu naakumulovat několik hodnot/funkcí a na ně potom aplikovat funkci, která bere tolik parametrů, kolik je naakumulováno hodnot (zjednodušeně řečeno):
val x = (f |@| g |@| h) apply (_ && _ && _)
Problém je ale s aplikovanou složitějších funkcí. Když se v nich vyskytne závorka, Scala odvodí funkci s jiným počtem parametrů, než jsem zamýšlel, takže tohle nefunguje:
val x = (f |@| g |@| h) apply ((_ || _) && _) // compiletime error
Jde to takhle:
val x = (f |@| g |@| h) apply (_.||(_) && _)
Ale to si s sebou nese nepříjemně vysoký wtf faktor.
Tenhle nedostatek se dá vyřešit vypuštěním podtržítek z anonymní funkce a pojmenováním všech parametrů:
val x = (f |@| g |@| h) apply { (a, b, c) => (a || b) && c)
Výsledek funguje, ale je poněkud roztahaný.
Žádné z předchozích tří řešeních není ideální. Naštěstí si díky zázraku implicitní konverze můžu napsat několik jednoduchých kombinátorů, se kterými výsledný kód vypadá přesně jak jsem si představoval:
trait F[T] extends Function1[T, Boolean] { def apply(t: T): Boolean def && (g: T => Boolean) = (a: T) => apply(a) && g(a) def || (g: T => Boolean) = (a: T) => apply(a) || g(a) def ^ (g: T => Boolean) = (a: T) => apply(a) ^ g(a) def unary_! = (a: T) => !apply(a) } implicit def toF[T](a: T => Boolean) = new F[T] { def apply(x: T) = a(x) } // *** val f: Int => Boolean = ??? val g: Int => Boolean = ??? val h: Int => Boolean = ??? val c: Int => Boolean = !f && g || h