Value classes introduce unwanted public methods

2019-04-04 02:10发布

问题:

Looking at some scala-docs of my libraries, it appeared to me that there is some unwanted noise from value classes. For example:

implicit class RichInt(val i: Int) extends AnyVal {
  def squared = i * i
}

This introduces an unwanted symbol i:

4.i   // arghh....

That stuff appears both in the scala docs and in the IDE auto completion which is really not good.

So... any ideas of how to mitigate this problem? I mean you can use RichInt(val self: Int) but that doesn't make it any better (4.self, wth?)


EDIT:

In the following example, does the compiler erase the intermediate object, or not?

import language.implicitConversions

object Definition {
  trait IntOps extends Any { def squared: Int }
  implicit private class IntOpsImpl(val i: Int) extends AnyVal with IntOps {
    def squared = i * i
  }
  implicit def IntOps(i: Int): IntOps = new IntOpsImpl(i)  // optimised or not?
}

object Application {
  import Definition._
  // 4.i  -- forbidden
  4.squared
}

回答1:

In Scala 2.11 you can make the val private, which fixes this issue:

implicit class RichInt(private val i: Int) extends AnyVal {
  def squared = i * i
}


回答2:

It does introduce noise (note: in 2.10, in 2.11 and beyond you just declare the val private). You don't always want to. But that's the way it is for now.

You can't get around the problem by following the private-value-class pattern because the compiler can't actually see that it's a value class at the end of it, so it goes through the generic route. Here's the bytecode:

   12: invokevirtual #24;
          //Method Definition$.IntOps:(I)LDefinition$IntOps;
   15: invokeinterface #30,  1;
          //InterfaceMethod Definition$IntOps.squared:()I

See how the first one returns a copy of the class Definition$IntOps? It's boxed.

But these two patterns work, sort of:

(1) Common name pattern.

implicit class RichInt(val repr: Int) extends AnyVal { ... }
implicit class RichInt(val underlying: Int) extends AnyVal { ... }

Use one of these. Adding i as a method is annoying. Adding underlying when there is nothing underlying is not nearly so bad--you'll only hit it if you're trying to get the underlying value anyway. And if you keep using the same name over and over:

implicit class RicherInt(val repr: Int) extends AnyVal { def sq = repr * repr }
implicit class RichestInt(val repr: Int) extends AnyVal { def cu = repr * repr * repr }

scala> scala> 3.cu
res5: Int = 27

scala> 3.repr
<console>:10: error: type mismatch;
 found   : Int(3)
 required: ?{def repr: ?}
Note that implicit conversions are not applicable because they are ambiguous:
 both method RicherInt of type (repr: Int)RicherInt
 and method RichestInt of type (repr: Int)RichestInt

the name collision sorta takes care of your problem anyway. If you really want to, you can create an empty value class that exists only to collide with repr.

(2) Explicit implicit pattern

Sometimes you internally want your value to be named something shorter or more mnemonic than repr or underlying without making it available on the original type. One option is to create a forwarding implicit like so:

class IntWithPowers(val i: Int) extends AnyVal {
  def sq = i*i
  def cu = i*i*i 
}
implicit class EnableIntPowers(val repr: Int) extends AnyVal { 
  def pow = new IntWithPowers(repr)
}

Now you have to call 3.pow.sq instead of 3.sq--which may be a good way to carve up your namespace!--and you don't have to worry about the namespace pollution beyond the original repr.



回答3:

Perhaps the problem is the heterogeneous scenarios for which value classes were plotted. From the SIP:

• Inlined implicit wrappers. Methods on those wrappers would be translated to extension methods.

• New numeric classes, such as unsigned ints. There would no longer need to be a boxing overhead for such classes. So this is similar to value classes in .NET.

• Classes representing units of measure. Again, no boxing overhead would be incurred for these classes.

I think there is a difference between the first and the last two. In the first case, the value class itself should be transparent. You wouldn't expect anywhere a type RichInt, but you only really operate on Int. In the second case, e.g. 4.meters, I understand that getting the actual "value" makes sense, hence requiring a val is ok.

This split is again reflected in the definition of a value class:

 1. C must have exactly one parameter, which is marked with val and which has public accessibility.

...

 7. C must be ephemeral.

The latter meaning it has no other fields etc., contradicting No. 1.

With

class C(val u: U) extends AnyVal

the only ever place in the SIP where u is used, is in example implementations (e.g. def extension$plus($this: Meter, other: Meter) = new Meter($this.underlying + other.underlying)); and then in intermediate representations, only to be erased again finally:

new C(e).u ⇒ e

The intermediate representation being accessible for synthetic methods IMO is something that could also be done by the compiler, but should not be visible in the user written code. (I.e., you can use a val if you want to access the peer, but don't have to).



回答4:

A possibility is to use a name that is shadowed:

implicit class IntOps(val toInt: Int) extends AnyVal {
  def squared = toInt * toInt
}

Or

implicit class IntOps(val toInt: Int) extends AnyVal { ops =>
  import ops.{toInt => value}
  def squared = value * value
}

This would still end up in the scala-docs, but at least calling 4.toInt is neither confusing, no actually triggering IntOps.



回答5:

I'm not sure it's "unwanted noise" as I think you will almost always need to access the underlying values when using your RichInt. Consider this:

// writing ${r} we use a RichInt where an Int is required
scala> def squareMe(r: RichInt) = s"${r} squared is ${r.squared}"
squareMe: (r: RichInt)String

// results are not what we hoped, we wanted "2", not "RichInt@2"
scala> squareMe(2)
res1: String = RichInt@2 squared is 4

// we actually need to access the underlying i
scala> def squareMeRight(r: RichInt) = s"${r.i} squared is ${r.squared}"
squareMe: (r: RichInt)String

Also, if you had a method that adds two RichInt you would need again to access the underlying value:

scala> implicit class ImplRichInt(val i: Int) extends AnyVal {
     |   def Add(that: ImplRichInt) = new ImplRichInt(i + that) // nope...
     | }
<console>:12: error: overloaded method value + with alternatives:
  (x: Int)Int <and>
  (x: Char)Int <and>
  (x: Short)Int <and>
  (x: Byte)Int
 cannot be applied to (ImplRichInt)
         def Add(that: ImplRichInt) = new ImplRichInt(i + that)
                                                        ^

scala> implicit class ImplRichInt(val i: Int) extends AnyVal {
     |   def Add(that: ImplRichInt) = new ImplRichInt(i + that.i)
     | }
defined class ImplRichInt

scala> 2.Add(4)
res7: ImplRichInt = ImplRichInt@6