Scala API中的选项和命名默认参数是不是像油和水?

Posted

技术标签:

【中文标题】Scala API中的选项和命名默认参数是不是像油和水?【英文标题】:Are Options and named default arguments like oil and water in a Scala API?Scala API中的选项和命名默认参数是否像油和水? 【发布时间】:2011-05-11 02:35:12 【问题描述】:

我正在开发一个 Scala API(顺便说一下,对于 Twilio),其中操作具有大量参数,其中许多参数具有合理的默认值。为了减少打字并提高可用性,我决定使用带有命名和默认参数的案例类。例如 TwiML Gather 动词:

case class Gather(finishOnKey: Char = '#', 
                  numDigits: Int = Integer.MAX_VALUE, // Infinite
                  callbackUrl: Option[String] = None, 
                  timeout: Int = 5
                  ) extends Verb

这里感兴趣的参数是callbackUrl。它是唯一真正可选的参数,如果没有提供值,则不会应用任何值(这是完全合法的)。

我已将其声明为一个选项,以便在 API 的实现端使用它执行 monadic map 例程,但这会给 API 用户带来一些额外的负担:

Gather(numDigits = 4, callbackUrl = Some("http://xxx"))
// Should have been
Gather(numDigits = 4, callbackUrl = "http://xxx")

// Without the optional url, both cases are similar
Gather(numDigits = 4)

据我所知,有两个选项(不是双关语)可以解决这个问题。要么让 API 客户端导入隐式转换到范围:

implicit def string2Option(s: String) : Option[String] = Some(s)

或者我可以使用 null 默认值重新声明案例类,并将其转换为实现端的选项:

case class Gather(finishOnKey: Char = '#', 
                  numDigits: Int = Integer.MAX_VALUE, 
                  callbackUrl: String = null, 
                  timeout: Int = 5
                  ) extends Verb

我的问题如下:

    有没有更优雅的方法来解决我的特殊情况? 更一般地说:命名参数是一种新的语言特性 (2.8)。难道选项和命名的默认参数就像油和水一样吗? :) 在这种情况下,使用空默认值可能是最佳选择吗?

【问题讨论】:

感谢您为问题 1 和 3 提供了很多好的答案!在决定是使用 Aaron 的 Opt、坚持使用 Option 还是仅使用 null 默认值之前,我将实现更多 API 使用示例用例。可惜没有明显的选择——也许我们需要一个新的语言特性来让选择变得不费脑筋? 【参考方案1】:

这是另一个解决方案,部分灵感来自Chris' answer。还涉及到一个wrapper,但是wrapper是透明的,你只需要定义一次,API的用户不需要导入任何转换:

class Opt[T] private (val option: Option[T])
object Opt 
   implicit def any2opt[T](t: T): Opt[T] = new Opt(Option(t)) // NOT Some(t)
   implicit def option2opt[T](o: Option[T]): Opt[T] = new Opt(o)
   implicit def opt2option[T](o: Opt[T]): Option[T] = o.option


case class Gather(finishOnKey: Char = '#', 
                  numDigits: Opt[Int] = None, // Infinite
                  callbackUrl: Opt[String] = None, 
                  timeout: Int = 5
                 ) extends Verb

// this works with no import
Gather(numDigits = 4, callbackUrl = "http://xxx")
// this works too
Gather(numDigits = 4, callbackUrl = Some("http://xxx"))
// you can even safely pass the return value of an unsafe Java method
Gather(callbackUrl = maybeNullString())

为了解决更大的设计问题,我认为选项和命名默认参数之间的交互并不像乍看之下那样多油多水。可选字段和具有默认值的字段之间存在明确的区别。可选字段(即Option[T] 类型之一)可能从不有值。另一方面,具有默认值的字段根本不需要将其值作为参数提供给构造函数。因此,这两个概念是正交的,因此字段可能是可选的并具有默认值也就不足为奇了。

