Is there an elegant way to return the input of a function used in Elixir pipelines?

Viewed 97

I have some Elixir code like this:

def generate_and_process_foo() do
  foo = generate_foo() |> transform_foo()
  process_foo(foo)
  foo
end

The process_foo() function performs side-effects, but I do not care about its return value. I want generate_and_process_foo() to return foo rather than the return value of process_foo().

I'd ideally like to refactor this code to place it all in a pipeline, while preserving my return value. I can do this:

def generate_and_process_foo() do
  generate_foo()
  |> transform_foo()
  |> (fn foo -> process_foo(foo); foo end).()
end

But that feels worse that the original version.

I'd ideally like to do something like this:

def generate_and_process_foo() do
  generate_foo()
  |> transform_foo()
  |> Function.side_effect(&process_foo/1)
end

Does anything like that exist? Or some trick I'm missing?

3 Answers

This has still not been released, but I think it is worth mentioning for that the upcoming 1.12 version of Elixir will include a tap/2 function doing exactly this in the Kernel, here is the PR.

def generate_and_process_foo() do
  generate_foo()
  |> transform_foo()
  |> tap(&process_foo/1)
end

In the meantime, it is really easy to define your own implementation, and to replace it with the standard library one once it gets released.

There is no such possibility out of the box (mostly because discarding results no matter what is not a common use-case in ,) but you might either easily create the function yourself as

def side_effect(foo, fun), do: (fun.(foo); foo)

or use a captured form for the anonymous function (which looks a bit weird, but might be shortened to either &&/2 or ||/2 if process_foo/1 returns a determined result.

 |> (&process_foo(&1) && &1 || &1).()

The [arguably] worse, but still possible, way to handle it would be with/1 block

def generate_and_process_foo() do
  with foo <- generate_foo() |> transform_foo(),
       process_foo(foo),
    do: foo
end

or even case

def generate_and_process_foo() do
  generate_foo()
  |> transform_foo()
  |> case do
    foo -> process_foo(foo); foo
  end
end

In elixir all functions must return a result. You cannot guarantee that your side effect function will always work, so usually the way to go this is to return either {:ok, foo} or {:error, foo}, things like try catch in elixir have a total different using compared to other languages, so usually you handle errors as data. What I would go with is:

def generate_and_process_foo() do
  foo = generate_foo() 
  |> transform_foo()
  |> process_foo()
  |> case do
    {:ok, foo} -> foo
    {:error, reason} -> # do something with errror
  end
end
Related