【问题标题】:Coding with Scala implicits in style使用 Scala 编码隐含风格
【发布时间】:2026-01-14 04:40:01
【问题描述】:

是否有任何样式指南描述了如何使用 Scala 隐式编写代码?

隐式非常强大,因此很容易被滥用。是否有一些通用准则说明何时适合使用隐式以及何时使用隐式代码会使代码变得模糊?

【问题讨论】:

  • 如果我们将 SO 设为样式指南,则可以制作此 CW
  • 有任何命名约定吗?我见过像def foo2bar 这样的东西,但我真的想知道是否有任何共识......
  • @soc 使用def FooToBar 怎么样? (首字母大写)这样被其他非隐式方法隐藏的可能性较小
  • @soc 就个人而言,我赞成BippyIsBoppy 约定;在标准库中使用:ByteIsIntegralCharIsIntegralFloatIsFractional 等。

标签: scala coding-style implicit


【解决方案1】:

我认为目前还没有社区范围的风格。我见过很多约定。我将描述我的,并解释我为什么使用它。

命名

我将我的隐式转换称为其中之一

implicit def whatwehave_to_whatwegenerate
implicit def whatwehave_whatitcando
implicit def whatwecandowith_whatwehave

我不希望这些被明确使用,所以我倾向于使用相当长的名称。不幸的是,类名中经常有数字,所以whatwehave2whatwegenerate 约定变得混乱。比如:tuple22myclass--你说的是Tuple2还是Tuple22

如果隐式转换的定义远离转换的参数和结果,我总是使用x_to_y 表示法以获得最大的清晰度。否则,我将名称更多地视为评论。所以,例如,在

class FoldingPair[A,B](t2: (A,B)) {
  def fold[Z](f: (A,B) => Z) = f(t2._1, t2._2)
}
implicit def pair_is_foldable[A,B](t2: (A,B)) = new FoldingPair(t2)

我同时使用类名和隐式作为一种注释,说明代码的意义——即在对中添加一个fold 方法(即Tuple2)。

用法

皮条客我的图书馆

对于 pimp-my-library 风格的构造,我最常使用隐式转换。我在所有地方都这样做,它添加了缺少的功能使生成的代码看起来更干净。

val v = Vector(Vector("This","is","2D" ...
val w = v.updated(2, v(2).updated(5, "Hi"))     // Messy!
val w = change(v)(2,5)("Hi")                    // Okay, better for a few uses
val w = v change (2,5) -> "Hi"                  // Arguably clearer, and...
val w = v change ((2,5) -> "Hi", (2,6) -> "!")) // extends naturally to this!

现在, 为隐式转换付出了性能损失,所以我不会以这种方式在热点中编写代码。但除此之外,我很可能会使用 pimp-my-library 模式而不是 def,一旦我超过了相关代码中的少数用途。

还有另一个考虑因素,即工具在显示隐式转换的来源方面不如方法的来源可靠。因此,如果我正在编写困难的代码,并且我希望任何使用或维护它的人都必须努力研究它以了解需要什么以及它是如何工作的,我——这几乎是从一种典型的 Java 哲学——更多可能会以这种方式使用 PML 来使步骤对受过训练的用户更加透明。 cmets会提示代码需要深入理解;一旦你深入了解,这些变化会帮助而不是伤害。另一方面,如果代码做的事情相对简单,我更有可能保留 defs,因为如果我们需要进行更改,IDE 将帮助我或其他人快速上手。

避免显式转换

我尽量避免显式转换。你当然可以写

implicit def string_to_int(s: String) = s.toInt

但它非常危险,即使您似乎在用 .toInt 填充所有字符串。

我做的主要例外是包装类。例如,假设您想让一个方法接受具有预先计算的哈希码的类。我会

class Hashed[A](private[Hashed] val a: A) {
  override def equals(o: Any) = a == o
  override def toString = a.toString
  override val hashCode = a.##
}
object Hashed {
  implicit def anything_to_hashed[A](a: A) = new Hashed(a)
  implicit def hashed_to_anything[A](h: Hashed[A]) = h.a
}

然后自动取回我开始使用的任何类,或者在最坏的情况下,通过添加类型注释(例如x: String)。原因是这使得包装类的侵入性最小。你真的不想知道包装器;您有时只需要该功能。您无法完全避免注意到包装器(例如,您只能在一个方向上修复 equals,有时您需要返回原始类型)。但这通常可以让您轻松编写代码,而这有时只是要做的事情。

隐式参数

隐式参数非常混乱。我尽可能使用默认值。但有时你不能,尤其是通用代码。

如果可能,我会尝试使隐式参数成为其他方法永远不会使用的东西。例如,Scala 集合库有一个CanBuildFrom 类,除了集合方法的隐式参数之外,它几乎完全无用。因此,意外串扰的危险很小。

如果这是不可能的——例如,如果一个参数需要传递给几个不同的方法,但这样做确实会分散代码正在做的事情(例如尝试在算术中间进行日志记录),那么我没有让一个通用类(例如String)成为隐式val,而是将它包装在一个标记类中(通常使用隐式转换)。

【讨论】:

  • 很好的答案 - 我喜欢 pair_is_foldable 命名风格;比我一直在做的要干净得多:-)
  • @oxbow_lakes - 在我修复格式之前你能够破译它给我留下了深刻的印象!
  • 当您想在名称中添加特殊字符时,Scala 允许使用反引号,因此您可以使用括号或符号和分组形式,例如implicit def '(T[S])to(String[S])'。我发现在使用隐式表示层次结构时很有用。请参阅我关于模拟析取类型的答案。此处无法显示反引号,因此我使用了单引号。
