【问题标题】:Why does + operator on parameterized types in scala always result in string为什么scala中参数化类型的+运算符总是导致字符串
【发布时间】:2015-06-14 02:33:31
【问题描述】:

考虑一个类如下:

import scala.collection.mutable.{HashMap => MutableHashMap}
class CustomHashMap[K,V](hashMap: MutableHashMap[K,V], initVal: V) {
  def addMaps(first: MutableHashMap[K, V], second: MutableHashMap[K, V]): MutableHashMap[K, V] = {
    second.foreach(pair => { first += (pair._1 -> (first.getOrElse(pair._1, initVal) + pair._2)) } )
    //The above line throws a compile time error
    first
  }
  //Other functions
}

添加两个参数化类型后,会出现编译时错误提示

expected (K,V) recieved (K,String)

我想知道为什么 scala 会进行这种隐式转换?由于在 java 中不允许运算符重载,这似乎是合乎逻辑的,但在 scala V 的情况下,实际上可能是一个可以为它定义方法 + 的类。

【问题讨论】:

  • Scala 不是鸭式语言。
  • V 可以是一个有+方法的类,但不能保证一定会!它可能是别的东西。
  • @m-z 那么在scala中没有办法实现这样的功能吗?
  • 不清楚你在追求什么。 V 应该是数字吗?或者只是有一个+ 方法?无论哪种方式,它都需要一些约束,例如结构类型或类型类。
  • 听起来你想要一个幺半群。在这段代码中,您实际上是在尝试为映射定义幺半群(您的代码结果在概念上与 scalaz 在first |+| second 中所做的相同,但是您使用可变映射并要求为每个操作提供幺半群的零) .

标签: java scala


【解决方案1】:

这是一种手动实现这种事情的方法:

由于您似乎正在尝试定义一个幺半群,我将冒昧地将 initValaddMaps 移动到操作的定义。

这可以使用 Scala 中的通用类型类模式来完成,但您必须手动定义 + 对每种类型的含义,您想在地图中使用。

基本上你有一个特质Monoid

trait Monoid[T] {
  def mzero: T                        // your initVal
  def madd(first: T, second: T): T    // the + operation
}

比您为扩展该特征的每种类型定义隐式实现。您可以在

中定义它们
  • Monoid 的伴生对象,会自动使用,
  • T 类的伴生对象,也会自动使用,
  • 隐含在其他地方,但您必须手动导入。

这是一个 Monoid 伴随对象的示例,它定义了字符串和各种数字的实现:

object Monoid {
  implicit object StringMonoid extends Monoid[String] {
    def mzero = ""
    def madd(first: String, second: String) = first + second
  }

  implicit def NumericMonoid[T](implicit ev: Numeric[T]): Monoid[T] = 
    new Monoid[T] {
      import Numeric.Implicits._

      def mzero = ev.zero
      def madd(first: T, second: T) = first + second
    }
}

然后在您的addMaps 函数中请求地图的元素应该是Monoids,并使用由 monoid 实现提供的元素上的操作:

def addMaps[K, V](first: MutableHashMap[K, V], second: MutableHashMap[K, V])
                 (implicit ev: Monoid[V]): MutableHashMap[K, V] = {
  second.foreach { pair => 
    first += (pair._1 -> ev.madd(first.getOrElse(pair._1, ev.mzero), pair._2)) }
  first
}

这是一个测试它是如何工作的:

scala> addMaps(MutableHashMap(1 -> 2, 3 -> 4), MutableHashMap(1 -> 3, 5 -> 7))
res1: scala.collection.mutable.HashMap[Int,Int] = Map(5 -> 7, 1 -> 5, 3 -> 4)

scala> addMaps(MutableHashMap(1 -> "foo", 2 -> "bar"), MutableHashMap(1 -> "baz", 3 -> "qoo"))
res2: scala.collection.mutable.HashMap[Int,String] = Map(2 -> bar, 1 -> foobaz, 3 -> qoo)

