【问题标题】:Implicit conversion not working with type-safe builder pattern隐式转换不适用于类型安全的构建器模式
【发布时间】:2013-05-12 21:47:49
【问题描述】:

我正在使用 Scala 类型安全构建器模式来处理简单的休息请求。这作为一个流畅的 api 非常有用。

sealed abstract class Method(name: String)

case object GET extends Method("GET")
case object POST extends Method("POST")

abstract class TRUE
abstract class FALSE

case class Builder[HasMethod, HasUri](
  method: Option[Method],
  uri: Option[String]) {

  def withMethod(method: Method): Builder[TRUE, HasUri] = copy(method = Some(method))
  def withUri(uri: String): Builder[HasMethod, TRUE] = copy(uri = Some(uri))
}

implicit val init: Builder[FALSE, FALSE] = Builder[FALSE, FALSE](None, None)

//Fluent examples
val b1: Builder[TRUE, FALSE] = init.withMethod(GET)
val b2: Builder[TRUE, TRUE] = init.withMethod(GET).withUri("bar")

我想通过允许将Method 实例转换为Builder 实例来使其更像DSL,但是当我添加尝试隐式包含init 构建器时,隐式转换和类型参数的组合混淆编译器。

implicit def toMethod[HasUri](m: Method)
  (implicit builder: Builder[_, HasUri]): Builder[TRUE, HasUri] = builder.withMethod(m)

// ** ERROR **: could not find implicit value for parameter builder: 
//              Builder[_, HasUri]  
val b3: Builder[TRUE, TRUE] = GET withUri "foo"

// However the implicit parameter is discovered fine when function is called directly
val b4: Builder[TRUE, FALSE] = toMethod(GET)
val b5: Builder[TRUE, TRUE] = toMethod(GET) withUri "foo"

除 b3 之外的所有行都编译。当显式调用 toMethod 函数时,可以隐式找到 builder 参数。此外,如果我删除通用参数(和类型安全),代码将按预期工作。

这是 scala 隐式转换的限制吗?还是我错过了实现此目的的正确语法?

我想隐式发现初始构建器实例,以使用户能够为他们自己的初始构建器提供一些构建器字段的默认值。

更新

为了使示例简单,我省略了一些代码,因为它只是我要修复的隐式转换。

类型安全的构建器模式在这里得到了很好的概述:http://blog.rafaelferreira.net/2008/07/type-safe-builder-pattern-in-scala.html

之后,您只能在 Builder 具有方法和 uri 时调用 build 方法。

我想发现builder作为隐式参数的原因是为了支持DSL中的以下情况。

url("http://api.service.org/person") apply { implicit b =>
  GET assert(Ok and ValidJson)
  GET / "john.doe" assert(NotFound)
  POST body johnDoeData assert(Ok)
  GET / "john.doe" assert(Ok and bodyIs(johnDoeData))
}

在这些情况下

  1. 使用url 指定的 uri 创建了一个新构建器
  2. 然后在闭包中重复使用 implicit b =>
  3. assert 方法只有在指定了 uri 和方法后才可用
  4. / 附加到当前 uri,这仅在构建器指定了 uri 时才可用。

另一个指定方法和uri的例子

GET url("http://api.service.org/secure/person") apply { implicit b =>
  auth basic("harry", "password") assert(Ok and ValidJson)
  auth basic("sally", "password") assert(PermissionDenied)
}

【问题讨论】:

  • 你可能想看看这个,看看它是否能说明你的隐式函数有一个隐式参数。 stackoverflow.com/questions/5080406/…
  • 我会重新考虑设计。您正在以一种奇怪的方式复制信息 - 一方面您有一个类型构造函数参数指示构建器在编译时是否有方法或 url,同时您有只能在运行时解决的选项。问题是,你想达到什么目的(应该在哪里检查类型)?如果您使用类型参数,请创建带有方法和 url(而不是选项)的子类。最后,您想将方法提升为构建器。无需检查现有的 HasUri 类型,它应该始终是 FALSE?
  • @cmbaxter,谢谢,我将尝试使用这些调试选项编译我的代码,看看它是否有任何启发。
  • @0__,您说得对,我正在使用幻像类型复制选项的运行时状态。这是为了让编译器跟踪设置了哪些选项以及何时可以使用构建器执行请求。我没有将其建模为特征和子类,因为当您尝试跟踪更多字段和不同选项时会发生组合爆炸(一些 java fluent api 会这样做,而 api 很好地使用它背后的代码是可怕的)。我已经更新了我的问题,以显示我正在尝试支持的 DSL。

标签: scala dsl type-inference implicit-conversion


【解决方案1】:

我感觉您的隐式解析问题并非来自 Scala 类型系统中的任何限制,而是取决于您在此处指定的存在类型:

