-
Notifications
You must be signed in to change notification settings - Fork 3.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Adds AnyTap and AnyPipe #6767
Adds AnyTap and AnyPipe #6767
Conversation
Fixes scala/bug#5324 This implements two implicit classes `AnyTap` and `AnyPipe`, which enrich every type `A` to inject `tap` and `pipe` method respectively. ```scala scala> val xs = List(1, 2, 3).tap(ys => println("debug " + ys.toString)) debug List(1, 2, 3) xs: List[Int] = List(1, 2, 3) scala> val s = List(1, 2, 3).pipe(xs => xs.mkString(",")) s: String = 1,2,3 ```
src/library/scala/Predef.scala
Outdated
* }}} | ||
* | ||
* @group implicit-classes-any */ | ||
implicit final class AnyTap[A](private val self: A) extends AnyVal { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why have two separate classes for these?
Also, any chance of turning these into a macro? That would be a huge value-add because you wouldn't need to create the closure etc. in order to use this workflow.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- I wasn't sure if someone wants to unimport either. I'm happy to bundle them into one class like
AnyOps
. - Besides things like
String_+
do we have precedence of using macros in standard library? I'm not sure if Scala team wants to add a macro responsibility at this juncture.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
String_+
isn't a macro; it's a synthetic method that the compiler injects into String
.
That said, there are quite a few macros in stdlib, such as the f""
string interpolator, reify
, and quasiquotes. Their implementations are linked up in FastTrack
, which is presumably how you'd want to link these in.
test/junit/scala/PredefTest.scala
Outdated
def testAnyTap: Unit = { | ||
var x: Int = 0 | ||
val result = List(1, 2, 3) | ||
.tap(xs => x = xs(0)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not a big deal, but isn't it a little non-idiomatic to index into a list instead of taking the head or something?
What's the rationale for this to be in There should be much stronger resistance against putting stuff in |
LGTM! Having these in the standard library, even not in Predef, is great. Unofficially ✅. |
See scala/bug#5324 for the rationale. Whether this change should or should not be part of Scala Library or |
The number of tests that need an update (even if it's a minor one) is a hint that adding extensions to Other than that, +1, it's handy. |
src/library/scala/Predef.scala
Outdated
* @tparam U the result type of the function `f`. | ||
* @return the original value `self`. | ||
*/ | ||
def tap[U](f: A => U): A = { f(self); self } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- Could the return type be
self.type
? - Why the
U
type parameter, not justA => Unit
(orself.type => Unit
)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
U
avoids value discarding warnings.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is that desirable? The value (if any) is discarded.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMO yes, as the function is specified as a side-effect.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there an advantage over having this polymorphic version in U
over A => Any
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hot take. Martin likes genericity. The parametric function A => U
is more beautiful (to me) than A => Any
though the purists would probably sneer at tap
as being "lawless."
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A => U
implies - to me - a more parametric, prettier relation than A => Any
implies - but the less beautiful thing is actually what you're getting, so I'd argue that should be reflected in the signature.
I don't know who the purists are, why they would consider the tap method lawless, or what lawfulness has to do with anything here in the first place.
I'm also not sure what hot takes means here, and at this point I'm too afraid to ask.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In journalism, a hot take is a "piece of deliberately provocative commentary that is based almost entirely on shallow moralizing" in response to a news story, "usually written on tight deadlines with little research or reporting, and even less thought".
sounds about right.
By laws, I meant something like Monoid law that can limit the properties of the types. Sorry if I came off as cryptic.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right. But tap
is lawful (for all types A
, for all x: A
, for all f: A => Any
, x.tap(f) == x
). And I still don't know what lawfulness has to do with the entire thing, and I have the feeling it's a red herring and doesn't have to do with anything.
But maybe we can ask the purists who would sneer. Who are they, so we can ask?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have the feeling it's a red herring and doesn't have to do with anything.
You could be right, but tap
is also related to K-combinator A => B => A
, as seen in Kestrels. K can be seen as Const
, which does form a Functor. In all these cases, they probably can be replaced withAny
(after all it's ignored), but I think [U]
helps in thinking of tap
that it has an explicit phantom type.
IMO the example of |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 to the macros.
I'm not opposed to these in the standard library, but I agree with @sjrd that they probably don't belong in Predef
. I'd rather see ensuring
/requiring
wind up as explicit imports than be used as justification to adding more extension methods on Any
.
src/library/scala/Predef.scala
Outdated
* }}} | ||
* | ||
* @group implicit-classes-any */ | ||
implicit final class AnyTap[A](private val self: A) extends AnyVal { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
String_+
isn't a macro; it's a synthetic method that the compiler injects into String
.
That said, there are quite a few macros in stdlib, such as the f""
string interpolator, reify
, and quasiquotes. Their implementations are linked up in FastTrack
, which is presumably how you'd want to link these in.
I worked on |
Yeah, better not use a method already on the type. I suggest as an alternative: val s = List(1, 2, 3).pipe(xs => scala.util.Random.shuffle(xs)) |
? |
Sure. I was just conservatively being verbose 🙂 |
@dwijnand The problem with |
|
scala> val times6 = (_: Int) * 6
times6: Int => Int = $$Lambda$1727/1479800269@10fbbdb
scala> (1 + 2 + 3).pipe(times6)
res0: Int = 36
scala> (1 - 2 - 3).pipe(times6).pipe(scala.math.abs)
res1: Int = 24 |
figuring out where to import it from or how to modify predef is as much work as writing it locally, i think it should be in predef |
@OlegYch for something bundled with scala-library, I think I think newbies could |
It looks like as I was writing this you were making them macros, so it's all awesome! 🥇 |
@Ichoran yea. I wasn't confident about making them macros, so I made the import changes first :) |
f(self) | ||
self | ||
} | ||
def tap[U](f: A => U): A = macro ??? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not sure how to return self.type
here using macro. If I just change the type to self.type
I get:
util-chainingops.scala:12: error: type mismatch;
found : x$macro$1.type (with underlying type List[Int])
required: _1.self.type where val _1: scala.util.ChainingOps[List[Int]]
.tap(xs => x = xs.head)
^
one error found
When the macro is expanded ChainingOps[List[Int]]
goes away, so.. can I still get _1.self.type
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah can't use self.type when using an enriched method.
You could do it if you did it as a synthetic method, like String.+
|
||
/** Adds chaining methods `tap` and `pipe` to every type. | ||
*/ | ||
final class ChainingOps[A](val self: A) extends AnyVal { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
private val self
. Otherwise every type gets a self
member!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. I had to open it up for self.type
. Another reason not do it I guess.
Implemented as a macro I'm guessing this would probably benefit from having a few tests for the different code styles. I'm not sure which are overkill, but I'm thinking of variations like infix |
This shouldn't be implemented as a macro to be usable with Dotty |
Not to mention it's super easy for an optimizer to inline and get rid of the overhead anyway. Don't use macros to improve performance when a simple inliner can do it. |
@allanrenucci @sjrd ok. I am indifferent about they being macros. You and @Ichoran @hrhino should hash it out. |
nah, I'm not going to hash it out with anyone. I'm okay with it being a normal method, too. |
|
@allanrenucci out of curiosity, does |
No, it doesn't Starting dotty REPL...
scala> f"Hello"
java.lang.NoSuchMethodError: scala.StringContext.f(Lscala/collection/Seq;)Ljava/lang/String; |
See also the "Intrinsfy raw and s String interpolators" patch (makes the compiler replace those calls with an equivalent, but more efficient, implementations). |
@sjrd - Can you please fix the optimizer to do this with high reliability, then? Right now, it falls completely flat. With tests of
I get results like
Gist: https://gist.github.com/Ichoran/4ba6460aadd93d7bd109bb712f75007f Having a secret performance gotcha feature, where you use |
@Ichoran "a secret gotcha feature" here is the lambda, and they are already given by default |
@OlegYch - It isn't a gotcha if the macro inlines the expression, eliminating the lambda. |
By that argument, we should turn Your test with The test with
No, I cannot, because I'm not familiar enough with the scalac/JVM optimizer to a) make even the start of a guess about what's going wrong and b) make the required changes in a reasonable time frame. I can tell you that it is straightforward for the Scala.js optimizer, though. It generates the same code up to renaming and 1 more temporary variable for the pipe example: $c_Lbench_Bench.prototype.zsum$1__p1__AI__I = (function(xs) {
var s = 0;
var i = 0;
while ((i < xs.u.length)) {
var x = xs.get(i);
s = ((s + ((2 + (($imul(x, x) + x) | 0)) | 0)) | 0);
i = ((1 + i) | 0)
};
return i
});
$c_Lbench_Bench.prototype.zsumPipe$1__p1__AI__I = (function(xs) {
var s = 0;
var i = 0;
while ((i < xs.u.length)) {
var jsx$1 = s;
var a = xs.get(i);
s = ((jsx$1 + ((2 + (($imul(a, a) + a) | 0)) | 0)) | 0);
i = ((1 + i) | 0)
};
return i
}); It doesn't even need the explicit |
@sjrd - It sounds to me like the your original characterization of fixing the optimizer was misleading, then. Maybe you meant, "If the optimizer can handle this case robustly, then all such code is sped up, which is much better than having to write special macros; thus, it is worth the time investment." To which I would respond: "Does anyone who can have time to do it now? If not, what is the harm in adding a macro that can be reverted to a plain extension method once the optimizers all consistently work on it?" Also, my use of the "non-local" return was of course to illustrate a point, which is that conceptually there's no reason that the return should be non-local. That it is is an unfortunate implementation detail, and manual splicing of the tree in a macro prevents it from being non-local. If it is in a Finally,
With data structures like I'd love for the optimizer to robustly handle this. But I don't think introducing a feature where the lore is "Don't use that feature--it might kill your performance" is a wise move. (Also: the lore used to be "don't use the optimizer, it does nothing"; this isn't true any more, but I'm not sure we can count on everyone turning it on on the JVM after so many years of it not helping.) |
It would be lovely if the strong-minded participants here negotiated with one another and found path forward so that this doesn't miss the release.... 🚢 |
Well, I think it's better to not have it at all than to have a version without macros. Anyone can trivially add these things, and possibly do so in a better way (e.g. with specialization) than the non-macro version. The macro version is not trivial, and is a substantial value-add. As an alternative, we could tweak things to allow calling |
For |
I've often wanted a "newbie" mode; maybe also a "deluxe" mode with various conveniences which may not be efficient, but are nice to have for REPL, scripting or for composing things run just once in main. Maybe that's the definition of a "unified scripting experience" that would also replace |
I disagree. Scala has a long and bloody history of library/tooling authors introducing symbolic operators, the Scala Days talks mercilessly ridiculing the unreadability of the operators, and maintainers disappearing in the dark or someone else quickly covering them up. See This doesn't mean that symbolic operators are always bad, but I'd really caution against anything that doesn't have clear meaning understood among the target user audience. I see |
with this being hidden under import, i don't see much use |
@OlegYch - Inline would be fine if the optimizer actually removed all the overhead, but it doesn't, at least not when I tested it. |
my take on where we stand, here:
otoh, these methods have been requested a million billion trillion times over the last 10+ years in every Scala venue under the sun. I don't think everyone rolling their own slightly different variant is a good solution and I don't think having a little bit of performance overhead, which will likely go away in some future Scala, is a show-stopper. if that overhead's a back-breaker for somebody, they can just not use it. so, I would support merging a non-macro-based version of this. closing, but I invite @eed3si9n to submit a new, non-macro-based version. |
Fixes scala/bug#5324
This implements an opt-in enrichment for any type called
tap
andpipe
: