如何在Scala中使用命名参数创建自定义函数类型?

6

假设我想创建一个名为ImportFunc的自定义函数类型,它需要一个名为fileImportID的Int参数和一个名为filename的字符串参数。使用类型别名可以轻松实现:

type ImportFunc = (Int, String) => Unit

问题是,任何试图使用这个函数的人都不知道Int和String实际上应该是什么。有没有办法我可以写出像这样的东西:

type ImportFunc = (fileImportID: Int, filename: String) => Unit
4个回答

5
当你调用一个函数时,实际上是在调用该函数的apply方法。换句话说,假设有以下代码:
def doImport(fileImportID: Int, filename: String) {
  println(s"Importing file #$fileImportID ($filename)")
}

以下代码片段:
val f = doImport _
f(123, "file.txt")

...只是语法糖,实际上等同于:

val f = doImport _
f.apply(123, "file.txt")

如果在使用命名参数进行调用时,编译器会查找参数名称的位置,那肯定就是在apply方法的定义中。 事实证明,对于Function2,这些参数的名称分别为v1v2。所以我们可以这样做:
scala> f.apply(v1=123, v2="file.txt")
Importing file #123 (file.txt)

现在让我们看看当使用语法糖(也就是删除对apply的显式调用)时,它是否仍然有效:

scala> f(v1=123, v2="file.txt")
Importing file #123 (file.txt)

很好,它工作了。 现在当然,v1v2不完全等同于fileImportIDfilename,但我们可以通过一些类型细化来修复它:

type ImportFunc = ((Int, String)=>Unit) { 
  def apply(fileImportID: Int, filename: String): Unit 
}

基本上这只是一个 (Int, String)=>Unit(或者换句话说是 Function2[Int, String, Unit]),但带有重新定义 apply 和我们所需的参数名称。让我们看到它的作用:

scala> val f: ImportFunc = doImport _
f: ImportFunc = <function2>
scala> f(fileImportID=123, filename="file.txt")
Importing file #123 (file.txt)

成功!

一个重要的侧面说明:就打字而言,ImportFuncFunction2[Int, String, Unit] 或任何其他类似的细化是相同的。 这是因为参数名称不是签名的一部分。所以在我的例子中,f 仍然可以传递到期望 Function2[Int, String, Unit] 的任何地方 (但从那时起,您将无法再使用自定义参数名称调用它)。


3
在Scala中,函数是从特质FunctionX中定义的,因此您可以按照以下方式进行操作:
trait ImportFunc extends ((Int, String) => Unit) {
  def apply(fileImportId: Int, filename: String): Unit
}

// Then custom definition can be implemented as following
val f1: ImportFunc = new ImportFunc {
  def apply(fid: Int, fn: String): Unit = ???
}
f1(1, "name") // call it

/** Companion object to ease the use */
object ImportFunc {
  /** Function factory: take a plain (Int, String) => Unit 
    and turn it into documented type */
  def apply(f: (Int, String) => Unit): ImportFunc = new ImportFunc {
    def apply(fileImportId: Int, filename: String): Unit = f(fileImportId, filename)
  }
}

val f2: ImportFunc = ImportFunc((fid: Int, fn: String) => ???)
f2(2, "eman") // call it

2

一种简单的“类型”解决方案:

type FileImportID = Int
type Filename = String
type ImportFunc = (FileImportID, Filename) => Unit

不喜欢使用 case class 来包装值以进行命名。 - cchantep
我把这个移到了另一个答案。 - skytteren
我不确定你为什么把它移到另一个答案,除了可能试图获得更多的声望之外。既然它们如此相似,为什么不在同一个选项中保留两个选项呢? - damian
@kingdamian42:我认为它们是不同的答案。我个人更喜欢另一个,但如果你不像我一样喜欢打字,这个也很有道理。 - skytteren

-1

我不太喜欢Int和String,因为它们很容易与其他字符串和整数混淆。请使用:

case class FileImportID(value: Int) extends AnyVal
case class Filename(value: String) extends AnyVal

//Leading to 
type ImportFunc = (FileImportID, Filename) => Unit

我几乎在所有情况下都使用类型。在任何函数调用中,如果有两个以上的参数相同,我倾向于混合使用字符串、整数和标准的AnyVals。因此,在代码中不会出现未定义的字符串和整数。 - skytteren
1
你可能已经猜到了,有人已经为这种模式发明了一个名字:http://darrenhobbs.com/2007/04/11/tiny-types/,http://www.markhneedham.com/blog/2009/03/10/oo-micro-types/,http://andypalmer.com/2009/07/tiny-types/,http://philcalcado.com/2009/08/21/ubiquitous-language-tiny-types-and-responsibility/,http://grahamnash.blogspot.de/2011/08/tiny-type-language-support.html。 - Jörg W Mittag

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接