简体   繁体   中英

Coding with Scala implicits in style

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?

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 (ie 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 (eg 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 (eg 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 (eg trying to do logging in the middle of arithmetic), then rather than make a common class (eg String ) be the implicit val, I wrap it in a marker class (usually with an implicit conversion).

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 (eg propagate the setting of service-type fields on instances)

Don't use for laziness' sake!

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM