Simplifying wrapper classes

I have several wrapper classes that contain basic forwarding methods like this:

case class Tracks(tracks: Vector[Track] = Vector()):

  def isEmpty = tracks.isEmpty
  def nonEmpty = tracks.nonEmpty
  def length = tracks.length
  def indices = tracks.indices
  def head = tracks.head
  def last = tracks.last

  def apply(i: Int) = tracks(i)
  def take(n: Int) = copy(tracks.take(n))
  def drop(n: Int) = copy(tracks.drop(n))
  def takeRight(n: Int) = copy(tracks.takeRight(n))
  def dropRight(n: Int) = copy(tracks.dropRight(n))
  def reverse = copy(tracks.reverse)

I was wondering if there is a way to get all Vector methods forwarded without this kind of boilerplate. Any ideas? Thanks.

Short answer: yes; implementing a custom collection.

Long answer: Not sure how easy / complex it is, never done it.
Good luck and hopefully someone with more experience on this topic will drop by.

Sorry, I don’t understand where the method forwarding is?

Sorry, did I use the wrong terminology? thought

def isEmpty = tracks.isEmpty

was “forwarding” the isEmpty method to tracks.

Ah, I thought you wanted some automatic forwarding to avoid boilerplate.

In Scala 2, there is nothing, and it seems like pretty questionable design to me anyway. If a Tracks is just a Vector[Track] then I wouldn’t have Tracks at all, or maybe type Tracks = Vector[Track].

And if Tracks is something more than just that — if it has other methods — then requiring users to add .tracks to get at the methods on Vector seems just fine to me. Desirable, even.

However, note that Scala 3 adds “exports”: https://dotty.epfl.ch/docs/reference/other-new-features/export.html … though honestly, I fear it will be abused at the expense of clarity.

3 Likes

Yes, that is what I want – to eliminate boilerplate – because I am an obsessive minimalist, I guess.

Yes, Tracks has other methods that I did not show.

I just did a quick test of export, and it seems to do exactly what I asked for if I just add

export tracks._

Nice! I’ll try not to abuse it.

By the way, one reason this feature is significant to me is that it allows me to essentially replace all occurrences of Vector[Track] with Tracks in my code without making any other changes. That not only eliminates work, but it also reduces the chances of an error.

Thanks for the tip!

After working with it a bit, I now realize that “export” does not quite do exactly what I wanted, but it’s still a step forward.

For basic Vector methods like “isEmpty”, “length”, etc., it does exactly what I want. But for methods like “take”, “drop”, etc., it returns a Vector[Track] rather than a Tracks instance. That should have been obvious, I guess.

If we allow for extremely questionable design, there’s implicit conversions. Just mentioning it for the sake of completeness, don’t try this at home. :scream:

In code blocks where the member is accessed often, it can be assigned to a dedicated val or imported to avoid repetitive use of the accessor.

1 Like

You could provide a method for “plunging” Vector-manipulating functions into the instance.

case class Tracks(tracks: Vector[Track] = Vector()) {
  def ~>(f: Vector[Track] => Vector[Track]): Tracks = 
    Tracks(f(tracks))
}

tracks ~> (_.drop(1))

It’s not super elegant, but depending on your usage patterns it may or may not be an improvement.

1 Like

If your other methods do not require additional fields, i.e. the Vector[Track] is the only thing stored in that class, you could maybe use extension methods instead? (as you can use export, I assume you’re using dotty)

extension (tracks: Vector[Tracks]) {
  def yourMethod1 = ...
  def yourMethod2 = ...
}

So instead of wrapping the Vector, you make your methods available on all Vectors with the right element type, which solves the problem of Vector methods returning Vector.

2 Likes

Why is it questionable? Isn’t it the foundation for the decorator pattern?

The decorator pattern usually is about extending the implementation of some well-defined, closed API (Component in the Wikipedia page), rather than just exposing any method available on a wrapped object. And for this use case Scala provides the stackable traits feature/pattern.

I thought your suggestion to use “extension” might be the answer. However, I tried it and was disappointed.

The problem is that the namespace is apparently shared with other Vector extensions. So if I have a Tracks = Vector[Track] and Things = Vector[Thing] (made up name), I can’t use the same method name in the two of them. I did not expect that, since I thought that Vector[Track] and Vector[Thing] were two different types. I could put them inside companion objects, but then I have to use imports or qualified names, which is not as elegant.

Also, I wanted a toString method, and for some reason the compiler wouldn’t let me add that.

Extension seems like a nice idea, but I had other problems with it as well when I tried to convert some some of my implicit classes to extensions. I certainly hope implicit classes are not going away unless or until extension can do everything they can do.

Indeed:

scala> class A; class B
// defined class A
// defined class B

scala> extension (as: Vector[A]) def foo = 3                                                                            
def foo(as: Vector[A]): Int

scala> extension (bs: Vector[B]) def foo = "bar"                                                                        
def foo(bs: Vector[B]): String

scala> Vector(new B).foo
val res0: String = bar

scala> Vector(new A).foo                                                                                                
1 |Vector(new A).foo
  |^^^^^^^^^^^^^^^^^
  |value foo is not a member of Vector[A].
  |An extension method was tried, but could not be fully constructed:
  |
  |    foo(Vector.apply[A]([new A() : A]:A*))

I’m curious if this is considered to be working-as-designed, or whether it could be changed.

I’m curious too. I was hoping to see an answer here.

Although “extension” has problems as noted above, I am finding that implicit classes can be used to solve this problem fairly nicely. (But it requires the use of the “implicit” keyword, which might freak some people out!)

That leaves me wondering what the relationship is between extension and implicit classes. Although extension apparently cannot be used to add data fields, it seems to serve the same purpose as implicit classes otherwise.

Is extension intended to eventually replace implicit classes? If so, it still needs some work.

implicit classes serve a single purpose, adding extension methods.
The extension keyword was added in Scala 3 to provide proper first-class support to that use case.

That’s what I thought, but I would still like to know if “extension” is ultimately intended to replace implicit classes. If the answer is yes, then it needs more work.

Here is another question regarding both extension and implicit classes. The example given for extension methods is

case class Circle(x: Double, y: Double, radius: Double)

extension (c: Circle)
  def circumference: Double = c.radius * math.Pi * 2

Why does the Circle that is being extended even need a name? Why can’t it just be

extension (Circle)
  def circumference: Double = radius * math.Pi * 2

with an implied name of “this”, as usual? That would make the code under the extension identical to code in the original class, which would promote consistency.