【问题标题】:Scala: How to define "generic" function parameters?Scala:如何定义“通用”函数参数?
【发布时间】:2010-11-18 04:24:34
【问题描述】:

我现在正在尝试学习 Scala,在 Haskell 方面有一点经验。对我来说很奇怪的一件事是 Scala 中的所有函数参数 必须 用类型注释 - Haskell 不需要。为什么是这样?举个更具体的例子:添加函数是这样写的:

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

但是,这只适用于双精度数(嗯,整数也适用,因为隐式类型转换)。但是,如果您想定义自己的类型并定义自己的 + 运算符,该怎么办。您将如何编写适用于任何定义 + 运算符的类型的 add 函数?

【问题讨论】:

  • 很高兴我在发布自己的几乎抄本之前看到了这一点。感谢您的提问。

标签: scala haskell polymorphism type-inference


【解决方案1】:

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

【讨论】:

  • 这令人印象深刻。隐式 Numeric 是否将 A 限制为 Numeric 的子类型?还是只需要提供“加号”方法的目标类型?
  • 谢谢!本质上,这就像 Haskell 中的类型类。我还在 scala-lang.org/node/114 找到了对 implicit 的一个很好的解释。
  • @mtnygard 不,“A”没有任何子类型约束。魔法发生在导入的 scala.Numeric 对象中。有许多隐式对象都实现了 Numeric 特征,并且 scalac 在编译时会选择合适的对象。神奇的东西,真的。详情请查看 api 文档。
  • 随后我在传递添加到函数时遇到了问题。 (例如,我定义了一个函数两次[T](f: (T) => T, T n) = f(f(n)),并尝试像这样调用它:两次(添加,2),但它失败:找不到参数数字的隐式值:Numeric[T]。有人吗?
  • 等效地,您可以编写具有上下文边界的 same 代码: def add[A: Numeric](x: A, y: A) = implicitly[Numeric[A] ].plus(x, y) 在许多情况下,您甚至不需要使用隐式来恢复隐式传递的参数,因为它将被隐式传递给期望它的其他函数。例如:scala> def addTwice[A: Numeric](x: A, y: A) = add(add(x, y), y) addTwice: [A](x: A,y: A)(隐含证据$1 : Numeric[A])A scala> addTwice(1, 2) res2: Int = 5
【解决方案2】:

为了巩固自己使用implicit的概念,我写了一个例子,不需要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 好,它基本上会为您完成所有这些工作。但是,这就是权衡的所在。

【讨论】:

    【解决方案3】:

    Haskell 使用Hindley-Milner 类型推断。这种类型推断功能强大,但限制了语言的类型系统。例如,假设子类化不适用于 H-M。

    无论如何,Scala 类型系统对于 H-M 来说太强大了,所以必须使用一种更有限的类型推断。

    【讨论】:

      【解决方案4】:

      我认为 Scala 要求对新定义函数的参数进行类型注释的原因在于 Scala 使用比 Haskell 中使用的更局部的类型推断分析。

      如果您的所有类都混合在一个特征中,例如 Addable[T],声明了 + 运算符,您可以将通用添加函数编写为:

      def add[T <: Addable[T]](x : T, y : T) = x + y
      

      这将 add 函数限制为实现 Addable 特征的类型 T。

      不幸的是,当前的 Scala 库中没有这样的特性。但是您可以通过查看类似的情况来了解它是如何完成的,Ordered[T] 特征。该特征声明了比较运算符,并被RichIntRichFloat 等类混合在一起。然后,您可以编写一个排序函数,例如,可以采用 List[T] where [T &lt;: Ordered[T]] 对混合在有序特征中的元素列表进行排序。由于像FloatRichFloat 这样的隐式类型转换,您甚至可以对IntFloatDouble 的列表使用排序函数。

      正如我所说,不幸的是,+ 运算符没有相应的特征。因此,您必须自己写出所有内容。你会做 Addable[T] trait,创建AddableIntAddableFloat 等,扩展 Int、Float 等的类并混入 Addable trait,最后添加隐式转换函数,例如,并将 Int 转换为 AddableInt,以便编译器可以实例化并使用您的 add 函数。

      【讨论】:

      • 这当然是可行的,但不是那么好。
      【解决方案5】:

      函数本身非常简单:

      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 extends GroupAdditive):

      class Real private (s: LargeInteger, err: LargeInteger, exp: Int) extends Number[Real] with Field[Real] with Ordered[Real] {
        ...
      
        def +(that: Real): Real = { ... }
      
        ...
      }
      

      这可能比您现在真正想知道的要多,但它确实展示了如何定义泛型参数以及如何实现它们。

      最终,特定类型不是必需的,但编译器至少需要知道类型界限。

      【讨论】:

      • 这个方法似乎要求 add 是一个类中的方法。
      • 嗯,是的。不是所有方法都必须在一个类中吗?即使在原始示例中,在 REPL 中定义“add”也会在隐式定义的类中创建它。
      • 您的解决方案需要将数字包装在 Real 类中;在 Scala 中这是一个坏主意,因为您可以使用隐式对 Haskell 类型类进行编码,并使用更好的语法。
      猜你喜欢
      • 1970-01-01
      • 2011-11-05
      • 1970-01-01
      • 2019-01-02
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-01-04
      • 2016-02-19
      相关资源
      最近更新 更多