Feature Request: tap function

459 views
Skip to first unread message

jbo...@wistia.com

unread,
Oct 20, 2016, 4:51:07 PM10/20/16
to elixir-lang-core
In Ruby I've found Object#tap to be really useful when building out pipelines. I've wanted to grab for it several times in my Elixir projects and have found myself building out my own implementations several times. I wanted to see how others felt about it as to me it seems to fit naturally with everything else in Elixir core.

Basically tap takes an object and a callback. It passes the object to the callback then returns the original unmodified object. It can be useful for doing things like triggering callbacks or logging without breaking the flow of pipelines.

Here's a little example of how it could be used for logging. Without tap you'd have to add a bunch of local variables or create wrapper function for the logging which returns the original object

%{name: "elixir"}
|> tap(&Logger.debug(inspect &1))
|> Map.update(:name, "phoenix")

|> tap(&Logger.debug(inspect &1))

=> %{name: "phoenix"}

# to device
16:42:04.225 [debug] %{name: "elixir"}
16:42:04.226 [debug] %{name: "phoenix"}

An implementation of this can be extremely simple:

  def tap(object, cb) do
    cb.(object)
    object
  end

Thoughts?

Greg Vaughn

unread,
Oct 20, 2016, 5:11:08 PM10/20/16
to elixir-l...@googlegroups.com
I love Object#tap in Ruby, however, it was my time spent in Elixir that I realized 'tap' is _only_ useful for side effects. It doesn't fit nicely in with a functional paradigm. Admittedly, Elixir is not strictly pure functional, but we still want to encourage functional approaches. I am against adding 'tap' to the core libraries.

In your case, I'd recommend making some 'debug' function that logs and then returns the parameter it received -- your side effect function now plays nicely in pipelines. Interestingly enough, that's exactly what IO.inspect does and why you can throw it into the middle of pipelines to examine things in the middle of processing.

-Greg Vaughn
> --
> You received this message because you are subscribed to the Google Groups "elixir-lang-core" group.
> To unsubscribe from this group and stop receiving emails from it, send an email to elixir-lang-co...@googlegroups.com.
> To view this discussion on the web visit https://groups.google.com/d/msgid/elixir-lang-core/695092c0-fc88-4b2a-841e-92aa6c77c69b%40googlegroups.com.
> For more options, visit https://groups.google.com/d/optout.

Keitaroh Kobayashi

unread,
Oct 20, 2016, 8:07:10 PM10/20/16
to elixir-l...@googlegroups.com
If I'm not mistaken, you can already use Stream.each/2 for this.

iex> (1..10) |> Stream.each(&IO.inspect/1) |> Enum.to_list

prints to stdout
1
2
3
4
5
6
7
8
9
10

and returns (IO.inspect/1 returns :ok)
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]


On 10/21/2016 05:51 AM, jbo...@wistia.com wrote:
> In Ruby I've found Object#tap
> <http://ruby-doc.org/core-2.3.1/Object.html#method-i-tap> to be really
> --
> You received this message because you are subscribed to the Google
> Groups "elixir-lang-core" group.
> To unsubscribe from this group and stop receiving emails from it, send
> an email to elixir-lang-co...@googlegroups.com
> <mailto:elixir-lang-co...@googlegroups.com>.
> <https://groups.google.com/d/msgid/elixir-lang-core/695092c0-fc88-4b2a-841e-92aa6c77c69b%40googlegroups.com?utm_medium=email&utm_source=footer>.

José Valim

unread,
Oct 21, 2016, 1:40:05 AM10/21/16
to elixir-l...@googlegroups.com
In your case, I'd recommend making some 'debug' function that logs and then returns the parameter it received -- your side effect function now plays nicely in pipelines.

Precisely. It is going to be more readable than the given example too:

%{name: "elixir"}
|> debug_map()
|> Map.update(:name, "phoenix")
|> debug_map()

Nathan Long

unread,
Aug 2, 2018, 8:48:23 AM8/2/18
to elixir-lang-core
> I realized 'tap' is _only_ useful for side effects

Not for me. I often temporarily define `tee/2` myself for debugging. Given this:

      def tee(input, fun) do
         fun.(input)
         input
      end

I can do a more targeted `IO.inspect`, looking only at the attribute(s) that I care about:

    some_struct
    |> some_operation()
    |> tee(fn thing -> IO.inspect([thing.foo, thing.bar], label: "foo and bar after some_operation") end)
    |> other_operation()
    |> ....

Putting `IO.inspect` or `Stream.each(&IO.inspect/1)` or `debug_map` in the pipeline gives me a lot of irrelevant output. To see just what I want without `tee` requires breaking up the pipeline.

step1 =
    some_struct
    |> some_operation()

    IO.inspect([step1.foo, step1.bar], label: "foo and bar after some_operation")

    step1
    |> tee(fn thing -> IO.inspect([thing.foo, thing.bar], label: "foo and bar after some_operation") end)
    |> other_operation()
    |> ....

It's precisely because I want a targeted inspection that plays nicely with pipelines that I define `tee` for myself.

Khaja Minhajuddin

unread,
Aug 6, 2018, 11:01:24 AM8/6/18
to elixir-lang-core
I'd love to see `tap` be part of the `Enum` module. I use it a lot in our app to emit events.

Alexis Brodeur

unread,
Aug 17, 2018, 2:40:31 AM8/17/18
to elixir-lang-core
Why not define a private log/2 function where the first argument is the thing to log and the second the context ?

    user
    |> log(:before_update)
    |> User.update(params)
    |> log(:after_update)

As a plus, this is a whole lot more descriptive of what you are actually logging.

IMHO, tee/1 is not a really pretty solution because it adds noise to the code.  It is difficult to discern what you actually want to log when taking a glance at the code.  Having multiple of those in a pipe can get really confusing.
Reply all
Reply to author
Forward
0 new messages