Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

splitByType / splitByEnum #103

Open
raquo opened this issue Oct 18, 2022 · 6 comments
Open

splitByType / splitByEnum #103

raquo opened this issue Oct 18, 2022 · 6 comments
Labels
design help wanted Extra attention is needed need to find time https://falseknees.com/297.html

Comments

@raquo
Copy link
Owner

raquo commented Oct 18, 2022

What if you have a Signal[MyEnum] or Signal[MyGADT], and you want to split the signal by type, much like the normal split operator splits the signal by value?

Waypoint has a useful SplitRender feature that works like this:

val splitter = SplitRender[Page, HtmlElement](router.currentPageSignal)
  .collectSignal[UserPage] { userPageSignal => renderUserPage(userPageSignal) }
  .collectStatic(LoginPage) { div("Login page") }
 
// this method requires a Signal of a specific page type, and SplitRender is able to provide it
def renderUserPage(userPageSignal: Signal[UserPage]): Div = {
  div(
    "User page ",
    child.text <-- userPageSignal.map(user => user.userId)
  )
}
 
val app: Div = div(
  h1("Routing App"),
  child <-- splitter.signal // get the signal.
)

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 def splitByType(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's SplitRender, 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.

@raquo raquo added help wanted Extra attention is needed design need to find time https://falseknees.com/297.html labels Oct 18, 2022
armanbilge pushed a commit to armanbilge/Airstream that referenced this issue Nov 16, 2023
armanbilge pushed a commit to armanbilge/Airstream that referenced this issue Nov 16, 2023
@HollandDM
Copy link
Contributor

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.

@raquo
Copy link
Owner Author

raquo commented Jan 26, 2024

@HollandDM I'll be happy to hear your ideas!

@HollandDM
Copy link
Contributor

HollandDM commented Jan 27, 2024

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.

@raquo
Copy link
Owner Author

raquo commented Jan 27, 2024

@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.

@HollandDM
Copy link
Contributor

@raquo Don't worry, I can still use my internal implementation in the mean time. Take your time!

@felher
Copy link

felher commented Feb 16, 2024

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
design help wanted Extra attention is needed need to find time https://falseknees.com/297.html
Projects
None yet
Development

No branches or pull requests

3 participants