也就是说,我认为对于此类字段使用Opt 而不是Option 可以提出合理的论点,而不仅仅是为客户节省一些输入。这样做会使 API 更加灵活,因为您可以将 T 参数替换为 Opt[T] 参数(反之亦然),而不会破坏构造函数的调用者[1]。

至于为公共字段使用null 默认值,我认为这是个坏主意。 “您”可能知道您需要null,但访问该字段的客户端可能不知道。即使该字段是私有的,当其他开发人员必须维护您的代码时,使用null 也是自找麻烦。所有关于 null 值的常见论点都在这里发挥作用——我不认为这个用例有任何特殊例外。

[1] 前提是您移除 option2opt 转换,以便调用者在需要 Opt[T] 时必须传递 T

【讨论】:

我以前使用null 默认值,但我真的很喜欢这个解决方案。 不错。它解决了所有问题,但缺点是在 API 接口中引入了一种新的(对用户而言)未知类型。 @DaGGeRRz 是的,但我认为成本可以忽略不计。它是一种实用程序类型,用户只需学习一次即可从整个代码库中受益,并且是一种非常简单的类型。【参考方案2】:

不要将任何内容自动转换为选项。使用my answer here,我认为你可以很好地做到这一点,但要以类型安全的方式。

sealed trait NumDigits  /* behaviour interface */ 
sealed trait FallbackUrl  /* behaviour interface */ 
case object NoNumDigits extends NumDigits  /* behaviour impl */ 
case object NofallbackUrl extends FallbackUrl  /* behaviour impl */ 

implicit def int2numd(i : Int) = new NumDigits  /* behaviour impl */ 
implicit def str2fallback(s : String) = new FallbackUrl  /* behaviour impl */ 

class Gather(finishOnKey: Char = '#', 
              numDigits: NumDigits = NoNumDigits, // Infinite
              fallbackUrl: FallbackUrl = NoFallbackUrl, 
              timeout: Int = 5

然后您可以随意调用它 - 显然,将您的 behaviour 方法添加到 FallbackUrlNumDigits 中。这里的主要缺点是它是大量的样板

Gather(numDigits = 4, fallbackUrl = "http://wibble.org")

【讨论】:

如果解决方案正在导入隐式转换,我发现这比导入 string2option 不太可能造成混乱,但我想完全避免转换。在 API 实现方面,只处理空值会更省力... 澄清我的其他评论。从某种意义上说,您的方法很好,因为它是类型安全的,并且如果我在传递的参数中需要更多行为,它会非常强大。但我没有。 :) 为所有参数类型添加一个包装类和隐式转换是 IMO 太笨重了。此外,它使 API 文档变得不那么清晰。当只能使用整数和字符串时,签名将指示 WrapperX 类型参数... 是的 - 我完全同意你的所有观点。就我个人而言,我会接受你原来的建议;即默认为Some("url") 我猜我们都信奉无空论。想一想,URL 通常会使用某种 URL 构建器 (AbsoluteUrl.apply(relativeUrl: String)) 构建,它可以返回一个选项,所以我可能会幸运地休息一下。 :) 不过,问题 2 仍然存在。【参考方案3】:

就个人而言,我认为在这里使用“null”作为默认值是完全可以的。当您想向您的客户传达可能未定义某些内容时,使用 Option 而不是 null。所以返回值可以声明为 Option[...],或者是抽象方法的方法参数。这使客户免于阅读文档,或者更有可能因为没有意识到某些内容可能为空而获得 NPE。

在您的情况下,您知道可能存在空值。如果您喜欢 Option 的方法,只需在方法的开头执行val optionalFallbackUrl = Option(fallbackUrl)

但是,这种方法仅适用于 AnyRef 类型。如果您想对任何类型的参数使用相同的技术(而不导致 Integer.MAX_VALUE 作为 null 的替换),那么我想您应该选择其他答案之一

【讨论】:

