Open raquo opened 2 years ago
I do have some ideals about this, and would like to help.
SplitByType
would be very welcomed, as my app usually required something similar to that.
@HollandDM I'll be happy to hear your ideas!
I'm currently using something like this https://gist.github.com/HollandDM/446bb41a8c89607b140d3bf3297b2a92. This macro's main feature is to turn a bunch of code from this:
sealed trait Foo
final case class Bar(strOpt: Option[String]) extends Foo
enum Baz extends Foo {
case Baz1, Baz2
}
case object Tar extends Foo
val splitter = fooSignal.splitMatch
.handleCase { case Bar(Some(str)) => str } { (str, strSignal) => renderStrNode(str, strSignal) }
.handleCase { case baz: Baz => baz } { (baz, bazSignal) => renderBazNode(baz, bazSignal) }
.handleCase {
case Tar => ()
case _: Int => ()
} { (_, _) => div("Taz") }
.toSignal
Into this:
val splitter = fooSignal.
.map { i =>
i match {
case Bar(Some(str)) => (0, str)
case baz: Baz => (1, baz)
case Tar => (2, ())
case _: Int => (2, ())
}
}
.splitOne(_._1) { ... }
(After macros expansion, compiler will warns this code "match may not be exhaustive" and "unreachable case" as expected).
As far as I know, "exhaustive matching" cannot be trigger by any mean beside defining a match case, as this is a compiler feature, not a scala's macros/runtime feature. So macros will need to do something similar to the code above. It's will be too much for us create exhaustive matching manually.
FYI: I think this is the entry for exhaustive match checking in scala 3 https://github.com/lampepfl/dotty/blob/main/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala#L825.
I can help with a PR if you want to go with this, but this is scala 3's macro only, and I don't have any experience with scala 2's macro, unfortunately.
@HollandDM Thanks, this is awesome, it's pretty much what I was hoping for!
Don't worry about Scala 2, I wasn't planning to support it for the same reasons you mention. I'm ok with all macro-enabled features being Scala 3 only (not like there's a lot, it's just this).
I'll be happy with any PR / help, but I won't be able to include this in 17.x, it will go in a later version. I will only be able to give this an in-depth look in a few months, so just keep that timeline expectation in mind.
@raquo Don't worry, I can still use my internal implementation in the mean time. Take your time!
I like this a lot. The implementation is quite complex compared to my initial one above, but the payoff is so much greater.
For example, this should work well with splitting a union of string literal types. Javascript has a ton of those. In fact, a common pattern for discriminated unions in typescript works exactly this way, as seen in the following link from the official typescript Documentation: https://www.typescriptlang.org/docs/handbook/2/narrowing.html#discriminated-unions
What if you have a
Signal[MyEnum]
orSignal[MyGADT]
, and you want to split the signal by type, much like the normalsplit
operator splits the signal by value?Waypoint has a useful SplitRender feature that works like this:
I could potentially move this functionality into Airstream, but it's arguably... not good enough. The biggest downside is the lack of exhaustiveness matching, that is, you can call
.signal
any time to get the signal, even if you didn't handle all the cases. I also dislike the need to call.signal
at all, perhaps I will hide it with an operator signature like defsplitByType(f: SplitRender => SplitRender): Signal[Out] = f(SplitRender(this)).signal
(various type params omitted for brevity).@felher suggested using Scala 3 metaprogramming for this, and although my design goal is to keep Laminar & Airstream simple, I think exhaustive matching on subtypes is actually a good use for this, that is, the metaprogramming implementation will probably be the simplest most straightforward one.
I will not be using any third party macro libraries though, plain Scala only, which means Scala 3 only, for the sake of reducing dev and maintenance effort.
For reference, here is @felher's implementation of
splitByEnum
: https://gist.github.com/felher/5515eb1124268b0e10eadc78778f49a8 – it's short, and even though I have zero experience with macros, I can still understand what it does at a high level. I even dare say this might be good to include in Airstream as-is.If we're doing this, we should probably also support a more complicated
splitByType
use case, which, similar to Waypoint'sSplitRender
, would let the user split type hierarchies on arbitrary subtypes, while providing exhaustive matching. I don't know how hard it is to implement with Scala 3 metaprogramming facilities, I need to find the time to learn this whole thing.Any help / advice is welcome in this direction.