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

Redesign encoding of element DSL #180

Open
armanbilge opened this issue Feb 3, 2023 · 10 comments
Open

Redesign encoding of element DSL #180

armanbilge opened this issue Feb 3, 2023 · 10 comments
Labels
enhancement New feature or request
Milestone

Comments

@armanbilge
Copy link
Owner

armanbilge commented Feb 3, 2023

Context:

What we have now is pretty good, thanks to Laminar's influence. But I feel now that it is wrong, and we can do better. This should also open doors to better support web components and stuff.

Basically, I believe we need only two types: tags and attributes.

Tags should be similar to our existing encoding. Something like this:

class Tag[F[_], E](name: String):
  def apply[M](modifiers: M)(using shapeless magic): Resource[F, E]

Then, for attributes, we should have something like this:

class Attribute[F[_], A]:
  def :=[E, V](value: V)(using Setter[F, E, A, V]): Modifier[F, E, Setter]
  def <--[E, V](value: Signal[F, V])(using Setter[F, E, A, V]): Modifier[F, E, SignalSetter]
  def -->[E, Ev](listener: Pipe[F, Ev, Nothing])(using Emits[F, E, Ev]): Modifier[F, E, Listener]

What is Setter[F, E, A, V]? It is evidence, that you are allowed to set a value of type V to an attribute A on an element E. It will also provide the implementation for how to do this, since it depends.

Similarly, Emits[F, E, Ev] is evidence that element E emits events of type Ev.

This enables some interesting things:

  1. Attributes A can be used only with the specific element E that actually support them.
  2. In fact, an attribute A may have a different implementation or even a different type depending on the element E it is modifying.
  3. Anyone can implement custom tags (e.g. in a web component library) that support certain attributes, simply by implementing these typeclasses.

If this works and type inference is ergonomic, then this should be a completely source-compatible change, hidden from users.

@armanbilge armanbilge added the enhancement New feature or request label Feb 3, 2023
@armanbilge armanbilge added this to the v0.3.0 milestone Feb 3, 2023
@armanbilge
Copy link
Owner Author

One shortcoming of this encoding is that the appropriate types / docs for a particular attribute will be very unclear.

@armanbilge
Copy link
Owner Author

armanbilge commented Feb 4, 2023

I prototyped this out a bit further. Not sure about my proposed encoding above, but this works (and is simpler):

//> using lib "org.typelevel::shapeless3-deriving:3.3.0"
//> using option "-Ykind-projector:underscores"
import shapeless3.deriving.K0

trait Modifier[E, M]

trait SetAttribute[A, V]

class Tag[E]:
  def apply[M <: Tuple](modifiers: M)(using
      K0.ProductInstances[Modifier[E, _], M]
  ): E = ???

class Attribute[A]:
  def :=[V](value: V): SetAttribute[A, V] = ???

// common to both
def sharedAttr: Attribute["shared"] = ???

// only on foo
def fooAttr: Attribute["foo"] = ???

// only on bar
def barAttr: Attribute["bar"] = ???

// on foo and bar, but with different types
def fizzBuzzAttr: Attribute["fizzbuzz"] = ???

trait Element
given sharedForElement[E <: Element]: Modifier[E, SetAttribute["shared", String]] = ???

trait FooElement extends Element
given fooForFooElement[E <: FooElement]: Modifier[E, SetAttribute["foo", String]] = ???
given fizzBuzzForFooElement[E <: FooElement]: Modifier[E, SetAttribute["fizzbuzz", Boolean]] = ???

trait BarElement extends Element
given barForBarElement[E <: BarElement]: Modifier[E, SetAttribute["bar", String]] = ???
given fizzBuzzForBarlement[E <: BarElement]: Modifier[E, SetAttribute["fizzbuzz", Int]] = ???


def fooTag: Tag[FooElement] = ???
def barTag: Tag[BarElement] = ???

def demo =
  fooTag(
    sharedAttr := "hello",
    fooAttr := "foo",
    fizzBuzzAttr := true
  )

  barTag(
    sharedAttr := "hello",
    barAttr := "bar",
    fizzBuzzAttr := 42
  )

