Minart 0.6.0 - Dropping Scala 2.x and Minart Pure
Today the first milestone of Minart 0.6.0 was released.
While there are still a few things that I want to break before the final 0.6.0 release (I plan to at least wait for Scala Native 0.5.0 to rewrite the code with multi-threading in mind), this has been in the oven for quite a while, so I decided to release something to take it for a test drive.
This release includes two big breaking changes that I think deserve some explanation:
Dropping Scala 2.x support
Since the version 0.1.2 until 0.5.3, Minart always supported Scala 2.11, 2.12 and 2.13.
Initially the library was quite simple, so this was not very cumbersome (even though there are quite a few "Fix 2.11 compilation" commits).
As more features were added, not only this became a problem, but it also became obvious that there were no users for those versions. As far as I can tell, I might be the only developer using the library, and some problems (especially performance problems in 2.11) could just stay undetected for a long time.
On top of that, there were a few things that I wanted to do for a while that were only possible in Scala 3, one of them was making Color
an opaque type.
I had played around in the past with alternative representations (using AnyVal
, using a plain Int
in the internals, ...), but in the end I think this solution is clearly superior.
The API stays clean and RAM surfaces can use optimized arrays without boxing.
Another was rewriting/replacing the SDL bindings. The obvious solution is to use the excellent sn-bindgen, but only Scala 3 is supported.
Finally, one of the reasons that forced me to stay with 2.12/2.13 for so long was literate programming solutions. I was experimenting with notebooks (namely Polynote) and I really believe that literate programming goes very well with procedural art. Such solutions are, however, more used by data scientists and data engineers, so they focus on supporting Scala 2 (as Spark support is a must).
However, there's a new kid in the block! Scala CLI has experimental support for markdown scripts. While this is not as nice as a notebook, I find it perfectly serviceable, with the bonus that it's possible to share executable articles as "gist blog posts".
For all those reasons, Minart will be a Scala 3 library (on 3.3 LTS) for the foreseeable future.
Dropping Minart Pure
The Minart Pure package was introduced in version 0.1.3.
At the time, I wanted to be able to write some projects using the IO monad, but (to my knowledge) there was no implementation that worked well with Scala Native at the time. Since I didn't need anything too fancy, I ended up rolling my own.
However, the truth is, Minart Pure was never a joy to use.
At it's core Minart is pretty low level, so it just felt awkward to have to have to allocate an IO just to put a pixel, so in the end a lot of the code ended up being mutable functions wrapped in RIO
. It was also not a joy to maintain, as it was pretty easy to just forget to add pure versions of new operations.
Also, Cats Effect supports Scala Native since 3.3.14 (I'm not sure about the state of ZIO, I think it always had some very limited experimental support, but I might be wrong), so it doesn't really make sense to have a sub-par IO implementation when there are much better solutions around.
And finally (and most importantly), as I added more functionalities to Minart (namely SurfaceView
and Plane
), I'm starting to think that pure Minart applications should not use IO
(this is not possible right now, however). I think it's preferable to go with a declarative approach (that might be procedural in some places) instead of an imperative approach that uses IO
just to be "purely functional".
To show you what I mean, look at this two examples (the old Minart Pure example vs. an impure version using Plane
/SurfaceView
):
//> using scala "3.3.1"
//> using lib "eu.joaocosta::minart::0.5.3"
import eu.joaocosta.minart.backend.defaults._
import eu.joaocosta.minart.graphics._
import eu.joaocosta.minart.graphics.pure._
import eu.joaocosta.minart.runtime._
// Pure and Imperative
val pureApplication: CanvasIO[Unit] =
CanvasIO.canvasSettings
.map { settings =>
for {
x <- (0 until settings.width)
y <- (0 until settings.height)
r = (255 * x.toDouble / settings.width).toInt
g = (255 * y.toDouble / settings.height).toInt
} yield CanvasIO.putPixel(x, y, Color(r, g, 255))
}
.flatMap(CanvasIO.sequence)
.andThen(CanvasIO.redraw)
// Impure and Declarative
def impureApplication(canvas: Canvas): Unit =
val plane = Plane.fromFunction {
(x, y) =>
val r = (255 * x.toDouble / canvas.width).toInt
val g = (255 * y.toDouble / canvas.height).toInt
Color(r, g, 255)
}
canvas.blit(plane.toSurfaceView(canvas.width, canvas.height))(0, 0)
canvas.redraw()
AppLoop
.statelessRenderLoop(pureApplication) // switch pureApplication/impureApplication here
.configure(Canvas.Settings(width = 128, height = 128, scale = Some(4)), LoopFrequency.Never)
.run()
While the impureApplication
does call some procedures, I would argue that it's much more functional in spirit than the pureApplication
.
As such, I'll keep working in this direction and, if someone wants/needs to use IO
, they should just use a production ready implementation like Cats Effect.