Skip to content

Conversation

@eed3si9n
Copy link
Member

@eed3si9n eed3si9n commented Aug 7, 2018

This implements opt-in extension methods, on all types, called tap and pipe:

scala> import scala.util.chaining._
import scala.util.chaining._

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 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

Fixes scala/bug#5324
This is a resend of #6767 without the macro commits.

Fixes scala/bug#5324

This implements an opt-in enrichment for any type called tap and pipe:

```scala
scala> import scala.util.chainingOps._
import scala.util.chainingOps._

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 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
```
@scala-jenkins scala-jenkins added this to the 2.13.0-M5 milestone Aug 7, 2018
@eed3si9n eed3si9n mentioned this pull request Aug 7, 2018
@eed3si9n eed3si9n changed the title Adds AnyTap and AnyPipe Adds tap and pipe Aug 7, 2018
@SethTisue SethTisue self-assigned this Aug 7, 2018
@SethTisue SethTisue added the release-notes worth highlighting in next release notes label Aug 7, 2018
Copy link
Member

@dwijnand dwijnand left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks Eugene.

@joshlemer
Copy link
Contributor

Sorry that this is a bikeshedding comment but it may be nicer to just import scala.util.chaining._, since the other examples of stdlib extension method imports do not use the Ops suffix.

import scala.collection.JavaConverters._
List(1).asJava
import scala.concurrent.duration._
1.second
import scala.util.chaining._
1.pipe(scala.math.abs)

@Ichoran
Copy link
Contributor

Ichoran commented Aug 8, 2018

Can we get a performance warning on these? Something like,

Note: `foo(x).pipe(bar)` may have a small amount of overhead at runtime compared to the
equivalent  `bar(foo(x))` or `{ val temp = foo(x); bar(temp) }`.

and then add inlining instructions to fix the overhead (if inlining will actually fix it)?

I continue to think it is a bad idea to include library features with the semantics of, say, match, but where there is a non-obvious-except-to-experts performance hit.

@SethTisue
Copy link
Member

SethTisue commented Aug 8, 2018

Can we get a performance warning on these?

In the Scaladoc, sure. Or in a linting rule included in a third-party performance-oriented linting ruleset, sure.

As a compiler warning, I feel strongly that no, that really wouldn't be appropriate. I think both Scala itself and the Scala standard library are already teeming with low-level performance gotchas like this — an extra method call, non-obvious boxing, allocating an extra short-lived object, that kind of thing. It's par for the course, not something the compiler constantly needs to call out.

@SethTisue
Copy link
Member

I agree with @joshlemer's naming suggestion, any other opinions on that?

@Ichoran
Copy link
Contributor

Ichoran commented Aug 8, 2018

@SethTisue - I just mean in the scaladoc! As you say, there are lots of performance gotchas already, and many are worse than these.

Still, it's nice to know when two semantically identical things have significantly different performance characteristics. (This isn't even specialized, so it boxes primitives too!)

@Ichoran
Copy link
Contributor

Ichoran commented Aug 8, 2018

I also agree regarding naming the import chaining

@SethTisue
Copy link
Member

I just mean in the scaladoc!

LOL, sorry for the overreaction then :-)

@Ichoran
Copy link
Contributor

Ichoran commented Aug 8, 2018

I should be more careful when using the term "warning" since it has a technical meaning when it applies to compiler messages.

@Ichoran
Copy link
Contributor

Ichoran commented Aug 8, 2018

Is there a reason not to make this specialized? (At least as specialized as Function1 is?)

@dwijnand
Copy link
Member

dwijnand commented Aug 9, 2018

Looks like all the feedback has been addressed.

@dwijnand dwijnand merged commit a95e563 into scala:2.13.x Aug 9, 2018
* @return a new value resulting from applying the given function
* `f` to this value.
*/
def pipe[B](f: A => B): B = f(self)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Other languages name this |>. Would be great to do so here too.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mentioned here: #6767 (comment)

Copy link
Contributor

@Jasper-M Jasper-M Aug 9, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reasoning is that symbolic operators are bad. Though, as a small datapoint: I have no idea what <<=, <+= or <++= mean, I only understand >>= if you tell me its name, I always mix up /: and :\, but |> is just obvious to me, and is better at visually separating steps than pipe. And you have to explicitly import this anyway so having this symbolic alias wouldn't be the worst thing.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally if we can ignore backward compat is get rid of match as keyword and make it a function named match. |> is basically match.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add |> too.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seconding @japgolly . I'm not for symbols in general but this is a very very special ubiquitous one. bash uses something similar, F# has it, PowerShell too.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ScalaWilliam You can add OCaml & ReasonML to that list.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe one of you should send a new pull request or contributor thread and get consensus around it.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As advised by @eed3si9n , here's a PR: #7326

fyi @japgolly @RawToast @hepin1989

@Jasper-M
Copy link
Contributor

Jasper-M commented Aug 9, 2018

According to some quick tests at least the implicit conversion should definitely be made @inline...

@eed3si9n eed3si9n deleted the wip/tap2 branch August 9, 2018 16:44
@xuwei-k
Copy link
Contributor

xuwei-k commented Aug 10, 2018

🤔

resolvers += "scala-pr" at "https://scala-ci.typesafe.com/artifactory/scala-integration/"

scalaVersion := "2.13.0-pre-79d5047"
Welcome to Scala 2.13.0-pre-79d5047 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_181).
Type in expressions for evaluation. Or try :help.

scala> import scala.util.chaining._
import scala.util.chaining._

