Add .tap to the standard library?

As a Scala programmer, I often use this following pattern:

extension [A](a: A)
  def tap: A = {
    println(a); a
  }

  def tap(f: A => Unit): A = {
    f(a); a
  }

It’s very useful for debugging, especially with long chains of operations such as:

getResults
.groupBy(...)
.filter(...)
.map(...)
.foreach(...)

Being able to simply insert a .tap into a chain of operations is very quick and clean

getResults
.groupBy(...)
.filter(...)
.tap
.map(...)
.foreach(...)

as opposed to having to assign part of the expression to a val, like so:

val part1 = getResults
.groupBy(...)
.filter(...)

println(part1)

part1
.map(...)
.foreach(...)

Basically it allows you to see the value of any expression in your program at any point, without having to do a lot of refactoring.

I find myself having to redefine this for each new project but also since it’s for debugging I don’t necessarily want to commit it either; maybe the standard library is the right place? I think many others use this pattern also.

And you might also say, “Shouldn’t you be using a debugger anyway?”, but in a debugger you can’t see the result of part of an expression, only the whole expression, so .tap works very nicely in those situations, which in Scala, happen quite often.

1 Like

This feels like something that can become a small library with zero dependencies, if this feature doesn’t exist in one the existing logging libraries.
I think it’s better in a library because I can think of several features (positioning, argument name via reflection, etc.) that people may want to have and something that can be easily evolved, unlike the Scala standard library.

5 Likes

Presumably, it will be inline eventually. For those stuck on Scala 2, don’t neglect -opt.

➜  ~ scala -J--enable-preview '-opt:inline:**' -J--add-exports -Jjdk.jdeps/com.sun.tools.javap=ALL-UNNAMED -nobootcp
Welcome to Scala 2.13.13 (OpenJDK 64-Bit Server VM, Java 21.0.2).
Type in expressions for evaluation. Or try :help.

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

scala> def f(s: String) = s.tap(println).length
def f(s: String): Int

scala> :javap #f
  public int f(java.lang.String);
    descriptor: (Ljava/lang/String;)I
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=2
         0: getstatic     #26                 // Field scala/util/ChainingOps$.MODULE$:Lscala/util/ChainingOps$;
         3: pop
         4: getstatic     #29                 // Field scala/util/package$chaining$.MODULE$:Lscala/util/package$chaining$;
         7: pop
         8: getstatic     #34                 // Field scala/Console$.MODULE$:Lscala/Console$;
        11: aload_1
        12: invokevirtual #38                 // Method scala/Console$.println:(Ljava/lang/Object;)V
        15: aload_1
        16: invokevirtual #44                 // Method java/lang/String.length:()I
        19: ireturn
      LineNumberTable:
        line 1: 0
        line 1: 8
        line 1: 15
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      20     0  this   L$line7/$read$$iw;
            0      20     1     s   Ljava/lang/String;
    MethodParameters:
      Name                           Flags
      s                              final

scala>

It’s awkward to surgically inline from the util.chaining package object.

'-opt:inline:scala.util.**,<sources>'
1 Like

90% of the times when I want to use tap it’s temporary. Adding an import and having to clean it up afterwards is usually more annoying than introducing a local val… It should really be in scala or Predef IMO (“part of the language” in a sense).

8 Likes

.tap exists as sjrd pointed out.
Also isn’t there .tapEach for collections?

1 Like

Ah I didn’t realize that that already existed, thanks!

2 Likes

The import is quite annoying indeed, and results in people just not knowing it exists.

I still add it all the time as tap and pipe are just too practical and readable.

2 Likes

Thanks for the reminder, I forgot to say, if you’re stuck on Scala 2, don’t forget

-Yimports:java.lang,scala,scala.Predef,scala.util.chaining

Probably you already have that for scala.annotation, so just stick chaining on that list.

5 Likes

Thanks for sharing these insights mate as I found it very much useful and informative.

1 Like