是Scala API中的选项和命名的默认参数,如石油和水吗?

时间:2010-11-16 21:52:11

标签: scala optional-parameters named-parameters

我正在开发一个Scala API(顺便说一下,对于Twilio),其中操作具有相当多的参数,其中许多都具有合理的默认值。为了减少键入并增加可用性,我决定使用带有命名和默认参数的case类。例如,对于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类,并将其转换为实现端的选项:

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

我的问题如下:

  1. 有没有更优雅的方法来解决我的具体案例?
  2. 更一般地说:命名参数是一种新的语言功能(2.8)。难道会发现选项和命名的默认参数就像石油和水一样吗? :)
  3. 在这种情况下,使用null默认值可能是最佳选择吗?

7 个答案:

答案 0 :(得分:24)

这是另一种解决方案,部分受Chris' answer的启发。它还涉及一个包装器,但包装器是透明的,您只需要定义一次,并且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转换,那么每当需要T时,调用者必须传递Opt[T]

答案 1 :(得分:9)

不要将任何内容自动转换为选项。使用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

然后您可以按照自己的意愿调用它 - 显然可以根据需要将行为方法添加到FallbackUrlNumDigits。这里的主要负面因素是它是一大堆样板

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

答案 2 :(得分:5)

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

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

但是,此方法仅适用于AnyRef类型。如果你想对任何类型的参数使用相同的技术(不会导致Integer.MAX_VALUE替换为null),那么我猜你应该选择其中一个答案

答案 3 :(得分:4)

我认为,只要Scala中没有语言支持真正的虚空(下面的解释)'type',使用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)

*openOrVoid只会遗漏None。但这是不可能的。

所以你真的应该考虑谁将使用你的API以及他们如何使用它。很可能您的用户已经使用Option来存储所有变量,原因是他们知道这些变量最终是可选的...

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

答案 4 :(得分:1)

我可能只是赞成你现有的方法,Some("callbackUrl")? API用户可以输入6个以上的字符,向他们显示该参数是可选的,并且可能使您的实现更容易。

答案 5 :(得分:1)

我认为你应该咬紧牙关继续Option。我以前遇到过这个问题,经过一些重构后它通常会消失。有时它没有,我和它一起生活。但事实是默认参数不是“可选”参数 - 它只是一个具有默认值的参数。

我非常赞成Debilski's answer

答案 6 :(得分:-1)

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

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

为什么这不能成为Predef的一部分?