The Scala 2.13 collections’ class updates introduced two new “chaining operations” named pipe
and tap
. Here’s a quick look at how they work, plus a little extra fun at the end.
pipe
pipe
works like a Unix pipe, passing values to functions. For example, given this setup:
import scala.util.chaining._
import scala.language.implicitConversions
def plus1(i: Int) = i + 1
def double(i: Int) = i * 2
def square(i: Int) = i * i // we’ll use this one later
the result of this “pipeline” is the integer value 4
:
val x = 1.pipe(plus1).pipe(double)
That might be a little easier to read if you think of the code as being like a Unix pipeline:
val x = echo 1 | plus1 | double
(More on this shortly.)
This code works because the value 1
is piped into plus1
, then the output of plus1
is piped into the double
method.
This is nice because just like you can chain collections methods together, this gives you a convenient way to chain together pure functions on an initial value. pipe
may not seem too exciting in a simple example like this, which can be written this way as usual:
val x = double(plus1(1))
but it may be a nice approach when you’re chaining a larger series of functions together.
tap
Similarly, tap
reminds me of the Unix tee
command. tap
does two things:
- Returns a value
- Lets you perform a side effect with the value, such as printing/logging it
Here’s an example of using pipe
twice, then a tap
, and one more pipe
at the end:
val x = 1.pipe(plus1) .pipe(double) .tap(res => println(s"DEBUG: x = $res")) .pipe(double)
Assuming the import statements and methods shown above, this is what that looks like in the Scala REPL:
scala> val x = 1.pipe(plus1).pipe(double).tap(res => println(s"DEBUG: x = $res")).pipe(double) DEBUG: x = 4 val x: Int = 8
As shown, at the time of the tap
, x
is assigned the value 4
, and the debug information is also printed. Then I use pipe
again to make the final value 8
.
A little fun: From pipe
to |
Just experimenting and exploring a little here ... if you want to have a little fun and make my pipe
example a little more readable, this is one of those rare times when a symbol might be easier to read than a method name. For instance, if you want to write code like this in Dotty:
val x = 1 | plus1 | double | square
you can currently define a |
a Scala 3 extension method like this:
def [A,B](a: A) |(f: (A) => B): B = a.pipe(f)
After you do that, your code can look like this:
scala> val x = 1 | plus1 | double | square val x: Int = 16
Visually, that’s an interesting alternative to this:
val x = square(double(plus1(1)))
Note: Other languages and libraries use the symbol
|>
to mean “pipe forward.”
In Scala 2 you can write something similar like this:
import scala.language.implicitConversions
implicit class Piper[A](val a: A) {
import scala.util.chaining._
implicit def |>[B](f: (A) => B): B = a.pipe(f)
}
and use it like this:
scala> 1 |> plus1 |> double res0: Int = 4
As shown in the next section, you can also write a method like |
(or |>
) from scratch, but for my purposes I just piggyback’d on Scala 2.13’s pipe
.
Other ways to write |>
If you’re curious about ways to write |>
from scratch, I found these approaches on this Reddit thread:
// approach 1
implicit class AnyEx[T](val v: T) extends AnyVal {
def |>[U](f: T ⇒ U): U = f(v)
}
// approach 2
implicit class AnyEx[+A, -B](f: B=>A) {
def |>:(b: B): A = f(b)
def |>:[C](g: C => B): C => A = (c: C) => f(g(c))
}
I’m always fascinated by people bringing tools and functionality to Scala — it shows a benefit of learning multiple languages. In this case their discussion was about |>
in F#. It looks like pipe-forward is also available in Elm.
this post is sponsored by my books: | |||
#1 New Release |
FP Best Seller |
Learn Scala 3 |
Learn FP Fast |
More information
For more information, see the pipe/tap pull request, the ChainingOps class scaladoc, and the Scala 2.13.0 release notes.