Scalafx Properties/Bindings

Visitor from the 20th century here, as far as GUI programming is concerned… I’m trying to recreate an old GUI app of mine. The original code is Scala 2.11 with Swing using a naive Presentation Model approach, and it’s way beyond redemption.

Now I’ve been looking into scalafx and I’m somewhat confused. It looks like the framework wants me to use properties and bindings, OTOH the API is very javaesque, for obvious reasons. Right now I feel really tempted to throw some unlawful cats code plus some extension methods on top…

given objectBindingFunctor: Functor[ObjectBinding] with
  override def map[A, B](fa: ObjectBinding[A])(f: A => B): ObjectBinding[B] =
    Bindings.createObjectBinding[B](() => f(fa.value), fa)

// Applicative accordingly, Monad maybe later

extension[A, J] (prop: Property[A, J])

  def binding[B](f: A => B): ObjectBinding[B] =
    Bindings.createObjectBinding[B](() => f(prop.value), prop)

extension (bnd: ObjectBinding[Boolean])

  def asBooleanBinding: BooleanBinding =
    Bindings.createBooleanBinding(() => bnd.value, bnd)

…so I could do stuff like this:

val hostBinding =
  hostText.text.binding { h => 
    Option.when(h.nonEmpty)(h) >>= Host.parseOption 
  }
val userBinding =
  userText.text.binding(u => Option.when(u.nonEmpty)(u))
val prefsBinding =
  (hostBinding, userBinding).mapN { (hostOpt, userOpt) =>
    (hostOpt, userOpt).mapN(UserPrefs(_))
}
acceptButton.disable <== prefsBinding.map(_.isEmpty).asBooleanBinding
// return prefsBinding.value via Dialog#resultConverter

Questions:

  • Am I missing some alternative framework/library for Scala 3 cross-platform apps (desktop only)?
  • Am I holding it wrong, i.e. am I missing some API for nicely chaining properties, either in scalafx itself or from 3rd party libs on top?
  • Am I going to shoot my own foot continuing the approach outlined above? How? Any better suggestions?
  • What’s a good way to get this property/binding forest properly organized, especially for testing? Naively I’d start by encapsulating the property/binding “logic” in some presentation model style wrapper that can be tested standalone. But then again this may be a 20th century thought. :slight_smile:

Any pointers and constructive criticism appreciated - thanks in advance!

ScalaFX is more or less 1:1 wrappers around corresponding JavaFX API, hence it would probably feel a bit Java-ish, and the JavaFX property/binding stuff is a bit clunky already. I’ve not used ScalaFX myself as I don’t think it’s quite the right approach, instead I’ve built my own library with minimal Scala-ish enhancements to key JavaFX APIs.

I’m not very familiar with Cats so I don’t quite see what you’re trying to do above, but you could get a long way with simple extention methods like this:

type Val[A] = ObservableValue[A]

extension [A] (v: Val[A])

  def apply() = v.getValue

  def map[B](f: A => B): Val[B] = 
    new ObjectBinding[B]:
      bind(v)
      def computeValue() = f(v.getValue)

extension [A1, A2] (v: (Val[A1], Val[A2]))

  def mapN[B](f: (A1, A2) => B): Val[B] = 
    new ObjectBinding[B]:
      bind(v._1, v._2)
      def computeValue() = f(v._1.getValue, v._2.getValue)      

Thanks a lot for the feedback!

I understand this, and I’m not complaining - just wondering what’s the best way to get towards a more scalaesque experience quickly.

I guess I’m not in a position, yet, to have an informed opinion and to implement stuff on top of raw JavaFX. For now I’d rather use what more experienced people have built and add a bit of glue code on top.

That’s basically what cats provides. The advantage is that I only need to implement a couple of methods (e.g. #pure and #ap for Applicative) and get a rich API on top of this for free (e.g. #mapN overloads for an arbitrary number of arguments). It feels a bit like cheating, though, as Applicative instances are supposed to be pure, whereas ObjectBinding certainly isn’t. But as long as this doesn’t raise practical issues…

Btw I found a way to do arbitrary arity without any boilerplate:

extension [Tup <: Tuple] (tup: Tup)

  def mapN[B](f: Tuple.InverseMap[Tup, ObservableValue] => B): ObservableValue[B] =
    val ovs = tup.toList.asInstanceOf[List[ObservableValue[Any]]]
    new ObjectBinding[B]:
      bind(ovs*)
      def computeValue() = 
        val vs = ovs.map(_.getValue)
        val vtup = vs.foldRight[Tuple](EmptyTuple)(_ *: _).asInstanceOf[Tuple.InverseMap[Tup, ObservableValue]]
        f(vtup)