【问题标题】:Best way to model optional arguments建模可选参数的最佳方法
【发布时间】:2020-12-14 01:15:28
【问题描述】:

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

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

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


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

例如:

【问题讨论】:

  • 请提出一个实际的具体问题,即使在规范帖子中也是如此。链接最好放在 cmets 中。
  • "要么是因为该参数存在默认值" 好吧,这种情况在语言中是支持的;不涉及样板。
  • @AlexeyRomanov 是的,但他们有一些警告。请检查更新后的答案,如果您有更多反馈,请告诉我:) - 为了记录,这个答案已添加到 Scala FAQ 所以我希望它尽可能完整和客观。

标签: scala design-patterns optional


【解决方案1】:

简单且社区接受的答案

一般来说,社区的共识是,下面列出的所有提案或替代方案都不值得权衡取舍。
因此,推荐的解决方案是仅使用 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 = "")

这个的优点是它封装了样板的on-definition site而不是on-call site;还使代码更易于阅读,并且得到工具的良好支持。
然而,最大的缺点是如果您有多个可选参数,则无法很好地扩展。例如,对于三个参数,您需要七个 (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()
}

请求原生支持

在贡献者的话语中,已提议为此用例添加语言级别或标准库级别的支持。但是,由于上述相同的原因,它们都被丢弃了。

此类提案的示例:

结论

与往常一样,根据您的具体情况和要提供的 API 选择要使用的技术。


斯卡拉 3

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

【讨论】:

    【解决方案2】:

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

    做事:

    • 默认参数值
    • Option 与默认值 None
    • 重载

    注意事项:

    • Option 没有默认值 None
    • OptionSome 作为默认值

    【讨论】:

    • 最好解释一下这些规则背后的基本原理 :)
    • 我已经添加了主要理由,我认为已经足够了。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-06-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多