【讨论】:

  • 我想我必须先了解一些 scala 才能将其标记为正确 0.o
  • @Sohaib 基本上 Monoid 与其他答案中的 Adder 相同,但有自己的 initVal。您可以使用相同的做法:例如,伴随对象中的隐式参数和实现来实现 Adder。另外,请不要犹豫,要求澄清。
  • 我理解 Monoid 的特征。我不明白的是对象 Monoid。特别是 StringMonoid 和 NumericMonoid。为什么一个是对象,另一个是方法?我知道像stringWrapper(s: String): RichString 这样的隐式方法是如何工作的。我没有问,因为我认为这些可能是我还不理解的特定语言结构,我最好单独搜索它们而不是在这里问。
  • @Sohaib 当 Scala 查找隐式参数时,它也会尝试查找到所需类型的隐式转换。因此,要为具有某些约束的所有类型(例如,[A <: Base][A : Ordering]Seq[A] 等)定义类型类的实现,您必须将其作为函数来执行。见stackoverflow.com/questions/30680206/…(这也是关于幺半群的:-)但是对于String,你不需要那个,你可以提供一个简单的隐式objectval
  • @Sohaib 所以这个带有Numeric 的实现将适用于IntDoubleLongBigIntBigDecimal 以及任何其他类型类的实现Numeric。 (但组合类型,例如 first: Map[A, Int]second: Map[A, Double] 和结果 Map[A, Double] 更复杂)。
【解决方案2】:

我相信问题是因为您正在尝试执行 V + V 并且 Scala 将类型 V 推断为字符串。这是我尝试向编译器指定V 将扩展一个支持V + V => V 的特征(称为Adder)。

import scala.collection.mutable.{HashMap => MutableHashMap}

trait Adder[A]{
    def +(b: A): A
}

class CustomHashMap[K, V <: Adder[V]](hashMap: MutableHashMap[K,V], initVal: V) {

  def addMaps(first: MutableHashMap[K, V], second: MutableHashMap[K, V]): MutableHashMap[K, V] = {
    second.foreach(pair => { first += (pair._1 -> (first.getOrElse[V](pair._1, initVal) + pair._2)) } )
    first
  }
  //Other functions ...
}

上述解决方案需要V 扩展Adder。如果我们想让这对用户更容易,我们可以使用隐式转换。为了添加自定义类型,您需要对作用域进行隐式转换(类似于我们对 Int、String 和 Double 所做的)。

import scala.collection.mutable.{HashMap => MutableHashMap}

trait Adder[A]{
  def add(x:A, y: A): A
}

implicit object IntAdder extends Adder[Int] {
  def add(x: Int, y: Int): Int = x + y
}

implicit object StringAdder extends Adder[String] {
  def add(x: String, y: String): String = x + y
}

implicit object DoubleAdder extends Adder[Double] {
  def add(x: Double, y: Double): Double = x + y
}

class CustomHashMap[K, V](hashMap: MutableHashMap[K,V], initVal: V)(implicit m: Adder[V]) {

  def addMaps(first: MutableHashMap[K, V], second: MutableHashMap[K, V]): MutableHashMap[K, V] = {
    second.foreach(pair => { first += (pair._1 -> m.add( first.getOrElse[V](pair._1, initVal), pair._2)) } )
    first
  }
  //Other functions ...
}

【讨论】:

  • 那么任何不使用 trait adder 的对象都不能与这个类一起使用?
  • 是的,我也在考虑同样的事情......一个想法是有一个隐式转换,它将知道如何从某种类型 X 转换为 A。所以你可以有类似的默认实现整数、长整数、字符串、双精度。对于任何自定义类型,我想您确实需要提供一个 + 函数。
  • 您能否以更详细的方式将其添加到答案中? :)
  • 会的...但我必须在接下来的一个小时左右外出:(。我希望你能等到那时...
  • 是的,这与@Kolmar 非常相似,尽管我现在更喜欢他的解决方案,因为我理解了它。谢谢您的帮助。在将答案标记为正确之前,我将等待更多投票。
【解决方案3】:

只是为了补充 marios 的答案,您可能想要使用一个函数式编程库,该库已经为您提供了类型类 Semigroup(添加)或 Monoid(添加和空元素)。

例如Cats:

scala> import cats.Monoid

scala> def joinMaps[K, V](a: Map[K, V], b: Map[K, V])(implicit m: Monoid[V]) =
  b.foldLeft(a) { case (res, (k, v)) => 
    res + (k -> m.combine(res.getOrElse(k, m.empty), v)) 
  }

测试:

scala> import cats.std.list._  // has a Monoid for List

scala> joinMaps(Map("a" -> List(1, 2), "b" -> List(3, 4)), 
                Map("b" -> List(5, 6), "c" -> List(7, 8)))
res1: Map[String,List[Int]] = Map(a -> List(1, 2), b -> List(3, 4, 5, 6),
                                  c -> List(7, 8))

一般来说,如果你将加法函数分成一个类型类(这里是Monoid)而不是要求它成为你的值类型V &lt;: Adder的一部分,它会使你的代码更加模块化。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2011-10-29
    • 2018-05-21
    • 2017-04-08
    • 2021-02-13
    • 1970-01-01
    • 2017-04-07
    • 2012-06-14
    • 1970-01-01
    相关资源
    最近更新 更多