k47.cz
mastodon twitter RSS
bandcamp explorer

Scala - Booleovská kompozice funkcí

23. 3. 2012 (aktualizováno 31. 5. 2021) — k47 (CC by-sa)

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, gh 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
píše k47, ascii@k47.cz