在 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 提供的语法糖,其中 object
和 apply
方法变得像函数一样可调用,给人一种错觉,即 AddOne
和 AddTwo
是函数并像选项 A 中的函数一样传递.
由于 Scala 具有明确的函数类型,我会说如果您需要将函数传递给您的函数,请使用函数类型,即您的选项 B。它们明确用于此目的。
顺便说一句,您所说的 "have never seen that before in scala" 是什么意思并不完全清楚。许多标准库方法都接受函数作为它们的参数,例如,集合转换方法。创建类型别名来传达更复杂类型的语义也是 Scala 的惯用做法,别名类型是否为函数并不重要。例如,我当前项目中的核心类型之一实际上是函数的类型别名,它使用描述性名称传达语义,还允许轻松传递符合要求的函数,而无需显式创建某些特征的子类。
我认为,理解 apply
方法的语法糖与函数或函数特征的使用方式没有任何关系也很重要。事实上,apply
方法确实具有仅用括号调用的能力;然而,这并不意味着具有 apply
方法的不同类型,即使具有相同的签名,也是 可互操作的 ,我认为这种互操作性在这种情况下很重要,以及轻松构建此类实例的能力。毕竟,在您的具体示例中,您是否可以在 IntMath
上使用语法糖只对 您的 代码很重要,但对于您的代码用户来说,能够轻松构建IntMath
的一个实例,以及将他们已经拥有的一些现有事物作为 IntMath
传递的能力更为重要。
对于 FunctionN
类型,您可以使用匿名函数语法构造这些类型的实例(实际上,有几种语法,至少是这些:x => y
、{ x => y }
、_.x
、method _
、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
实例,这更麻烦且不那么惯用。
我是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 提供的语法糖,其中 object
和 apply
方法变得像函数一样可调用,给人一种错觉,即 AddOne
和 AddTwo
是函数并像选项 A 中的函数一样传递.
由于 Scala 具有明确的函数类型,我会说如果您需要将函数传递给您的函数,请使用函数类型,即您的选项 B。它们明确用于此目的。
顺便说一句,您所说的 "have never seen that before in scala" 是什么意思并不完全清楚。许多标准库方法都接受函数作为它们的参数,例如,集合转换方法。创建类型别名来传达更复杂类型的语义也是 Scala 的惯用做法,别名类型是否为函数并不重要。例如,我当前项目中的核心类型之一实际上是函数的类型别名,它使用描述性名称传达语义,还允许轻松传递符合要求的函数,而无需显式创建某些特征的子类。
我认为,理解 apply
方法的语法糖与函数或函数特征的使用方式没有任何关系也很重要。事实上,apply
方法确实具有仅用括号调用的能力;然而,这并不意味着具有 apply
方法的不同类型,即使具有相同的签名,也是 可互操作的 ,我认为这种互操作性在这种情况下很重要,以及轻松构建此类实例的能力。毕竟,在您的具体示例中,您是否可以在 IntMath
上使用语法糖只对 您的 代码很重要,但对于您的代码用户来说,能够轻松构建IntMath
的一个实例,以及将他们已经拥有的一些现有事物作为 IntMath
传递的能力更为重要。
对于 FunctionN
类型,您可以使用匿名函数语法构造这些类型的实例(实际上,有几种语法,至少是这些:x => y
、{ x => y }
、_.x
、method _
、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
实例,这更麻烦且不那么惯用。