Search code examples
multithreadingscalajavafxscala-catsscalafx

ScalaFX and Cats Effect


I am trying to integrate Cats Effect into a ScalaFX desktop application, and I am having trouble getting the tasks to execute. I would like to run a background thread/fiber to initialize the window when it is displayed. What I THINK I'm doing is:

  1. Starting the JavaFX application on a blocking IO.
  2. JavaFX is doing a bunch of stuff, including starting its own threads and stuff. I am not sure if this matters.
  3. On the "JavaFX Application Thread" thread, the controller's initializable method is running (confirmed with the println), and creating the IO I would like to execute.
  4. I am creating the Supervisor to execute it in the background.
  5. It is not printing "Hello".

I've tried a few solutions, like calling SupervisorIO.evalOn() instead of the simple call below. I've tried calling "join" on the thread created by Supervisor#supervise to try and force it to execute, and nothing is happening.

I simplified the application to look like this (the FXML is just a BorderPane and a Label and it configures the controller class; I didn't think you'd need to see it):

object CatsTest extends IOApp.Simple {
  override def run: IO[Unit] = {
    IO.blocking {
      Application.launch(classOf[CatsTestMain])
    }
  }
}

class CatsTestMain extends Application {
  override def start(primaryStage: Stage): Unit = {
    val screen = getClass.getResource("/main-screen.fxml")
    val loader = new FXMLLoader()
    loader.setLocation(screen)
    val root: Parent = loader.load[javafx.scene.Parent]
    val controller = loader.getController[MainController]()

    primaryStage.setScene(new Scene(new javafx.scene.Scene(root)))
    primaryStage.show()
  }
}

class MainController extends Initializable {
  override def initializable(location: URL, resources: ju.ResourceBundle): Unit = {
    println("in controller")
    val supervisor = Supervisor[IO](await=true)
    supervisor.use { supervisor =>
      supervisor.supervise(IO.println("Hello"))
    }
  }
}

I don't know if I need to marshal back into the original thread to use Cats' resources, or what is the problem. I'm new to Cats Effect, and this was the first application I've written in a while that wasn't bound to use Akka/Pekko, so it seemed like a good way to learn it, but I can't help but think I've created more problems than I've solved not just using Future objects.

ANSWER: The final code that prints "In controller" and then "Hello" is below. In CatsTest.run, by creating the Dispatcher Resource and then calling "use", it created an IO to hand up to the IOApp that it could run. This IO creates a factory method that has access to the dispatcher and then creates a blocking IO from which to launch the JavaFX application defined in CatsTestMain. (I suppose the CATS-way to do this would be to put the first part in one IO and chain that to the blocking IO). The JavaFX application then uses the controller factory method defined above in the FXMLLoader to pass the dispatcher to the Controller class so that it can be used by the window controller. After this, JavaFX calls Initializable#initialize on the controller and prints out "In controller", and then the dispatcher can be used to execute an IO.

object CatsTest extends IOApp.Simple {
  var factory: javafx.util.Callback[Class[?], Object] = null

  override def run: IO[Unit] = {
    Dispatcher.sequential[IO] use { dispatcher =>
      factory = { (tpe: Class[?]) => 
        if(classOf[MainController].isAssignableFrom(tpe)) {
          tpe.newInstance().asInstanceOf[MainController].dispatcherOpt = Some(dispatcher)
        } else {
          tpe.newInstance()
        }
      }
      IO.blocking { Application.launch(classOf[CatsTestMain]) }
    }
  }
}

class CatsTestMain extends Application {
  override def start(primaryStage: Stage): Unit = {
    ... the stuff above to initialize the loader
    loader.setControllerFactory(CatsTest.factory)
    ... the rest of the stuff to create and show the stage
  }
}

class MainController extends Intializable {
  var dispatcherOpt: Option[Dispatcher[IO]] = None

  override def intialize(location: URL, resources: ju.ResourceBundle): Unit = {
    println("In controller")
    dispatcherOpt.map(dispatcher => {
      dispatcher.unsafeRunAndForget(IO.println("Hello"))
    })
  }

Solution

  • There are two problems here:

    The first and most important one is that supervisor.use returns an IO, which is just a description of a computation, nothing more. It needs to be explicitly run or sequenced with other IOs. This is the fundamental knowledge needed to write cats-effect applications, IO are just descriptions, values, not handlers of a running computation; contrary to Future
    What you want to use for this kind of interop is a Dispatcher, not a Supervisor: https://typelevel.org/cats-effect/api/3.x/cats/effect/std/Dispatcher.html that gives you a unsafeRunAndForget to send the IO to run in the background.

    Second, you are doing a bad management of the life cycle. You don't want to create and destroy one Dispatcher per IO to execute. Ideally MainController should receive an already allocated Dispatcher and something else must ensure it is properly closed when not needed anymore.

    Third, if the IOs you plan to run will affect the interface, those need to run on the appropriate threads of JavaFX.


    so it seemed like a good way to learn it

    Personally, I would disagree.

    • First, IO is better suited for web servers that have to deal with concurrency.
    • Second, these kinds of apps that require bidirectional interop are actually harder to write for a newcomer. Because they expect you to be proficient in both JavaFX internals and cats-effect execution model.

    Having said that. A lot of folks have built similar things during the last years. You may ask in the Discord servers for advice and resources.