重载运算符的编码样式

时间:2019-06-21 02:08:26

标签: scala pattern-matching coding-style

假设我有一个名为Rational的类,它“纯”地表示有理数,即它将a / b的表示形式保持为(a, b),并实现了常用的运算符+, -, *, /和其他运算符来处理这些元组,而不是评估每个操作的实际分数。

现在假设我想定义将Rational实例添加到Int时发生的情况,以及已经为Rational添加到Rational的行为。然后,当然,我可能最终想将Rational添加到Double,或添加到FloatBigInt其他数字类型...

方法1:提供+(Rational, _)的几种实现方式:

def + (that:Rational):Rational  = {
    require(that != null, "Rational + Rational: Provided null argument.")
    new Rational(this.numer * that.denom + that.numer * this.denom, this.denom * that.denom)
}

def + (that:Int): Rational = this + new Rational(that, 1) // Constructor takes (numer, denom) pair

def + (that:BigInt): Rational = ....
.
.
.

方法2:Any上的模式匹配:

def + (that:Any):Rational  = {
    require(that != null, "+(Rational, Any): Provided null argument.")
    that match {
        case that:Rational => new Rational(this.numer * that.denom + that.numer * this.denom, this.denom * that.denom)
        case that:Int | BigInt => new Rational(this.numer + that * this.denom, this.denom) // a /b + c = (a + cb)/b
        case that:Double => ....
        .
        .
        .
        case _ => throw new UnsupportedOperationException("+(Rational, Any): Unsupported operand.")
     }
}

我从模式匹配方法中看到的一个好处是可以节省实际的源代码行,但可能会降低可读性。也许更关键的是,当我获得未定义行为+的类型时,我可以控制自己的工作。我不确定通过第一种方法如何实现,也许通过在所有其他方法的下面添加Any的重载?无论哪种方式,听起来都是危险的。

关于应该选择第一种方法还是第二种方法的想法?我没有看到任何安全问题吗?我是否愿意接受ClassCastException或其他类型的例外?

1 个答案:

答案 0 :(得分:3)

强制执行编译时错误的方法是确保plus方法实际上不能通过类型约束,隐式参数等接受类型Any

一种解决方法是利用scala Numeric类型类。最好为Rational创建一个实例,因为您可以轻松实现所有必需的方法,并且此时可以将plus定义为

def +[T: Numeric](that: T) : Rational

您现在还可以提取隐式toInt参数的toLong / toFloat / toDouble / Numeric方法来处理未知类,如果需要的话,也不会抛出运行时错误-即使您不这样做,也至少可以显着减少可以传递的错误类型。

您还可以定义自己的类型类,并为要支持的类型定义适当的实例。然后,您可以将加法逻辑保留在+方法中,或将其移入类型类实例:

trait CanBeAdded[T] {
  def add(t: T, rational: Rational) : Rational
}

object CanBeAdded {
  implicit val int = new CanBeAdded[Int] {
    override def add(t: Int, rational: Rational): Rational = ???
  }

  implicit val long = new CanBeAdded[Long] {
    override def add(t: Long, rational: Rational): Rational = ???
  }

  implicit val rational = new CanBeAdded[Rational] {
    override def add(t: Rational, rational: Rational): Unit = ???
  }
}

case class Rational(a: BigInt, b: BigInt) {
  def +[T: CanBeAdded](that: T) = implicitly[CanBeAdded[T]].add(that, this)
}

我喜欢第二种选择,因为我不得不怀疑允许将您的Rational类型添加到任何数字类型是否有意义。您提到您希望+能够接受Double s,但是精确的表示形式与通常在Double s中出现的舍入误差相结合似乎会导致某些奇怪而违反直觉的行为,结果没有多大意义。