Scala 2.13’s ‘pipe’ and ‘tap’ chaining operations

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.

More information

For more information, see the pipe/tap pull request, the ChainingOps class scaladoc, and the Scala 2.13.0 release notes.