【问题标题】:Scala: generic weighted average functionScala:通用加权平均函数
【发布时间】:2017-09-08 23:40:46
【问题描述】:

我想实现一个通用的加权平均函数,它放宽了对值和权重为同一类型的要求。即,我想支持说:(value:Float,weight:Int)(value:Int,weight:Float) 参数的序列,而不仅仅是:(value:Int,weight:Int)。 [在此之前,请参阅我之前的 question。]

这是我目前拥有的:

def weightedSum[A: Numeric](weightedValues: GenSeq[(A, A)]): (A, A)

def weightedAverage[A: Numeric](weightedValues: GenSeq[(A, A)]): A = {
    val (weightSum, weightedValueSum) = weightedSum(weightedValues)
    implicitly[Numeric[A]] match {
        case num: Fractional[A] => ...
        case num: Integral[A] => ...
        case _ => sys.error("Undivisable numeric!")
    }
}

例如,如果我喂它,这将非常有效:

val values:Seq[(Float,Float)] = List((1,2f),(1,3f))
val avg= weightedAverage(values)

但是,如果我不将权重从Int“向上转换”到Float

val values= List((1,2f),(1,3f)) //scalac sees it as Seq[(Int,Float)] 
val avg= weightedAverage(values)

Scala 编译器会告诉我:

错误:找不到类型的证据参数的隐式值 数值[AnyVal]
val avg= weightedAverage(values)

有没有办法解决这个问题?

我曾尝试编写一个NumericCombine 类,我用AB 参数化了该类,它将这些类型“组合”成一个“通用”类型AB(例如,组合Float 和@ 987654343@ 给你Float) :

abstract class NumericCombine[A: Numeric, B: Numeric] {
    type AB <: AnyVal

    def fromA(x: A): AB
    def fromB(y: B): AB
    val num: Numeric[AB]

    def plus(x: A, y: B): AB = num.plus(fromA(x), fromB(y))
    def minus(x: A, y: B): AB = num.minus(fromA(x), fromB(y))
    def times(x: A, y: B): AB = num.times(fromA(x), fromB(y))
}

并且我设法在此基础上使用 typeclass 模式编写了简单的 timesplus 函数,但是由于 NumericCombine 引入了路径依赖类型 AB,因此“组合”类型被证明是更多比我预想的要难。查看this 问题了解更多信息,并查看here 了解NumericCombine 的完整实现。

更新

作为对another question(完整工作演示here)的回答,已经获得了一个稍微令人满意的解决方案,但是考虑到@ziggystar 在discussion 中提出的观点,仍有一些设计改进的空间。

【问题讨论】:

  • 是的,您遇到的问题类似于不久前的something I asked about。我找不到满意的答案。
  • @jwvh 谢谢。是的,我遇到了这个问题。我不明白为什么添加的隐式没有做任何事情。
  • @sailfish009 感谢您的链接。是的,我了解隐含证据参数的工作原理。我试图让 scalac 不选择IntFloat = AnyVal 的最低分母,因为显然没有Numeric[AnyVal]

标签: scala generics implicit-conversion implicit


【解决方案1】:

线性组合

我认为涉及通过S 类型的标量对T 类型的某些元素进行加权/缩放的更一般的任务是线性组合的任务。以下是一些任务的权重限制:

所以根据这种分类最一般的情况是线性组合。 根据维基百科,它要求权重 S 是一个字段,而 TS 上形成一个 vector space

编辑:您可以对类型提出的真正最一般的要求是T 在环S 上形成一个module (wiki),或者T 是一个S-模块。

尖顶

您可以使用类型类设置这些要求。还有spire,它已经有FieldVectorSpace 的类型类。我自己没用过,你自己去看看吧。

Float/Int 不起作用

从这个讨论中也可以明显看出,并且您已经观察到,将Float 作为权重,将Int 作为元素类型是行不通的,因为整数不会形成实数上的向量空间。您必须先将Int 提升为Float

通过类型类推广

标量类型只有两个主要候选者,即FloatDouble。 并且主要只有Int 是推广的候选者,因此您可以将以下作为简单但不那么通用的解决方案:

case class Promotable[R,T](promote: R => T)

object Promotable {
  implicit val intToFloat = Promotable[Int,Float](_.toFloat)
  implicit val floatToDouble = Promotable[Float,Double](_.toDouble)
  implicit val intToDouble = Promotable[Int,Double](_.toDouble)

  implicit def identityInst[A] = Promotable[A,A](identity)
}As a "small" solution you could write a typeclass 

def weightedAverage[S,VS](values: Seq[(S,VS)])(implicit p: Promotable[VS,S]) = ???

