Silent exploration
In my application, I have a bunch of components capable of rendering Html:
class StandardComponent {
def render: Html
}
They are run at runtime from ComponentDefinition
objects with a help ComponentBuilder
that provides runtime data access:
class ComponentBuilder {
def makeComponent(componentDef: ComponentDefinition): StandardComponent
}
Then there are a few helpers that make it easier to render subcomponents within components:
def fromComponent(componentDef: ComponentDefinition)(htmlFn: Html => Future[Html])(implicit componentBuilder: ComponentBuilder): Future[Html]
def fromComponents(componentDefs: Seq[ComponentDefinition])(htmlFn: Seq[Html] => Future[Html])(implicit componentBuilder: ComponentBuilder): Future[Html]
def fromOptionalComponent(componentDefOpt: Option[ComponentDefinition])(htmlFn: Option[Html] => Future[Html])(implicit componentBuilder: ComponentBuilder): Future[Html]
def fromComponentMap[K](componentDefMap: Map[K, ComponentDefinition])(htmlFn: Map[K, Html] => Future[Html])(implicit componentBuilder: ComponentBuilder): Future[Html]
The problem is that often a component must use multiple of these calls from*
. Even though they are intended to be nested, they can get confusing:
implicit val componentBuilder: ComponentBuilder = ???
val subComponent: ComponentDefinition = ???
val subComponents: Seq[ComponentDefinition] = ???
val subComponentOpt: Option[ComponentDefinition] = ???
fromComponent(subComponent) { html =>
fromComoponents(subComponents) { htmls =>
fromOptionalComponent(subComponentOpt) { optHtml =>
???
}
}
}
What I would like to do is about the same:
withSubComponents(
subComponent, subComponents, subComponentOpt
) { case (html, htmls, optHtml) => /* as Html, Seq[Html], and Option[Html] */
???
}
So, I want to make a withSubComponents
variadic variable in my arguments, and I want the closure that is required for its second argument list to have an argument list that depends on the first argument list in arty and type. Ideally, it also accepts an implicit one ComponentBuilder
, as the individual helpers do. This is the perfect syntax, but I am open to alternatives. I could give some examples of what I have so far, but all I have is ideas so far. It seems to me that I need to create an HList CoProduct and then I need a way to link the two arguments together.
source to share
The first step in improving the DSL could be to move the methods into an implicit conversion like this:
implicit class SubComponentEnhancements[T](subComponent: T)(
implicit cb: ComponentBuilder[T]) {
def fromComponent(f: cb.HtmlType => Future[Html]): Future[Html] = ???
}
Note that I have declared it fromComponent
valid for every type T
that has ComponentBuilder
. As you can see, I also assumed it ComponentBuilder
has HtmlType
. In your example, that would be Seq[Html]
, Option[Html]
etc. ComponentBuilder
now looks like this:
trait ComponentBuilder[T] {
type HtmlType
def render(componentDef: T): HtmlType
}
I also assumed I was ComponentBuilder
able to map a component to some type Html
. Let's declare some component builders. The ability to call a method fromComponent
for different types.
object ComponentBuilder {
implicit def single =
new ComponentBuilder[ComponentDefinition] {
type HtmlType = Html
def render(componentDef: ComponentDefinition) = {
// Create standard component from a component definition
val standardComponent = new StandardComponent
standardComponent.render
}
}
implicit def seq[T](
implicit cb: ComponentBuilder[T]) =
new ComponentBuilder[Seq[T]] {
type HtmlType = Seq[cb.HtmlType]
def render(componentDef: Seq[T]) =
componentDef.map(c => cb.render(c))
}
implicit def option[T](
implicit cb: ComponentBuilder[T]) =
new ComponentBuilder[Option[T]] {
type HtmlType = Option[cb.HtmlType]
def render(componentDef: Option[T]) =
componentDef.map(c => cb.render(c))
}
}
Note that each of the component builders specifies HtmlType
which is synchronized with the type ComponentBuilder
. Builders for container types simply ask the component builder for their content. This allows us to set up different combinations without any extra effort. We could generalize this concept even further, but so far that's fine.
With regard to the component builder single
, you can define a more general way that allows you to have different types of component definitions. Converting them to a standard component can be done with a help Converter
that can be located in different places on the server (companion object X
, companion object, Converter
or a separate object that users must import manually).
trait Converter[X] {
def convert(c:X):StandardComponent
}
object ComponentDefinition {
implicit val defaultConverter =
new Converter[ComponentDefinition] {
def convert(c: ComponentDefinition):StandardComponent = ???
}
}
implicit def single[X](implicit converter: Converter[X]) =
new ComponentBuilder[X] {
type HtmlType = Html
def render(componentDef: X) =
converter.convert(componentDef).render
}
In any case, the code now looks like this:
subComponent fromComponent { html => subComponents fromComponent { htmls => subComponentOpt fromComponent { optHtml => ??? } } }
It looks like a familiar pattern for renaming methods:
subComponent flatMap { html => subComponents flatMap { htmls => subComponentOpt map { optHtml => ??? } } }
Please note that we are in the desired thinking space, the above code will not compile. If we had some way to do this, we could write something like the following:
for {
html <- subComponent
htmls <- subComponents
optHtml <- subComponentOpt
} yield ???
This looks pretty amazing to me, unfortunately Option
and Seq
has a function flatMap
, so we need to hide them. The following code looks clean and gives us the ability to hide the flatMap
and methods map
.
trait Wrapper[+A] {
def map[B](f:A => B):Wrapper[B]
def flatMap[B](f:A => Wrapper[B]):Wrapper[B]
}
implicit class HtmlEnhancement[T](subComponent:T) {
def html:Wrapper[T] = ???
}
for {
html <- subComponent.html
htmls <- subComponents.html
optHtml <- subComponentOpt.html
} yield ???
As you can see, we are still in the space of desired reverie, let's see if we can fill in the gaps.
case class Wrapper[+A](value: A) {
def map[B](f: A => B) = Wrapper(f(value))
def flatMap[B](f: A => Wrapper[B]) = f(value)
}
implicit class HtmlEnhancement[T](subComponent: T)(
implicit val cb: ComponentBuilder[T]) {
def html: Wrapper[cb.HtmlType] = Wrapper(cb.render(subComponent))
}
The implementation is not that difficult because we can use the tools we created earlier. Note that during wishful thinking I returned Wrapper[T]
while we really need the html, so now I am using HtmlType
from the component linker.
To improve type inference, we'll change a bit ComponentBuilder
. We will change the type member to a type HtmlType
parameter.
trait ComponentBuilder[T, R] {
def render(componentDef: T): R
}
implicit class HtmlEnhancement[T, R](subComponent: T)(
implicit val cb: ComponentBuilder[T, R]) {
def html:Wrapper[R] = Wrapper(cb.render(subComponent))
}
Various developers also need to change
object ComponentBuilder {
implicit def single[X](implicit converter: Converter[X]) =
new ComponentBuilder[X, Html] {
def render(componentDef: X) =
converter.convert(componentDef).render
}
implicit def seq[T, R](
implicit cb: ComponentBuilder[T, R]) =
new ComponentBuilder[Seq[T], Seq[R]] {
def render(componentDef: Seq[T]) =
componentDef.map(c => cb.render(c))
}
implicit def option[T, R](
implicit cb: ComponentBuilder[T, R]) =
new ComponentBuilder[Option[T], Option[R]] {
def render(componentDef: Option[T]) =
componentDef.map(c => cb.render(c))
}
}
The end result now looks like this:
val wrappedHtml =
for {
html <- subComponent.html
htmls <- subComponents.html
optHtml <- subComponentOpt.html
} yield {
// Do some interesting stuff with the html
htmls ++ optHtml.toSeq :+ html
}
// type of `result` is `Seq[Html]`
val result = wrappedHtml.value
// or
val Wrapper(result) = wrappedHtml
As you may have noticed I missed it Future
, you can add this as you wish.
I'm not sure if this is how you envisioned the DSL, but it at least gives you some tools to create a really cool one.
source to share