我会采用该解决方案 - 因为您使用 Option 的唯一原因是获得一个没有价值的状态。但是null已经存在这样的状态了,反正你也得去处理。 是的,我倾向于使用 null 并且您对此表示赞同。关于Integer.MAX_VALUE,用来表示默认位数是无限的。 实际上,我强烈反对为案例类字段使用null 默认值。由于该字段自动公开,所有避免nulls 的常见原因都适用。即使在类定义中,当您考虑长期可维护性时,具有潜在的null 值也容易出错。 在这种情况下,将其设为私有 '_fallbackUrl' 并使用 'val fallbackUrl = Option(_fallbackUrl)'。你会得到一个不错的 Option val,同时仍然保持类客户端的易用性 当一些构造函数参数有前导下划线而其他没有时,易用性会受到影响:Gather(timeout = 10, _callbackUrl = "http://my.org")。虽然 field 是私有的,但它的 name 仍然是公共 API 的一部分。此外,公共字段 (callbackUrl) 与构造函数参数 (_callbackUrl) 的名称不同这一事实可能会导致混淆。【参考方案4】:

我认为只要 Scala 不支持真正的 void(解释如下)“类型”,从长远来看,使用 Option 可能是更清洁的解决方案。甚至可能适用于所有默认参数。

问题是,使用您的 API 的人知道您的某些参数是默认的,不妨将它们作为可选参数处理。所以,他们将它们声明为

var url: Option[String] = None

一切都很好,很干净,他们可以等着看是否有任何信息来填写这个选项。

当最终使用默认参数调用您的 API 时,他们将面临问题。

// Your API
case class Gather(url: String)  def this() =  ...  ... 

// Their code
val gather = url match 
  case Some(u) => Gather(u)
  case _ => Gather()

我认为这样做会容易得多

val gather = Gather(url.openOrVoid)

*openOrVoidNone 的情况下将被忽略。但这是不可能的。

因此,您确实应该考虑谁将使用您的 API,以及他们可能如何使用它。很可能您的用户已经使用Option 来存储所有变量,因为他们知道它们最终是可选的……

默认参数很好,但它们也使事情复杂化;特别是当周围已经有 Option 类型时。我认为你的第二个问题有些道理。

【讨论】:

非常好的观点,尤其是关于 API 用户在调用 API 之前如何声明其参数的假设。 默认参数是 API 的一部分吗?如果不是,当您真正的意思是“我不在乎”时,传递 None 将是不好的做法。如果默认更改为 Some(thingElse) 怎么办?【参考方案5】:

我可以只支持你现有的方法吗,Some("callbackUrl")? API 用户只需输入另外 6 个字符,向他们表明该参数是可选的,并且可能会使您的实现更容易。

【讨论】:

所有参数都是可选的,但如果未指定,此特定参数将设置为 None。 :) DaGGeRRz,我不明白为什么这是个问题。作为 API 用户,您提供的第一个示例完全可以理解。我用 Some(url) 调用它没有问题。看起来你在试图避免一个不是问题的问题。【参考方案6】:

我认为你应该硬着头皮继续使用Option。我以前遇到过这个问题,通常在一些重构后就消失了。有时它没有,我忍受它。但事实是默认参数不是“可选”参数——它只是一个具有默认值的参数。

我非常赞成Debilski'sanswer。

【讨论】:

【参考方案7】:

我也对此感到惊讶。为什么不概括为:

implicit def any2Option[T](x: T): Option[T] = Some(x)

有什么理由不能只是 Predef 的一部分?

【讨论】:

是的 - 这是一个糟糕的主意,正如已经多次指出的那样。 有很多原因,尤其是你最终会得到Some(null) 乱扔你的程序,而拥有Option 的全部意义在于类型安全(即它不能与其内容) 正如我在上面的评论中提到的,我想避免隐式转换。而且我还对具有命名默认参数的 API 设计的“更广泛的图景”感兴趣。 :)

以上是关于Scala API中的选项和命名默认参数是不是像油和水?的主要内容,如果未能解决你的问题,请参考以下文章

Scala 必知必会

Scala可变参数列表,命名参数和参数缺省

Scala - 柯里化和默认参数

在 pyspark 的 Scala UDF 中使用默认参数值?

大数据进阶之路——Scala 函数和对象

Scala 字符串模板