【讨论】:

  • 谢谢!最初考虑了当场转换权重和值的想法,但认为可能效率低下,也不完全是我们在数学上所做的。这个:gist.github.com/ShahOdin/9b3a5326c7ee355c1cbe76e8bfb2a97c 是我们在讨论了一个相关问题后得出的结论:stackoverflow.com/questions/43392597/… 这是有效的,但是你对限制类型提出了一个有效的观点。您如何最好地将其纳入^
  • @ShS 你的类型比我的更通用,我认为这项任务不需要这种通用性。可以表示AB这两种类型产生第三种类型AB,而我的建议只要求向量空间的类型可以转换为标量类型。
  • 好吧,您引入了向量空间的概念,但您没有充分探索它的潜力。所以在数学上,一个向量空间由values 集合Vplus/minusweight 字段Wtimes/divide 以及缩放:scale(w:W,v:V):V 组成提升/组合仅适用于 1D。即,支持values 类型的最佳方式是:(Int,Int)weights 类型比如Float?这让我想知道基于Numeric 进行参数化是否是一个好主意,因为Numeric 并非旨在支持说元组。
  • 这是我面临的更大困境的体现。目前我有 1D、2D 和 3D plus 函数采用 A(A,B)(A,B,C)。这意味着我必须拥有我可能想要实现的任何统计函数的1D, 2D3D 版本,这不是很好。我的理解是我可以使用更高的 kindered 函数来避免这种情况。不过,我不确定最好的方法是什么。我还没有访问过Spire,这可能就是他们所做的,但我会对在数据结构上泛化操作(Numeric 等)的最小方式感兴趣
  • @ShS 布尔值可以形成一个二进制字段。你可以单独对权重求和,因为它们形成了一个场,并且定律 (a + b)v = av + bv 控制着这个加法相对于向量空间的行为。如果您真的想要这个最终通用的解决方案,我认为您应该(1)查看 spire 中的实现,如果这不适合您(2)根据场和向量空间的公理自行推出。但这并不能解决您无法将不能很好地组合在一起的类型的问题。我认为后者的正确解决方案是让调用者提升类型。
【解决方案2】:

首先你的模板是错误的。 (很抱歉,如果“模板”表达式是错误的——我是 scala 的新手)。 您的函数需要两个元素属于相同类型( [A: Numeric] )的元组,而不是元素具有不同类型( [A: Numeric, B: Numeric] )( (Int, Float) vs (Float,浮动) )

无论如何,下面的编译器都可以编译,并希望在你用你想要的微积分填充它后能很好地工作。

import scala.collection._

def weightedSum[A: Numeric, B: Numeric](weightedValues: GenSeq[(A,B)]): (A,B) = {
  weightedValues.foldLeft((implicitly[Numeric[A]].zero, implicitly[Numeric[B]].zero)) { (z, t) =>
    (   implicitly[Numeric[A]].plus(z._1, t._1), 
        implicitly[Numeric[B]].plus(z._2, t._2)
    ) 
  } 
}

def weightedAverage[A: Numeric, B: Numeric](weightedValues: GenSeq[(A,B)]): A = {
  val (weightSum, weightedValueSum) = weightedSum(weightedValues)
  implicitly[Numeric[A]] match {
    case num: Fractional[A] => implicitly[Numeric[A]].zero
    case num: Integral[A] => implicitly[Numeric[A]].zero
    case _ => sys.error("Undivisable numeric!")
  }
}

val values1: Seq[(Float, Float)] = List((1, 2f), (1, 3f))
val values2: Seq[(Int, Float)] = List((1, 2f), (1, 3f))

val wa1 = weightedAverage(values1)
val wa2 = weightedAverage(values2)

【讨论】:

  • 最初的尝试没有“错误”。如果您访问过该链接,您会注意到它起作用的原因是发生了价值转换。 (与val x=2 val y:Double=x 工作的原因相同)而且您的weighted sum 实现不正确(谷歌加权总和看看是什么)困难在于您的实现方便地忽略的“不同”类型的参数相乘/求和。
  • 1) “初始尝试”完全错误,也没有编译 - 正如 ShS 所述。 2)问题是关于类型的。 3)我回答的目的是指出对类型的错误处理,而不是“加权和”的实现(我说“......在你用你想要的微积分填充它之后”)。 4)看到代码用(Int,Double)编译,求和没问题,只需添加一个'mult'或你需要的任何其他东西(我们不是那个“手头”)。
猜你喜欢
  • 2010-09-21
  • 1970-01-01
  • 2016-11-09
  • 2016-02-13
  • 1970-01-01
  • 1970-01-01
  • 2015-06-02
  • 2016-05-23
  • 1970-01-01
相关资源
最近更新 更多