使用 Scala 隐式编码风格

发布于 2024-11-07 02:09:31 字数 96 浏览 0 评论 0原文

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

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

Are there any style guides that describe how to write code using Scala implicits?

Implicits are really powerful, and therefore can be easily abused. Are there some general guidelines that say when implicits are appropriate and when using them obscures code?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

紫﹏色ふ单纯 2024-11-14 02:09:32

这个产品鲜为人知,以至于还没有被命名(据我所知),但它已经成为我个人最喜欢的产品之一。

因此,我将在这里冒险,并将其命名为“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))
  }
}

This one is so little-known that it has yet to be given a name (to the best of my knowledge), but it's already firmly established as one of my personal favourites.

So I'm going to go out on a limb here, and name it the "pimp my type class" pattern. Perhaps the community will come up with something better.

This is a 3-part pattern, built entirely out of implicits. It's also already used in the standard library (since 2.9). Explained here via the heavily cut-down Numeric type class, which should hopefully be familiar.

Part 1 - Create a type class

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
  //...
}

//...

Part 2 - Add a nested class providing infix operations

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)
    // ...
  }
}

Part 3 - Pimp members of the type class with the operations

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

Then use it

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

Full code:

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))
  }
}
人疚 2024-11-14 02:09:31

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

命名

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

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 方法(即 元组2)。

用法

Pimp-My-Library

对于 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 来使步骤对受过训练的用户更加透明。注释会警告代码需要深入理解;一旦你深入了解,这些改变就会带来帮助而不是伤害。另一方面,如果代码做的事情相对简单,我更有可能将定义保留在适当的位置,因为如果我们需要进行更改,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)。原因是这使得包装类的侵入最小化。你并不是真的想了解包装器;你真的想了解包装器。有时你只需要功能。您无法完全避免注意到包装器(例如,您只能在一个方向上修复等于,有时您需要返回到原始类型)。但这通常可以让您轻松地编写代码,有时这正是您要做的事情。

隐式参数

隐式参数的混杂程度令人震惊。只要有可能,我都会使用默认值。但有时你不能,特别是对于通用代码。

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

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

I don't think there is a community-wide style yet. I've seen lots of conventions. I'll describe mine, and explain why I use it.

Naming

I call my implicit conversions one of

implicit def whatwehave_to_whatwegenerate
implicit def whatwehave_whatitcando
implicit def whatwecandowith_whatwehave

I don't expect these to be used explicitly, so I tend towards rather long names. Unfortunately, there are numbers in class names often enough so the whatwehave2whatwegenerate convention gets confusing. For example: tuple22myclass--is that Tuple2 or Tuple22 you're talking about?

If the implicit conversion is defined away from both the argument and result of the conversion, I always use the x_to_y notation for maximum clarity. Otherwise, I view the name more as a comment. So, for instance, in

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)

I use both the class name and the implicit as a sort of a comment about what the point of the code is--namely to add a fold method to pairs (i.e. Tuple2).

Usage

Pimp-My-Library

I use implicit conversions the most for pimp-my-library style constructions. I do this all over the place where it adds missing functionality or makes the resulting code look cleaner.

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!

Now, there is a performance penalty to pay for implicit conversions, so I don't write code in hotspots this way. But otherwise, I am very likely to use a pimp-my-library pattern instead of a def once I go above a handful of uses in the code in question.

There is one other consideration, which is that tools are not as reliable yet at showing where your implicit conversions come from as where your methods come from. Thus, if I'm writing code that is difficult, and I expect that anyone who is using or maintaining it is going to have to study it hard to understand what is required and how it works, I--and this is almost backwards from a typical Java philosophy--am more likely to use PML in this fashion to render the steps more transparent to a trained user. The comments will warn that the code needs to be understood deeply; once you understand deeply, these changes help rather than hurt. If, on the other hand, the code's doing something relatively straightforward, I'm more likely to leave defs in place since IDEs will help me or others quickly get up to speed if we need to make a change.

Avoiding explicit conversions

I try to avoid explicit conversions. You certainly can write

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

but it's awfully dangerous, even if you seem to be peppering all your strings with .toInt.

The main exception I make is for wrapper classes. Suppose, for example, you want to have a method take classes with a pre-computed hash code. I would

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
}

and get back whatever class I started with either automatically or, at worst, by adding a type annotation (e.g. x: String). The reason is that this makes wrapper classes minimally intrusive. You don't really want to know about the wrapper; you just need the functionality sometimes. You can't completely avoid noticing the wrapper (e.g. you can only fix equals in one direction, and sometimes you need to get back to the original type). But this often lets you write code with minimal fuss, which is sometimes just the thing to do.

Implicit parameters

Implicit parameters are alarmingly promiscuous. I use default values whenever I possibly can instead. But sometimes you can't, especially with generic code.

If possible, I try to make the implicit parameter be something that no other method would ever use. For example, the Scala collections library has a CanBuildFrom class that is almost perfectly useless as anything other than an implicit parameter to collections methods. So there is very little danger of unintended crosstalk.

If this is not possible--for example, if a parameter needs to be passed to several different methods, but doing so really distracts from what the code is doing (e.g. trying to do logging in the middle of arithmetic), then rather than make a common class (e.g. String) be the implicit val, I wrap it in a marker class (usually with an implicit conversion).

生生漫 2024-11-14 02:09:31

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

隐式转换

当从 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
  • 作为依赖注入的一个版本(例如在实例上传播服务类型字段的设置)

不要为了懒惰而使用!

I don't believe I have come across anything, so let's create it here! Some rules of thumb:

Implicit Conversions

When implicitly converting from A to B where it is not the case that every A can be seen as a B, do it via pimping a toX conversion, or something similar. For example:

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

Don't go mad! Use for very common core library functionality, rather than in every class to pimp something for the sake of it!

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

Implicit Parameters

Use these to either:

  • provide typeclass instances (like scalaz)
  • inject something obvious (like providing an ExecutorService to some worker invocation)
  • as a version of dependency injection (e.g. propagate the setting of service-type fields on instances)

Don't use for laziness' sake!

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文