【解决方案2】:

我不相信我遇到过任何东西,所以让我们在这里创建它!一些经验法则:

隐式转换

当从A 隐式转换为B 时,不是每个A 都可以被视为B 的情况,通过拉皮条来实现toX 转换,或类似的东西。例如:

val d = "20110513".toDate //YES
val d : Date = "20110513" //NO!

别生气!用于非常常见的核心库功能,而不是在每个类中拉皮条为了它!

val (duration, unit) = 5.seconds      //YES
val b = someRef.isContainedIn(aColl)  //NO!
aColl exists_? aPred                  //NO! - just use "exists"

隐式参数

使用这些来:

  • 提供typeclass 实例(如scalaz
  • 注入一些显而易见的(例如为某些工作人员调用提供ExecutorService
  • 作为依赖注入的一个版本(例如,在实例上传播 service-type 字段的设置)

不要为了懒惰而使用

【讨论】:

  • 不应该是type class,而不是typeclass吗?
【解决方案3】:

这一款鲜为人知,尚未命名(据我所知),但它已成为我个人的最爱之一。

所以我要在这里冒险,并将其命名为“pimp my type class”模式。也许社区会想出更好的东西。

这是一个由 3 部分组成的模式,完全由隐式构建而成。它也已经在标准库中使用(从 2.9 开始)。此处通过大幅缩减的 Numeric 类型类进行了解释,希望大家熟悉。

第 1 部分 - 创建类型类

trait Numeric[T] {
   def plus(x: T, y: T): T
   def minus(x: T, y: T): T
   def times(x: T, y: T): T
   //...
}

implicit object ShortIsNumeric extends Numeric[Short] {
  def plus(x: Short, y: Short): Short = (x + y).toShort
  def minus(x: Short, y: Short): Short = (x - y).toShort
  def times(x: Short, y: Short): Short = (x * y).toShort
  //...
}

//...

第 2 部分 - 添加提供中缀操作的嵌套类

trait Numeric[T] {
  // ...

  class Ops(lhs: T) {
    def +(rhs: T) = plus(lhs, rhs)
    def -(rhs: T) = minus(lhs, rhs)
    def *(rhs: T) = times(lhs, rhs)
    // ...
  }
}

第 3 部分 - 使用操作的类型类的 Pimp 成员

implicit def infixNumericOps[T](x: T)(implicit num: Numeric[T]): Numeric[T]#Ops =
  new num.Ops(x)

那就用吧

def addAnyTwoNumbers[T: Numeric](x: T, y: T) = x + y

完整代码:

object PimpTypeClass {
  trait Numeric[T] {
    def plus(x: T, y: T): T
    def minus(x: T, y: T): T
    def times(x: T, y: T): T
    class Ops(lhs: T) {
      def +(rhs: T) = plus(lhs, rhs)
      def -(rhs: T) = minus(lhs, rhs)
      def *(rhs: T) = times(lhs, rhs)
    }
  }
  object Numeric {
    implicit object ShortIsNumeric extends Numeric[Short] {
      def plus(x: Short, y: Short): Short = (x + y).toShort
      def minus(x: Short, y: Short): Short = (x - y).toShort
      def times(x: Short, y: Short): Short = (x * y).toShort
    }
    implicit def infixNumericOps[T](x: T)(implicit num: Numeric[T]): Numeric[T]#Ops =
      new num.Ops(x)
    def addNumbers[T: Numeric](x: T, y: T) = x + y
  }
}

object PimpTest {
  import PimpTypeClass.Numeric._
  def main(args: Array[String]) {
    val x: Short = 1
    val y: Short = 2
    println(addNumbers(x, y))
  }
}

【讨论】: