为可选参数建模的最佳方式



正如标题所说,在Scala中建模可选参数的最佳方法是什么?

对于可选参数,我指的是执行函数主体时不需要的值。

要么是因为该参数存在默认值,要么根本不需要参数本身(例如配置或调试标志);请注意,在Java上,我可能会将null传递给这些参数。


这是Scala社区的常见问题解答,特别是由新手制作的。

例如:

  • SO中:从A到Some(A)的隐式转换
  • 关于用户的话语:https://users.scala-lang.org/t/passing-true-optional-arguments-to-functions/6087
  • GitterScala通道中:https://gitter.im/scala/scala?at=5d28f90d01621760bca2eae3

社区接受的简单答案

总的来说,社区一致认为,以下列出的所有提案或备选方案都不值得进行权衡
因此,建议的解决方案是只使用Option数据类型,并手动/显式地将值包装在Some

def test(required: Int, optional: Option[String] = None): String =
optional.map(_ * required).getOrElse("")
test(required = 100) // ""
test(required = 3, optional = Some("Foo")) // "FooFooFoo"

然而,这种方法的明显缺点是呼叫站点上的必要样板。但是,可以说,它使代码更易于阅读和理解,从而更易于维护。

尽管如此,有时您可以使用其他技术提供更好的API,如默认参数或重载(下面讨论)

备选方案和建议

隐式转换

由于前面解决方案的样板,一次又一次地提到了使用隐式转换的常见替代方案;例如:

implicit def a2opt[A](a: A): Option[A] = Some(a)

因此,前面的函数可以这样调用:

test(required = 3, optional = "Foo")

这样做的缺点是隐式转换隐藏了optional是可选参数的事实(当然,如果它的名称不同的话),并且这种转换可以应用于代码的许多其他(非故意的)部分;这就是通常不鼓励隐式转换的原因。

另一种选择是使用扩展方法,而不是类似optional = "foo".opt隐式转换。然而,扩展方法需要添加更多的代码,并且该站点调用仍然有一些样板文件,这使得这个方法看起来像是一个平庸的中点
(免责声明,如果您正在使用cats,则在作用域.some中已经有这样的扩展方法了,因此您可能需要使用它)

默认参数

该语言支持为函数的参数提供默认值,这样,如果没有传递,编译器将插入默认值。

有人可能认为,这应该是对可选论点进行建模的最佳方式;然而,他们有三个问题。

  1. 您并不总是有一个默认值,有时您只想知道该值是否被传递。例如,一个标志。

  2. 如果它在自己的参数组上,您仍然需要添加空括号,这可能看起来很难看(当然,这是一种主观意见)

def transact[A](config: Config = Config.default)(f: Transaction => A): A
transact()(tx => ???)
  1. 您只能有一个带有默认参数的重载
object Functions {
def run[A](query: Query[A], config: Config = Config.default): A = ???
def run[A](query: String, config: Config = Config.default): A = ???
}

错误:在对象函数中,方法run的多个重载替代项定义了默认参数。

过载

另一个常见的解决方法是提供该方法的重载版本;例如:

def test(required: Int, optional: String): String =
optional * required
def test(required: Int): String =
test(required, optional = "")

这种方法的优点是它将样板文件封装在定义站点上,而不是调用站点上;还使代码更易于阅读,并得到工具的良好支持
然而,最大的缺点是,如果您有多个可选参数,则这不会很好地扩展;例如,对于三个参数,您需要七个(7)重载。

但是,如果您有许多可选参数,那么最好只要求一个Config/Context参数,并使用Builder

生成器模式

def foo(data: Dar, config: Config = Config.default)
// It probably would be better not to use a case class for binary compatibility.
// And rather define your own private copy method or something.
// But that is outside of the scope of this question / answer.
final case class Config(
flag1: Option[Int] = None,
flag2: Option[Int] = None,
flag3: Option[Int] = None
) {
def withFlag1(flag: Int): Config =
this.copy(flag1 = Some(flag))
def withFlag2(flag: Int): Config =
this.copy(flag2 = Some(flag))
def withFlag3(flag: Int): Config =
this.copy(flag3 = Some(flag))
}
object Config {
def default: Config = new Config()
}

请求本机支持

在贡献者的讨论中,有人建议为这个用例添加语言级别或stdlib级别的支持。然而,由于上述相同的原因,所有这些都被丢弃了。

此类提议的例子:

  • https://contributors.scala-lang.org/t/sip-suggestion-add-and-syntactic-sugar-for-more-convenient-option-t-usage/2413
  • https://contributors.scala-lang.org/t/can-we-wean-scala-off-implicit-conversions/4388/57

共谋

一如既往,根据您的具体案例和您想要提供的API来选择要使用的技术。


Scala 3

也许引入联合类型可以为编码可选参数提供一种更简单的方法?

在我看来,只有一条经验法则:避免显式传递None

剂量:

  • 默认参数值
  • 默认值为NoneOption
  • 过载

注意事项:

  • 不带默认值NoneOption
  • Option,默认值为Some

最新更新