在我的应用程序中,我有一堆能够渲染HTML的组件:

class StandardComponent {
  def render: Html
}

它们在运行时由ComponentDefinitionComponentBuilder对象实例化,它提供对运行时数据的访问:
class ComponentBuilder {
  def makeComponent(componentDef: ComponentDefinition): StandardComponent
}

然后,有几个帮助程序可以帮助在组件中呈现子组件:
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]

问题是,组件经常需要使用其中的几个from*调用。尽管它们被设计为可嵌套的,但可能会变得一团糟:
implicit val componentBuilder: ComponentBuilder = ???

val subComponent: ComponentDefinition = ???
val subComponents: Seq[ComponentDefinition] = ???
val subComponentOpt: Option[ComponentDefinition] = ???

fromComponent(subComponent) { html =>
  fromComoponents(subComponents) { htmls =>
    fromOptionalComponent(subComponentOpt) { optHtml =>
      ???
    }
  }
}

我想做的大概是:
withSubComponents(
  subComponent, subComponents, subComponentOpt
) { case (html, htmls, optHtml) => /* as Html, Seq[Html], and Option[Html] */
  ???
}

因此,我想在其参数中使withSubComponents可变,并且在其第二个参数列表中使它采用的闭包具有一个参数列表,该列表取决于arity和type中的第一个参数列表。理想情况下,它也需要隐式的ComponentBuilder,就像各个助手一样。那是理想的语法,但是我愿意接受其他选择。我可以提供一些到目前为止我所拥有的例子,但是到目前为止我所拥有的只是想法。感觉就像我需要创建一个CoProduct的HList,然后需要将两个参数绑定(bind)在一起的方法。

最佳答案

改善DSL的第一步可以是将方法移至隐式转换,如下所示:

implicit class SubComponentEnhancements[T](subComponent: T)(
  implicit cb: ComponentBuilder[T]) {

  def fromComponent(f: cb.HtmlType => Future[Html]): Future[Html] = ???
}

请注意,我声明fromComponent对定义了T的每种类型ComponentBuilder有效。如您所见,我还想象ComponentBuilder具有HtmlType。在您的示例中,应该是Seq[Html]Option[Html]等。ComponentBuilder现在看起来像这样:
trait ComponentBuilder[T] {
  type HtmlType
  def render(componentDef: T): HtmlType
}

我还想象ComponentBuilder能够将组件呈现为某种类型的Html。让我们声明一些组件构建器,以便能够在不同类型上调用fromComponent方法。
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))
    }
}

请注意,每个组件构建器都指定了一个与HtmlType类型同步的ComponentBuilder。容器类型的构建器只是向组件构建器请求其内容。这使我们可以嵌套不同的组合而无需付出太多额外的努力。我们可以进一步推广该概念,但是现在很好。

至于single组件构建器,您可以定义更通用的名称,从而可以使用不同类型的组件定义。可以使用位于服务器不同位置的Converter(将X的伴侣对象,Converter的伴随对象或用户需要手动导入的单独对象)转换为标准组件。
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
  }

无论如何,现在的代码如下所示:
subComponent fromComponent { html =>
  subComponents fromComponent { htmls =>
    subComponentOpt fromComponent { optHtml =>
      ???
    }
  }
}

这看起来像一个熟悉的模式,让我们重命名方法:
subComponent flatMap { html =>
   subComponents flatMap { htmls =>
     subComponentOpt map { optHtml =>
       ???
     }
   }
 }

请注意,我们处于一厢情愿的思想空间中,以上代码将无法编译。如果我们有某种使其编译的方法,则可以编写如下内容:
for {
  html <- subComponent
  htmls <- subComponents
  optHtml <- subComponentOpt
} yield ???

对我来说,这真是太神奇了,不幸的是OptionSeq本身都有flatMap函数,因此我们需要隐藏它们。以下代码看起来很干净,使我们有机会隐藏flatMapmap方法。
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 ???

如您所见,我们仍处于一厢情愿的思维空间,让我们看看是否可以填补空白。
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))
}

实现并不是那么复杂,因为我们可以使用之前创建的工具。请注意,在一厢情愿的思考中,我实际上需要html时返回了Wrapper[T],因此现在使用组件构建器中的HtmlType

为了改善类型推断,我们将略微更改ComponentBuilder。我们将HtmlType类型成员更改为类型参数。
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))
}

不同的建造者也需要改变
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))
    }
}

现在的最终结果如下所示:
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

您可能已经注意到,我跳过了Future,您可以根据需要自己添加它。

我不确定这是否是您对DSL的设想,但它至少为您提供了一些工具,可以创建一个真正酷的工具。

10-04 17:40