k47.cz
mastodon twitter RSS
bandcamp explorer

Scala - Zřetězené porovnávání

13. 12. 2010 (aktualizováno 30. 5. 2021) — k47 (CC by)

Perl 6 / Raku má neuvěřitelně bohatou syntaxi a jednou z tisíce vychytávek je zřetězené porovnávání, kdy se může zapsat podmínku ve tvaru 20 <= $x <= 25. Když se na to podíváte, dává to smysl na první pohled, ale většina jazyků výraz nevyhodnotí jako 20 <= $x && $x <= 25, ale jako (20 <= $x) <= 25, což není, co by člověk chtěl.

Nicméně zřetězené porovnávání se dá docela pěkně implementovat ve Scale.


case class CC[T <% Ordered[T]](val v: T, val bool: Boolean = true) {
// view bound T <% Ordered[T] zaručuje, že T má metody <, >, <=, >=

  private def make(x: Boolean) = new CC(v, bool && x)

  def <<:(a: CC[T]) = make(a.v < v) // <: je klíčové slovo - tzv. upper type bound
  def :<<(a: CC[T]) = make(v < a.v)

  def >>:(a: CC[T]) = make(a.v > v) // >: je klíčové slovo - tzv. lower type bound
  def :>>(a: CC[T]) = make(v > a.v)

  def <=:(a: CC[T]) = make(a.v <= v)
  def :<=(a: CC[T]) = make(v <= a.v)

  def >=:(a: CC[T]) = make(a.v >= v)
  def :>=(a: CC[T]) = make(v >= a.v)
}

implicit def v2cc[T <% Ordered[T]](v: T) = new CC(v)
implicit def cc2b[T](v: CC[T]) = v.bool

// test
import scala.language.implicitConversions

val a = 20
val b = 15

println(if (10 <<: a :<< 30) "ok" else "chyba")
println(if (10 <<: a :<< 15) "chyba" else "ok")
println(if (10 <<: a :>> b)  "ok" else "chyba")

Metody končící dvojtečkou se v pozici operátorů volají v opačném pořadí. Metody se bohužel nemůžou jmenovat <:>:, protože to jsou klíčová slova. Teoreticky všechny metody začínající dvojtečkou by mohly mít podobu standardních operátorů porovnání (tedy místo :<< by se použilo <, místo :>> pak > atd), ale z důvodu konzistence a jako signál, že se děje něco nekalého, jsem nechal tvar takový jaký je.

Mimochodem, příklad funguje díky tomu, že kompilátor implicitními konverzemi transformuje výraz 10 <<: 15 :<< 20 na cc2b(bv2cc(15).<<:(10).:<<(20)).


Verze pro Scalu 3 je téměř identická (a kromě view bounds bude fungovat i ta předchozí). Změnilo se jen přejmenování implicit na given, které nemusí mít jméno a použití typu Conversion, který je jediný způsob, jak dělat implicitní konverze. Ty vyšly z módy jako matoucí a potenciálně velice nebezpečná vlastnost jazyka a ve třetí verzi je jejich definice omezení na typ Conversion a použití musí předcházet import scala.language.implicitConversions.

case class CC[T: Ordering](v: T, boolVal: Boolean) {
  import Ordering.Implicits.infixOrderingOps

  def <<:(a: CC[T]) = CC(v, boolVal && a.v < v)
  def :<<(a: CC[T]) = CC(v, boolVal && v < a.v)

  def >>:(a: CC[T]) = CC(v, boolVal && a.v > v)
  def :>>(a: CC[T]) = CC(v, boolVal && v > a.v)

  def <=:(a: CC[T]) = CC(v, boolVal && a.v <= v)
  def :<=(a: CC[T]) = CC(v, boolVal && v <= a.v)

  def >=:(a: CC[T]) = CC(v, boolVal && a.v >= v)
  def :>=(a: CC[T]) = CC(v, boolVal && v >= a.v)
}

given [T: Ordering]: Conversion[T, CC[T]] = new CC(_, true)
given [T]: Conversion[CC[T], Boolean] = _.boolVal

import scala.language.implicitConversions

// ...
píše k47, ascii@k47.cz