case class RoutePts(points: Vector[RoutePt]):
import points.*
export points.*
def alongTrackShift(delt: Scalar) = points.alongTrackShift(delt)
extension(points: Vector[RoutePt])
def alongTrackShift(delt: Scalar) = if delt == 0 then points else
points.map(_.alongTrackShift(delt))
Shouldn’t the export statement cause the alongTrackShift method defined in the extension to be usable for class RoutePts? That’s what I thought, but I found that I had to add the first definition of alongTrackShift above in the RoutePts class to make it available. What am I missing?
So the methods provided by extensions are “second-class” because they are not recognized by export.
If it is technically feasible, I would like to see extension methods become first class, and I would also like to see the addition of vals allowed in extensions.
I realize that would be a big change because it would apply to each instance of a class rather than the class definition in general, but it would be very useful for me. Let me explain why.
I have several classes that are basically wrappers for a Vector of a particular type. Here are a couple of examples:
case class RoutePts(points: Vector[RoutePt]):
case class Tracks(tracks: Vector[Track]):
I am constructing flyable aircraft trajectories by splicing together various segment types such as climb, cruise, descent, and various maneuver types. Sometimes I need just the raw Vector, and other times I need the enclosing wrapper class. As it stands now, I have to go back and forth between the two. It’s not hard to do, but it would be cleaner and more elegant if I did not have to even define the wrapper class and could just use a type alias instead:
type RoutePts = Vector[RoutePt]
type Tracks = Vector[Track]
Then I would use extensions on Vector[RoutePt] and Vector[Track] to do the rest, and there would be no distinction between the Vector and the wrapper class.
Type aliases with extensions go a long way, but they don’t allow the storage of vals. Sometimes I have costly vals to compute, and I want to store them as lazy vals rather than recompute them each time I need them for a particular instance.
I was using implicit classes successfully for years, but as you may recall (since you identified it), I had a nasty bug recently involving cyclic implicit references. Needless to say, I’d rather avoid that kind of problem in the future.
It seems to me that it should be possible to give extensions the full capability of implicit classes, which means being able to store vals.
That’s one of the reasons why Scala 3 removed implicit classes; they were too powerful and easy to abuse.
The intent of extension methods is not to “avoid the wrapper” but to add a method to a class that you can’t modify (like from a library). Extensions were made to clarify the intended use case.
Your use case falls under a normal class. Just use the wrapper. Nothing wrong with it!
Some people tend to take this “avoid the wrapper” obsession way too far. The Scala 3 version of the Red Book (FP in Scala 2nd edition) heavily suffers from this. The code is difficult to read and understand, with a lot of indirection, and would have greatly benefited from using just normal classes. Apparently there is some small performance benefit but I don’t think it’s worth it.
Yup, givens improve on that by being a lot more restrictive.
That’s the general idea in Scala 3 implicit replacements: clarify intended use case, reduce power. You shouldn’t “feel bad” for using the new idioms. It takes a while to let go of Scala 2 habits.
Often I find this to be only cosmetically elegant in a superficial way. Eventually you’ll want more functionality, and you’ll be reaching for a normal class. I was just writing some code like that today, and had to make that decision:
And this is wonderfully elegant in my opinion. Givens, case classes with lazy vals, type aliases, all play along very nicely, each doing their specific duty that they are suited for. Scala 3 does a good job of clarifying their intent, and preventing me from otherwise digging myself into the implicit class hole.
Sticking purely to extension methods definitely starts to lose its elegance once it gets a little beefy (take a look at the Red Book’s code).
Thanks for that helpful explanation. One thing surprised me though:
That’s one of the reasons why Scala 3 removed implicit classes; they were too powerful and easy to abuse.
That is news to me, given that I am still using implicit classes with Scala 3, and I have heard nothing about their being “removed” or even deprecated. Did I miss something?
Regardless, I guess I should consider replacing some of them with extensions.
In Scala 3, implicit classes are still supported for compatibility reasons but the recommended way to achieve the same result is to use extension methods.
Scala 3.4 added warnings for a lot of historical Scala 2 constructs, as per Scala 3.4.0 and 3.3.3 LTS released! | The Scala Programming Language (Naturally, the warnings are intended to be easily silencable in crossbuilt codebases where continued use of Scala 2 constructs is necessary.)
Perhaps implicit class was simply overlooked for inclusion in that?
I am converting my implicit classes to extensions, but I am having some issues. Extensions are a nice feature, but I’m wondering why certain simplifications are not allowed.
I mentioned earlier that I would like to see extensions made available with exports. Now I have what I think is a more modest request, and I am not sure why it is not already available. I would just like to be able to use basic imports in the extension functions.
For example, if I have
extension(points: Vector[RoutePt])
I would like to add
import points.*
to get access to all the basic Vector methods, including head, tail, last, length, isEmpty, etc. But the compiler does not allow that. Is there some legitimate reason for that? And can this feature be added in a future version? Why should I have to write points.head when I can just write head?
Thanks for letting me know about that previous discussion. I just added the following.
My main use case for extensions is to add methods to collection wrapper classes such as
case class Tracks(tracks: Vector[Track]):
...
extension(tracks: Vector[Track])
...
Unless there is some technical reason not to do so, I say just make the import automatic (as in Kotlin?) and be done with it. That would be consistent with adding a method within the class constructor itself if you had access to the source code, so I don’t see how that could be a problem.
An explicit import declaration as others have suggested would be the next best thing, but I don’t see why it should even be needed since it is not needed in the class constructor itself.
I’m assuming you want to achieve a similar effect to widening an inherited OO interface and its implementation with extra methods, but have to work with objects and delegation instead - and that is what exports offer.
That’s what’s going on with Copier in the example here: Export Clauses, playing a similar role with its scanUnit and printUnit as Tracks could with its tracks instance.
Copier.status plays a similar role to your originally intended use of extension methods, I suspect. In the example, it happens to delegate to two similar methods on the inside, but there’s nothing stopping you from adding completely new methods unrelated to the delegatee’s interface.