【问题标题】:In Scala, why can't I implement a trivial generic function like this?在 Scala 中,为什么我不能实现这样一个微不足道的泛型函数?
【发布时间】:2015-07-12 15:36:53
【问题描述】:

我想要一个名为“double”的通用函数,它的行为是这样的,可以通过def +(x:T):T 方法应用于任何类型:

double("A")
> "AA"
double(1)
> 2
double(0.2)
> 0.4

所以我这样写这个函数:

def double[T](x:T):T = { x+x }

但是当我在 REPL 中运行它时,scala 对此感到不满:

scala> def double[T](x:T):T = { x+x }
<console>:7: error: type mismatch;
 found   : T
 required: String
       def double[T](x:T):T = { x+x }
                                  ^

我认为structural type 可能是一种实现鸭子类型的方法,我尝试了这样的方法,但它也不起作用:

def double[T <: { def +(x:T):T }](x:T):T = { x + x }
def double[T <: { def +[U<:T](x:U):U}](x:T) = { x + x }

有人对此有想法吗?谢谢!


我在Haskell中找到,类似的函数可以这样写:

double x = x + x

只是想知道为什么我不能在 Scala 中做到这一点......

【问题讨论】:

  • "我在 Haskell 中发现,类似的函数可以这样写" 是的,但是在 Haskell 中,您定义的是 Num a =&gt; a -&gt; a 类型的函数,而不是 a -&gt; a(这将是等价的在 Scala 中尝试执行的操作),如果您将函数编写为 double :: a -&gt; a; double x = x + x,您将得到与 Scala 中相同的错误。

标签: scala generics functional-programming type-inference duck-typing


【解决方案1】:

不是每个类型 T 都有一个 + 方法,所以这是行不通的。奇怪的错误消息来自编译器将第一个x 视为String,因为每种类型都有一个toString 方法,这是它可以将通用T 视为具有+ 的唯一方法。但是随后T 被传递给+,而不是String,它需要第二次隐式转换才能使其工作——即使我们这样做了,它也会返回String 而不是@ 987654334@.

问题是我们需要一种方法来证明T 具有+ 操作。据我所知,标准库中没有任何东西可以做到这一点,但我们可以创建一个类型类来提供类型可以“加倍”的证据。

trait CanDouble[A] {
    def double(a: A): A
}

// Create some instances for types we know, like String, or numeric types
implicit val StringDouble: CanDouble[String] = new CanDouble[String] {
    def double(a: String): String = a + a
}

// Uses the Numeric type class to create a CanDouble for all Numeric types
implicit def numericDouble[A: Numeric]: CanDouble[A] = {
    new CanDouble[A] {
         def double(a: A): A = implicitly[Numeric[A]].plus(a, a)
    }
}

现在我们可以定义double 方法,该方法需要类型类CanDouble 的证据。

def double[A: CanDouble](a: A): A = implicitly[CanDouble[A]].double(a)

scala> double(1)
res4: Int = 2

scala> double(0.4)
res5: Double = 0.8

scala> double("a")
res6: String = aa

理想情况下,您可以将StringDoublenumericDouble 等所有类型类实例放在伴随对象CanDouble 中。


我不认为结构类型在这里可以工作,因为您不允许在结构优化中使用抽象类型参数,该参数是在优化之外定义的(类型参数T)。来自SLS

在结构细化的方法声明中,任何值参数的类型只能引用包含在细化内的类型参数或抽象类型。也就是说,它必须要么引用方法本身的类型参数,要么引用细化中的类型定义。此限制不适用于方法的结果类型。

无论如何,通常应该避免使用结构类型,因为它们非常慢。在这种情况下应该首选类型类。

【讨论】:

  • 谢谢!但是为什么它可以像Haskell中的double x = x + x一样简单地写...我以为Haskell也是一种使用静态类型的函数式语言..
  • Scala 不是 Haskell。我不知道为什么你可以在 Haskell 中做到这一点,但我的回答描述了为什么你不能在 Scala 中做到这一点,这就是重点。
  • 在 Haskell 中,在给定的时间范围内只能有一个 + 函数。在 Scala 中,很多对象都可以有一个+ 方法。因此,当你写+ 时,Haskell 已经知道它的签名,而 Scala 不知道,必须得到帮助。
  • @EugeneBurmako 如果函数是 typeclass 的一部分(a 可以在这里抽象) - 对于不同类型的 a,您可能有多个实现 +(实例)在整个构建中.回到 Scala——对象本身可以被视为函数的参数(a 的一些实现)——那么(大约)scala 的代码只是一组函数。如果您将对象添加到签名中 - 找到合适的不是问题(可能除了类型参数和继承)。理论上当然:)
  • @Eugene Burmako 如果你的意思是haskell,它就像在scala中一样——你不能有模棱两可的隐含,所以你需要导入适当的隐含。如果没有歧义 - 只需使用适当类型的参数调用函数(您可以显式指定类型而不是 haskell 中的类型推断)。隐含解析的原理非常相似(你可以检查my answer 的结尾与在scala 中实现的haskell 的Num more-less),Haskell 的唯一优点是更好的类型推断。
