k47.cz  — každý den dokud se vám to nezačne líbit
výběr foto Praha povídky kultura
TECH ▞▞ kolo | twitter RSS

Scala - Booleovská kompozice funkcí

23. 3. 2012 (před 7 lety) — k47 (CC by-sa) (♪)

pro­jektu Cha­n­mi­ner jsem měl nějaké funkce f, g, h typu A => Boolean a po­tře­bo­val jsem je sklá­dat jako kdyby šlo o Bo­o­lean hod­noty, např: f && (g || h). Bylo by pěkné, kdyby to šlo takhle přímo, ale bo­hu­žel Functi­onN nemá žádnou z metod &&, !!, unary_! nebo ^.


Kla­sické řešení je vy­tvo­řit větší funkci, v níž za­vo­lám f, gh a zkom­bi­nuji 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á ne­vý­hody: musím ně­ko­li­krát opa­ko­vat všechny ar­gu­menty, což člo­věka začne štvát velice rychle. V ukázce jsou jména ar­gu­mentů jenom jed­no­pís­menné, kdyby měly po­pisný název, tak by se v zá­plavě písmen úplně ztra­til záměr kódu.


Pomoci může ScalaZ a mo­no­idy, které se dají k po­dob­nému účelu použít. Monoid je struk­tura s jednou aso­ci­a­tivní bi­nární ope­rací, nu­lo­vým prvkem a ně­ko­lika axiomy. Monoid je na­pří­klad de­fi­no­ván pro celá čísla, kde ope­race je + a nulový prvek je 0. Stejně tak je de­fi­no­ván i pro funkce A => Boolean, kde ope­race je lo­gické nebo a nulový prvek je false (zjed­no­du­šeně řečeno).

Takže když bych chtěl mezi sebou zo­ro­vat ně­ko­lik funkcí, můžu napsat:

val x = f |+| g |+| h

Jak vidno, není to příliš fle­xi­bilní řešení.

Stejně jako je na celých čís­lech de­fi­no­ván další monoid, kde ope­race je ná­so­bení a nulový prvek je 1 exis­tuje i na bool funk­cích ještě jeden monoid pro and a true, ale aby se použil tenhle musely by naše pre­di­káty vracet spe­ci­ální typ Bo­o­le­an­Con­junction a i tak není možné jed­no­duše míchat tyhle dva mo­no­idy. Takže nic, tohle mi práci ne­u­lehčí.


ScalaZ ale nabízí další uži­teč­nou věc: apli­ka­tivní bu­il­dery. Pomocí nich můžu na­a­ku­mu­lo­vat ně­ko­lik hodnot/funkcí a na ně potom apli­ko­vat funkci, která bere tolik pa­ra­me­trů, kolik je na­a­ku­mu­lo­váno hodnot (zjed­no­du­šeně řečeno):

val x = (f |@| g |@| h) apply (_ && _ && _)

Pro­blém je ale s apli­ko­va­nou slo­ži­těj­ších funkcí. Když se v nich vy­skytne zá­vorka, Scala odvodí funkci s jiným počtem pa­ra­me­trů, než jsem za­mýš­lel, takže tohle ne­fun­guje:

val x = (f |@| g |@| h) apply ((_ || _) && _) // compiletime error

Jde to takhle:

val x = (f |@| g |@| h) apply (_.||(_) && _)

Ale to si s sebou nese ne­pří­jemně vysoký wtf faktor.

Tenhle ne­do­sta­tek se dá vy­ře­šit vy­puš­tě­ním pod­tr­ží­tek z ano­nymní funkce a po­jme­no­vá­ním všech pa­ra­me­trů:

val x = (f |@| g |@| h) apply { (a, b, c) => (a || b) && c)

Vý­sle­dek fun­guje, ale je po­ně­kud roz­ta­haný.


Žádné z před­cho­zích tří ře­še­ních není ide­ální. Na­štěstí si díky zá­zraku im­pli­citní kon­verze můžu napsat ně­ko­lik jed­no­du­chých kom­bi­ná­torů, se kte­rými vý­sledný kód vypadá přesně jak jsem si před­sta­vo­val:

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 & hosté, ascii@k47.cz