implicit def toMethod[HasUri](m: Method)
  (implicit builder: Builder[_, HasUri]): Builder[TRUE, HasUri] = builder.withMethod(m)

如果我没记错的话,在这种情况下,存在类型被视为 Nothing。没有任何东西是每个可能的 Scala 类的子类,因此您的方法实际上变成了:

implicit def toMethod[HasUri](m: Method)
  (implicit builder: Builder[Nothing, HasUri]): Builder[TRUE, HasUri] = builder.withMethod(m)

Scala 将在当前范围内查找 Builder[Nothing,HasUri] 的子类以提供给您的方法,并且除了 Builder[Nothing,HasUri] 之外没有任何类可以匹配所需类型,因为您的构建器类是不变的,即Builder[A,B]<:<Builder[C,D] iff A=:=C & B=:=D

因此,您有两个选择:

  • 给签名增加一个参数,toMethod[HasUri]变成toMethod[A,HasUri]
  • 利用 Scala 正确实现类型差异

由于您想强制您的 Builder[A,HasUri] 是 Builder[Nothing,HasUri] 的子类,并且

Nothing <:< A for any A

您想强制执行 Builder[A,HasUri] &lt;:&lt; Builder[B,HasUri] iff B&lt;:&lt;A 即 Builder 在其第一个类型参数中是有争议的。您可以通过在类型前面放置一个 - 符号来强制控制:

Builder[-HasMethod, HasUri] 在 HasMethod 中是协变的,在 HasUri 中是不变的


结论

类型系统很强大,但即使是简单的任务也不一定要使用复杂的模式:

  • HasUri 不是从 m 推断出来的,因为它是方法 toMethod 的类型参数
  • HasMethod 未被推断,因为您使用 _ 擦除它

如果参数不包含在您的解析中,那么使用带有两个泛型参数的隐式参数有什么意义?我只想写:

case class DefaultBuilder(m:Method) extends Builder[True,HasUri]

当你最终遇到这种情况时,正如有人已经说过的那样,那是因为你的设计错误地解决了问题。你能解释一下为什么构建器必须隐含在 toMethod 中吗?

implicit def toMethod(m:Method) = DefaultBuilder(m)

【讨论】:

  • 感谢您非常详细的回答。今晚我会试试你的建议。关于:如果参数不包含在您的解析中,那么使用带有两个泛型参数的隐式参数有什么意义?隐式参数用于获取 DSL 中的当前默认构建器。泛型正在捕获构建器的状态,build() 仅在 Builder[TRUE, TRUE] 上可用。 toMethod 通过将方法添加到当前的默认值来创建一个新的构建器。此默认设置可能有也可能没有设置方法和 url,新的将设置方法。
  • 那么好主意是隐含一个 BuildingScope 和一个 defaultBuilder 来解决
【解决方案2】:

此代码现在在 Scala 2.11 中按原样工作,但在 Scala 2.10(我用来编写此原始代码)中不起作用。

我一直在寻找可能出现这种情况的原因,并且只能在 scala-lang 的 jira 中找到这个错误。

https://issues.scala-lang.org/browse/SI-3346

我在 Scala 2.10 中尝试了多种方法来解决此问题,但都无法解决。其中包括@Edmondo1984 的建议和限制HasMethodHasUri 参数如下:

case object GET extends Method("GET")
case object POST extends Method("POST")

sealed trait TBool
trait TTrue extends TBool
trait TFalse extends TBool

case class Builder[HasMethod <: TBool, HasUri <: TBool](method: Option[Method],
                                                        uri: Option[String]) {

  def withMethod(method: Method): Builder[TTrue, HasUri] = copy(method = Some(method))
  def withUri(uri: String): Builder[HasMethod, TTrue] = copy(uri = Some(uri))
}

object Builder {
  implicit val init: Builder[TFalse, TFalse] = Builder[TFalse, TFalse](None, None)

  // Example build method
  implicit class CanExecute(builder: Builder[TTrue, TTrue]) {
    def execute(): String = s"Build(${builder.method} ${builder.uri}"
  }
}


//Fluent examples
val b1: Builder[TTrue, TFalse] = init.withMethod(GET)
val b2: Builder[TTrue, TTrue] = init.withMethod(GET).withUri("bar")


implicit def toMethod[HasUri <: TBool](m: Method)
                                      (implicit builder: Builder[_, HasUri]): Builder[TTrue, HasUri] = builder.withMethod(m)

// ** ERROR **: could not find implicit value for parameter builder:
//              Builder[_, HasUri]
// ** BUT ** Works in Scala 2.11
val b3: Builder[TTrue, TTrue] = GET withUri "foo"

GET withUri "foo" execute ()

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-05-30
    • 1970-01-01
    • 2011-09-18
    • 2014-03-20
    • 1970-01-01
    相关资源
    最近更新 更多