Scala:如何定义“通用”的函数参数?

53

我现在正在学习Scala,有一点Haskell的经验。有一件让我感到奇怪的事情是:在Scala中,所有函数参数必须带类型注解,而这在Haskell中并不需要。为什么会这样呢?试着给出一个更具体的例子:一个加法函数的写法如下:

def add(x:Double, y:Double) = x + y

但是,这仅适用于双精度浮点数(好吧,整数也可以,因为存在隐式类型转换)。但是如果您想定义自己的类型,该类型定义了自己的 + 运算符,那该怎么办呢?如何编写一个函数,使其适用于定义了 + 运算符的任何类型?


3
感谢您的询问,很高兴在发布我的帖子之前看到了这篇与我自己几乎一模一样的内容。 - wheaties
5个回答

70

Haskell使用Hindley-Milner类型推导算法,而Scala为了支持面向对象的一面,不得不暂时放弃使用它。

为了轻松编写适用于所有适用类型的添加函数,您需要使用Scala 2.8.0:

Welcome to Scala version 2.8.0.r18189-b20090702020221 (Java HotSpot(TM) 64-Bit Server VM, Java 1.6.0_15).
Type in expressions to have them evaluated.
Type :help for more information.

scala> import Numeric._
import Numeric._

scala> def add[A](x: A, y: A)(implicit numeric: Numeric[A]): A = 
     | numeric.plus(x, y)
add: [A](x: A,y: A)(implicit numeric: Numeric[A])A

scala> add(1, 2)
res0: Int = 3

scala> add(1.1, 2.2)
res1: Double = 3.3000000000000003

很不错。隐式数字约束是否将A限定为Numeric的子类型?还是目标类型只需要提供一个“plus”方法? - user73774
2
谢谢!这就像 Haskell 中的类型类一样。我还在 http://www.scala-lang.org/node/114 找到了一个很好的 implicit 解释。 - airportyh
6
不,"A"没有任何子类型约束。奇妙的事情发生在导入的scala.Numeric对象中。有许多隐式对象都实现了Numeric trait,当scalac编译时会选择适当的对象。真是神奇的东西。请查看API文档以获取详细信息。 - Walter Chang
我在后续传递add函数时遇到了问题。(例如,我定义了一个函数两次[T](f: (T) => T, T n) = f(f(n)),并尝试像这样调用它:twice(add, 2),但失败了:找不到参数numeric的隐式值:Numeric[T]。有人知道怎么解决吗? - Wilfred Springer
2
同样地,您可以使用上下文界定编写相同的代码: def add[A: Numeric](x: A, y: A) = implicitly[Numeric[A]].plus(x, y) 在许多情况下,您甚至不需要使用implicitly来恢复隐式传递的参数,因为它将被隐式传递给其他期望它的函数。例如:scala> def addTwice[A: Numeric](x: A, y: A) = add(add(x, y), y) addTwice: [A](x: A,y: A)(implicit evidence$1: Numeric[A])Ascala> addTwice(1, 2) res2: Int = 5 - Blaisorblade

19
为了巩固我对“隐式”的概念,我写了一个例子,不需要scala 2.8,但是使用了相同的概念。我认为这可能会对某些人有所帮助。 首先,您定义一个通用抽象类 Addable :
scala> abstract class Addable[T]{
 |   def +(x: T, y: T): T
 | }
defined class Addable

现在您可以像这样编写add函数:
scala> def add[T](x: T, y: T)(implicit addy: Addable[T]): T = 
 | addy.+(x, y)
add: [T](T,T)(implicit Addable[T])T

这类似于Haskell中的类型类。然后,要将此通用类实现为特定类型,您需要编写以下内容(这里是Int、Double和String的示例):

scala> implicit object IntAddable extends Addable[Int]{
 |   def +(x: Int, y: Int): Int = x + y
 | }
defined module IntAddable

scala> implicit object DoubleAddable extends Addable[Double]{
 |   def +(x: Double, y: Double): Double = x + y
 | }
defined module DoubleAddable

scala> implicit object StringAddable extends Addable[String]{
 |   def +(x: String, y: String): String = x concat y
 | }
defined module StringAddable

此时,您可以使用所有三种类型调用add函数:

scala> add(1,2)
res0: Int = 3

scala> add(1.0, 2.0)
res1: Double = 3.0

scala> add("abc", "def")
res2: java.lang.String = abcdef

当然不如 Haskell 那么好,因为 Haskell 基本上可以为你完成所有这些。但是,这就是权衡的地方。

3
我认为Scala要求在新定义函数的参数上进行类型注释的原因是,Scala使用比Haskell更本地化的类型推断分析。
如果您的所有类都混合了一个名为Addable[T]的特质,该特质声明了+运算符,则可以将通用添加函数编写为:
def add[T <: Addable[T]](x : T, y : T) = x + y

这限制了add函数只能应用于实现了Addable trait的类型T。

不幸的是,当前Scala库中没有这样的trait。但是你可以看一下类似情况下的处理方式,比如Ordered[T] trait。这个trait声明了比较运算符,并被RichInt、RichFloat等类进行混合。然后你可以编写一个sort函数,它可以接受一个List[T],其中[T <: Ordered[T]]来对混合了ordered trait的元素列表进行排序。由于隐式类型转换,比如Float到RichFloat,你甚至可以在Int、Float或Double列表上使用你的sort函数。

正如我所说,不幸的是,没有对应于+运算符的trait。因此,你需要自己编写所有内容。你需要编写Addable[T] trait,创建AddableInt、AddableFloat等类,它们扩展Int、Float等并混合Addable trait,最后添加隐式转换函数,将Int转换为AddableInt等,以便编译器可以实例化并使用你的add函数。


这肯定是可行的,但不够好。 - airportyh

3
Haskell使用Hindley-Milner类型推断。这种类型推断非常强大,但限制了语言的类型系统。例如,子类化在H-M中无法很好地工作。
无论如何,Scala的类型系统对于H-M来说过于强大,因此必须使用一种更有限的类型推断。

1

这个函数本身将会非常简单:

def add(x: T, y: T): T = ...

更好的方法是,你可以重载+方法:
def +(x: T, y: T): T = ...

然而,还有一个缺失的部分,那就是类型参数本身。按照现在的写法,该方法缺少其类。最有可能的情况是您正在调用T实例上的+方法,并将另一个T实例传递给它。我最近也这样做了,定义了一个特质,其中说:“加性群由加操作和反转元素的手段组成”

trait GroupAdditive[G] extends Structure[G] {
  def +(that: G): G
  def unary_- : G
}

然后,稍后我定义了一个Real类,它知道如何添加自身的实例(Field扩展GroupAdditive):
class Real private (s: LargeInteger, err: LargeInteger, exp: Int) extends Number[Real] with Field[Real] with Ordered[Real] {
  ...

  def +(that: Real): Real = { ... }

  ...
}

这可能比你现在想知道的更多,但它确实展示了如何定义通用参数以及如何实现它们。

最终,具体类型并不是必需的,但编译器至少需要知道类型边界。


这种方法似乎需要add是一个类中的方法。 - airportyh
是的,所有的方法都必须在一个类中吗?即使在原始示例中,在REPL中定义“add”也会将其创建在一个隐式定义的类内部。 - user73774
你的解决方案需要将数字包装在Real类中;在Scala中,这是一个不好的想法,因为你可以使用隐式编码Haskell类型类,并具有更好的语法。 - Blaisorblade

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