scala> "a".tap(x => x)
res0: _1.self.type forSome { val _1: scala.util.ChainingOps[String] } = a

scala> 42.tap(x => x)
scala.reflect.internal.Types$TypeError: type mismatch;
 found   : Int
 required: ?#self.type

That entry seems to have slain the compiler.  Shall I replay
your session? I can re-run each line except the last one.
[y/n]

@xuwei-k
Copy link
Contributor

xuwei-k commented Aug 10, 2018

compiler bug? (regression?) 🤔

Welcome to Scala 2.12.6 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_181).
Type in expressions for evaluation. Or try :help.

scala> final class ChainingOps[A](val self: A) extends AnyVal {
     |   def tap[U](f: A => U): self.type = {
     |     f(self)
     |     self
     |   }
     | }
<console>:12: error: type mismatch;
 found   : ChainingOps.this.self.type (with underlying type A)
 required: AnyRef
Note that A is unbounded, which means AnyRef is not a known parent.
Such types can participate in value classes, but instances
cannot appear in singleton types or in reference comparisons.
         def tap[U](f: A => U): self.type = {
                                ^
Welcome to Scala 2.13.0-M4 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_181).
Type in expressions for evaluation. Or try :help.

scala> final class ChainingOps[A](val self: A) extends AnyVal {
     |   def tap[U](f: A => U): self.type = {
     |     f(self)
     |     self
     |   }
     | }
defined class ChainingOps

@dwijnand
Copy link
Member

#7036

@xuwei-k
Copy link
Contributor

xuwei-k commented Aug 10, 2018

compiler bug? (regression?)

scala/bug#10768 ? or another issue?🤔

@dwijnand dwijnand mentioned this pull request Aug 10, 2018
@SethTisue
Copy link
Member

paging @milessabin in aisle literal types

@xuwei-k
Copy link
Contributor

xuwei-k commented Aug 10, 2018

scala/bug#10768 ? or another issue?🤔

Hmmm, maybe another issue 🤔?

$ sbt
> set resolvers += "pr" at "https://scala-ci.typesafe.com/artifactory/scala-pr-validation-snapshots/"
> set scalaVersion := "2.13.0-pre-dae0dd8-SNAPSHOT" // pull req #6452
> console

Welcome to Scala 2.13.0-20180806-115211-dae0dd8 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_181).
Type in expressions for evaluation. Or try :help.

scala> case class Foo[A](self: A) { def bar: self.type = self }
defined class Foo

scala> Foo(1).bar
warning: there was one feature warning; for details, enable `:setting -feature' or `:replay -feature'
scala.reflect.internal.Types$TypeError: type mismatch;
 found   : Int
 required: ?#self.type

That entry seems to have slain the compiler.  Shall I replay
your session? I can re-run each line except the last one.
[y/n]

@milessabin
Copy link
Contributor

I'll take a look.

@milessabin
Copy link
Contributor

Looks like a variant of scala/bug#10792 (which was fixed in #6454).

@xuwei-k
Copy link
Contributor

xuwei-k commented Aug 11, 2018

Looks like a variant of scala/bug#10792 (which was fixed in #6454).

My example ( 79d5047 ) contains #6454 commits but still fail.

@milessabin
Copy link
Contributor

Yeah, it's a variant that's not covered by that fix.

@nafg
Copy link
Contributor

nafg commented Aug 12, 2018

Re familiarity of |>, FWIW Javascript has a proposal to add it: https://github.com/tc39/proposal-pipeline-operator

@Ichoran
Copy link
Contributor

Ichoran commented Aug 14, 2018

For anyone doubting that tap might be used in places where performance matters and thus that a macro solution is not of high value, the first PR I know of that used it is:

https://github.com/scala/scala/pull/7063/files#diff-873c1cee0e89e8df586c53df0c303ceaR44

which could have a substantial performance impact on small byte arrays. Maybe someone wants to benchmark it both ways; it's more hassle to benchmark the difference than it's worth to avoid writing it a way that's assuredly fast. (One could counter: but nextBytes isn't all that fast anyway, and this is true, but in a widely-used library it's generally better to "prematurely" optimize to the extent consistent with what's already there. Un-optimized, which is really easy to test by copying the tap code it delivers a ~30% slowdown on an array of four bytes.)

@milessabin
Copy link
Contributor

I've just fixed the issue reported by @xuwei-k. With #7070 the result type of his example is now revealed to be a bit of a monstrosity,

scala> case class Foo[A](self: A) { def bar: self.type = self }
defined class Foo

scala> val res = Foo(1).bar
                        ^
       warning: inferred existential type _1.self.type forSome { val _1: Foo[Int] }, which cannot be expressed by wildcards, should be enabled
       by making the implicit value scala.language.existentials visible.
       ----
       This can be achieved by adding the import clause 'import scala.language.existentials'
       or by setting the compiler option -language:existentials.
       See the Scaladoc for value scala.language.existentials for a discussion
       why the feature should be explicitly enabled.
res: _1.self.type forSome { val _1: Foo[Int] } = 1

self.type is almost certainly not what you want, even now that it doesn't crash the compiler.

bigwheel added a commit to bigwheel/tapandpipe that referenced this pull request Aug 21, 2018
@SethTisue SethTisue changed the title Adds tap and pipe Adds tap and pipe methods (available via import scala.util.chaining._) Aug 22, 2018
@SethTisue
Copy link
Member

renewed discussion at #7326 of whether to have |> (instead of, or in addition to, pipe)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

release-notes worth highlighting in next release notes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Piping methods on Any similar to scalaz "|>", Ruby "tap"