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

人气:697 发布:2022-10-16 标签: design-patterns scala optional

问题描述

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

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

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

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

例如:

此处so:Implicit conversion from A to 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中已有这样的扩展方法,因此您可能希望使用该扩展方法)。

默认参数

该语言支持为函数的参数提供默认值,以便如果未传递,编译器将插入默认值。

有人可能认为这应该是对可选参数建模的最佳方式;然而,它们有三个问题。

您并不总是有一个缺省值,有时您只想知道是否传递了该值。例如,标志。

如果是在自己的参数组上,还需要加上空括号(当然这是主观意见)。

def transact[A](config: Config = Config.default)(f: Transaction => A): A

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

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

重载

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

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

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

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

但是,如果您有许多可选参数,最好只请求一个Config/Context参数并使用生成器。

生成器模式。

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()
}

本机支持请求

关于贡献者的论述,有人建议为该用例添加语言级别或标准库级别的支持。然而,由于上述相同的原因,它们都已被丢弃。

此类建议的示例:

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

引入联合类型可能会带来对可选参数进行更简单编码的可能性?

979