【解决方案2】:

在haskell中你可以:

Prelude> let double x = x + x // (1)
Prelude> let quadruple x = double (double x) //(2)

Prelude> :t double
double :: Num a => a -> a
Prelude> :t quadruple
quadruple :: Num a => a -> a

在 Scala 中,您必须明确指定 Num

scala> def double[T: Numeric] (a: T) = implicitly[Numeric[T]].plus(a, a)
double: [T](a: T)(implicit evidence$1: Numeric[T])T

scala> def quadruple[T: Numeric](a: T) = double(double(a))
quadruple: [T](a: T)(implicit evidence$1: Numeric[T])T

因为 haskell 的类型推断更聪明。 (1) 第一行确实找到了 typeclass Num:

Prelude> :info Num
class Num a where
  (+) :: a -> a -> a //looks like structural types, but ...
  (*) :: a -> a -> a
  (-) :: a -> a -> a
  negate :: a -> a
  abs :: a -> a
  signum :: a -> a
  fromInteger :: Integer -> a
    -- Defined in ‘GHC.Num’ //... but here is implementations found accross build - they are explicitly saying that they are instances of Num

instance Num Integer -- Defined in ‘GHC.Num’
instance Num Int -- Defined in ‘GHC.Num’
instance Num Float -- Defined in ‘GHC.Float’
instance Num Double -- Defined in ‘GHC.Float’

Scala 在结构类型方面也存在问题——你不能定义多态结构类型(不仅如此——你不能定义多态 lambdas)"Parameter type in structural refinement may not refer to an abstract type defined outside that refinement"

否则Num 将在 Scala 中定义为:

implicit class Num[T <: { def +(x:T):T }](a: T) = ... //will not work, and pretty slow by the way

查看其他答案以了解它的真正定义方式 (Numeric)。

在第 (2) 行编译器从 double 的应用程序推断 x (Num x) 的输入类型。 Scala 就是做不到这一点。它类似于haskell 的Num 将是:

scala> trait Num[T]{ val a: T; def + (b: Num[T]): Num[T] }
defined trait Num

scala> implicit class NumInt(val a: Int) extends Num[Int] {override def + (b: Num[Int]) = NumInt(a + b.a)}
defined class NumInt

scala> def double[T](a: Num[T]) = a + a
double: [T](a: Num[T])Num[T]

scala> double(5)
res4: Num[Int] = NumInt@424f5762

但问题还是一样 - 你必须在 scala 中指定输入类型 (a: Num[T]),它无法推断它们。

但是,即使在 Haskell 中,您也不能这样说:

Prelude> let double x = x +++ x

<interactive>:28:18:
    Not in scope: ‘+++’
    Perhaps you meant ‘++’ (imported from Prelude)
Otherwise `Num` would be defined in Scala as something like that:

而Haskell真正的鸭式打字也不是那么好用:http://chrisdone.com/posts/duck-typing-in-haskell

【讨论】:

    【解决方案3】:

    这是何时使用类型类的完美示例。

    + 只是一个函数。您没有向编译器提供诸如

    之类的信息
    def +(t : T, t : T) : T = ...
    

    而你不能,因为你不知道 T 是什么。

    在这里它将按如下方式工作。你有一个类型构造函数,叫做 Doubles:

    trait Doubles[T]{
      def double(t : T) : T
    }
    

    现在在一个伴生对象中,为了方便起见,我将重写你的双重函数如下:

    object Doubles{
      def double[T](t : T)(implicit doubles : Doubles[T]) =    doubles.double(t)
    }
    

    所以这就是说,我可以加倍 T,只要范围内有 T 的 Doubles,或者您明确为我提供 T 的 Doubles。否则,我将无法加倍 T,而您'会得到一个编译器错误。

    以下将是该类型类 Doubles[T] 的实例:

    object Implicits{
        //Now, you wouldn't want to have to write this for 
        //every kind of number.  Scala already provides you with a numeric
        //typeclass.  So here's a function that gives a Doubles[N] 
        //whenever you ask for a Doubles[Numeric[T]], i.e. a Doubles for a     
        //member of the Numeric typeclass:
    
        implicit def numDoubler[N](implicit num : Numeric[N]) : Doubles[N] = new Doubles[N]{
            def double(n : N) : N = num.plus(n,n)
        }
    
        implicit object stringDoubler extends Doubles[String]{
            def double(t : String) : String = t + t
        }
    
     //So something like this is no longer needed:
     // implicit object intDoubler extends Doubles[Int]{
     //   def double(t : Int) : Int = t + t
     // }
    
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-01-23
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多