@armanbilge
Copy link
Owner Author

armanbilge commented Feb 5, 2023

Here's another variation, that uses ordinary Strings to encode attributes.

//> using lib "org.typelevel::shapeless3-deriving:3.3.0"
//> using option "-Ykind-projector:underscores"
import shapeless3.deriving.K0

trait Modifier[E, M]

trait SetAttribute[A, V]

class Tag[E]:
  def apply[M <: Tuple](modifiers: M)(using
      K0.ProductInstances[Modifier[E, _], M]
  ): E = ???

extension [A <: Singleton](attr: A)
  def :=[V](value: V): SetAttribute[A, V] = ???

trait Element
given sharedForElement[E <: Element]: Modifier[E, SetAttribute["shared", String]] = ???

trait FooElement extends Element
given fooForFooElement[E <: FooElement]: Modifier[E, SetAttribute["foo", String]] = ???
given fizzBuzzForFooElement[E <: FooElement]: Modifier[E, SetAttribute["fizzbuzz", Boolean]] = ???

trait BarElement extends Element
given barForBarElement[E <: BarElement]: Modifier[E, SetAttribute["bar", String]] = ???
given fizzBuzzForBarlement[E <: BarElement]: Modifier[E, SetAttribute["fizzbuzz", Int]] = ???


def fooTag: Tag[FooElement] = ???
def barTag: Tag[BarElement] = ???

def demo =
  fooTag(
    "shared" := "hello",
    "foo" := "foo",
    "fizzbuzz" := true
  )

  barTag(
    "shared" := "hello",
    "bar" := "bar",
    "fizzbuzz" := 42
  )

I'm not sure yet how I feel about it.

Cons

  • more noisy, because of all the "
  • if we made a hard switch, it would be a breaking change
  • less compatibility with other libraries

Pros

  • no method call or allocations!
  • no pollution of the global namespace
  • it can exactly match the HTML names
  • any component library can define its own attributes without fanfare, and without fear of clashes

Probably in the end, will support both styles to some extent 🤔 but this could also be highly confusing.

@hejfelix
Copy link

Pick one for your own and everyone's sake 😁 looks like a nice way to go IMHO

@hejfelix
Copy link

I vote for the non-string props version

@armanbilge
Copy link
Owner Author

armanbilge commented Feb 12, 2023

@hejfelix btw one downside of this proposed change, is that attributes will no longer be Functors or Covariant. This is because there is no longer a type associated with a particular attribute, since it depends on the element it is applied to. We can workaround this with some additional syntax, but it might not be as nice for your usecase.

@hejfelix
Copy link

hejfelix commented Feb 12, 2023

Yeah, that would suck. What would the workaround look like?

edit nonetheless it seems like the right way going forward

@armanbilge
Copy link
Owner Author

armanbilge commented Feb 12, 2023

What would the workaround look like?

Actually, let me get back to you on this. In my experiments above, I only played out the key := value case.

I still need to try onEvent --> sink to verify if/how type inference works 🤔 because now I'm not so sure.

If type inference doesn't work, then we'd have to continue coding the specific Event type into each onEvent attribute, which means it can continue to be a Functor, which works out for you.

And maybe this is okay, clashes between onEvent attributes (same name but different event type) might be less likely 😅

@hejfelix
Copy link

Yeah I recall pleasing the type inference for the mods is a pain. Very happy that this project exists ❤️ thanks for working hard on this!

@armanbilge
Copy link
Owner Author

Ok, I have a new iteration on this idea that I think supports everything we want to do here (and possibly more), without reneging on the Functor and Covariant instances :)

The idea is basically to re-encode tags to inject its type as an invisible context. Then all attributes within the tag can use that for inference.

class Tag[E]:
  def apply[M <: Tuple](modifiers: TagContext[E] ?=> M)(using
      K0.ProductInstances[Modifier[E, _], M]
  ): E = ???

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

2 participants