在 Scala 中将多个函数定义为同一类型的惯用方法是什么?

What's the idiomatic way to define multiple functions as the same type in Scala?

我是ruby、python和javascript(特别是后端node.js)方面经验丰富的程序员,我曾在java工作过、perl 和 c++,我在学术上使用过 lisp 和 haskell,但我是 Scala 的新手,正在尝试学习一些约定。

我有一个接受函数作为参数的函数,类似于排序函数接受比较器函数的方式。更惯用的实现方式是什么?

假设这是接受函数参数y的函数:

object SomeMath {
  def apply(x: Int, y: IntMath): Int = y(x)
}

是否应该将 IntMath 定义为 trait 以及 IntMath 的不同实现定义在不同的对象中? (我们称此选项为 A)

trait IntMath {
   def apply(x: Int): Int
}

object AddOne extends IntMath {
   def apply(x: Int): Int = x + 1
}

object AddTwo extends IntMath {
  def apply(x: Int): Int = x + 2
}

AddOne(1)
// => 2
AddTwo(1)
// => 3
SomeMath(1, AddOne)
// => 2
SomeMath(1, AddTwo)
// => 3

或者 IntMath 应该是函数签名的类型别名吗? (选项 B)

type IntMath = Int => Int

object Add {
  def one: IntMath = _ + 1
  def two: IntMath = _ + 2
}

Add.one(1)
// => 2
Add.two(1)
// => 3
SomeMath(1, Add.one)
// => 2
SomeMath(1, Add.two)
// => 3

但哪个更符合 scala 的习惯?

或者两者都不惯用? (选项 C)

我以前在函数式语言方面的经验使我倾向于 B,但我以前从未在 scala 中看到过。另一方面,尽管 trait 似乎增加了不必要的混乱,但我已经看到该实现并且它在 Scala 中似乎工作得更加顺利(因为该对象可以使用 apply 函数调用)。

[更新] 修复了 IntMath 类型被传递到 SomeMath 的示例代码。 Scala 提供的语法糖,其中 objectapply 方法变得像函数一样可调用,给人一种错觉,即 AddOneAddTwo 是函数并像选项 A 中的函数一样传递.

由于 Scala 具有明确的函数类型,我会说如果您需要将函数传递给您的函数,请使用函数类型,即您的选项 B。它们明确用于此目的。

顺便说一句,您所说的 "have never seen that before in scala" 是什么意思并不完全清楚。许多标准库方法都接受函数作为它们的参数,例如,集合转换方法。创建类型别名来传达更复杂类型的语义也是 Scala 的惯用做法,别名类型是否为函数并不重要。例如,我当前项目中的核心类型之一实际上是函数的类型别名,它使用描述性名称传达语义,还允许轻松传递符合要求的函数,而无需显式创建某些特征的子类。

我认为,理解 apply 方法的语法糖与函数或函数特征的使用方式没有任何关系也很重要。事实上,apply 方法确实具有仅用括号调用的能力;然而,这并不意味着具有 apply 方法的不同类型,即使具有相同的签名,也是 可互操作的 ,我认为这种互操作性在这种情况下很重要,以及轻松构建此类实例的能力。毕竟,在您的具体示例中,您是否可以在 IntMath 上使用语法糖只对 您的 代码很重要,但对于您的代码用户来说,能够轻松构建IntMath 的一个实例,以及将他们已经拥有的一些现有事物作为 IntMath 传递的能力更为重要。

对于 FunctionN 类型,您可以使用匿名函数语法构造这些类型的实例(实际上,有几种语法,至少是这些:x => y{ x => y }_.xmethod _method(_))。在 Scala 2.11 之前甚至没有办法创建 "Single Abstract Method" 类型的实例,即使在那里它也需要一个编译器标志才能真正启用此功能。这意味着您的类型的用户必须这样写:

SomeMath(10, _ + 1)

或者这个:

SomeMath(10, new IntMath {
  def apply(x: Int): Int = x + 1
})

当然,前一种方法更清晰。

此外,FunctionN 类型提供了功能类型的单一公分母,这提高了互操作性。例如,数学中的函数除了它们的签名外没有任何类型的 "type",只要它们的签名匹配,您就可以使用任何函数代替任何其他函数。这 属性 在编程中也很有用,因为它允许为不同的目的重用您已有的定义,减少转换次数,从而减少您在编写这些转换时可能犯的潜在错误。

最后,在某些情况下,您确实希望为类函数接口创建一个单独的类型。一个例子是,对于成熟的类型,您可以定义一个伴生对象,并且伴生对象参与隐式解析。这意味着如果你的函数类型的实例应该被隐式提供,那么你可以利用有一个伴随对象并在那里定义一些常见的隐式,这将使它们可供该类型的所有用户使用而无需额外导入:

// You might even want to extend the function type
// to improve interoperability
trait Converter[S, T] extends (S => T) {
  def apply(source: S): T
}

object Converter {
  implicit val intToStringConverter: Converter[Int, String] = new Converter[Int, String] {
    def apply(source: Int): String = source.toString
  }
}

这里有一段与类型关联的隐式范围是有用的,因为否则 Converter 的用户将需要始终导入某些 object/package 的内容以获得默认隐式定义;但是,使用这种方法,默认情况下将搜索 object Converter 中定义的所有隐式。

不过,这些情况并不常见。作为一般规则,我认为,您应该首先尝试使用常规函数类型。如果您发现 确实 需要一个单独的类型,出于实际原因,那么您应该创建自己的类型。

另一种选择是不定义任何东西并保持函数类型明确:

def apply(x: Int, y: Int => Int): Int = y(x)

通过明确哪些参数是数据对象,哪些是函数对象,使代码更具可读性。 (纯粹主义者会说函数式语言没有区别,但我认为这在大多数现实世界的代码中都是有用的区别,特别是如果你来自更声明性的背景)

如果参数或结果变得更复杂,则可以将它们定义为自己的类型,但函数本身仍然是显式的:

def apply(x: MyValue, y: MyValue => MyValue2): MyValue2 = y(x)

否则,B 更可取,因为它允许您将函数作为 lambda 传递:

SomeMath(1, _ + 3)

A 会要求您声明一个新的 IntMath 实例,这更麻烦且不那么惯用。