Value classes introduce unwanted public methods
Asked Answered
H

5

15

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
}
Hakodate answered 30/7, 2013 at 10:20 Comment(2)
Is was going to say make it private or lose the qualifier, but apparently that's not allowed for value classes. So I guess the answer is: you can't.Kalli
Or even better: 4.i.i.i.i.i.iPlutus
S
5

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
}
Schoonmaker answered 30/6, 2015 at 8:54 Comment(0)
A
5

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.

Adlib answered 30/7, 2013 at 18:57 Comment(1)
Good points. Regarding (2), see my short answer for an import based renaming.Hakodate
S
5

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
}
Schoonmaker answered 30/6, 2015 at 8:54 Comment(0)
H
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).

Hakodate answered 30/7, 2013 at 16:37 Comment(0)
H
2

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.

Hakodate answered 30/7, 2013 at 17:48 Comment(0)
P
0

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
Plumate answered 30/7, 2013 at 15:3 Comment(2)
It kind of shows the schizophrenic nature of value classes. On the one hand the idea is allow things like unit tagging (your first example). In that case you wouldn't necessarily think about implicit classes. On the other hand, it's the mechanism to get overhead free extension methods. In that case, you want the class to be transparent, never returning the RichInt type, and so requiring a val doesn't make sense.Hakodate
@Hakodate I think I agree: as far as I understand them, value classes are not meant to encapsulate or hide the fact that they are a thin layer over the value type they wrap. Implicit classes, on the other hand, are meant to allow the compiler to swap one type for another (and not to care about the underlying type). Implicit value classes, by mixing these two properties, do tend to look a little awkward...Plumate

© 2022 - 2024 — McMap. All rights reserved.