在我的应用程序中,我有一堆能够呈现Html的组件:
class StandardComponent {
def render: Html
}
它们在ComponentDefinition
对象的运行时由ComponentBuilder
实例化,提供对运行时数据的访问:
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,然后我需要把这两个参数绑定在一起的方式。
答案 0 :(得分:1)
改进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 ???
注意,我们处于一厢情愿的空间,上面的代码不会编译。如果我们有一些方法可以编译它,我们可以编写如下内容:
Option
对我来说这看起来非常棒,不幸的是Seq
和flatMap
本身就有flatMap
功能,所以我们需要隐藏这些功能。以下代码看起来很干净,并为我们提供了隐藏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 ???
方法的机会。
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))
}
正如你所看到的,我们仍然处于一厢情愿的空间,让我们看看我们是否可以填补这些空白。
Wrapper[T]
实现并不复杂,因为我们可以使用我们之前创建的工具。请注意,在一厢情愿的过程中,我在实际需要html时返回了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
您可能已经注意到,我跳过{{1}},您可以随意添加。
我不确定你是不是想象你的DSL,但它至少给你一些工具来创造一个非常酷的。