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.

+3


source to share


1 answer


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.

+1


source







All Articles