It’s important to note that [IO.inspect/2] returns the given item unchanged. This makes it
possible to “spy” on values by inserting an IO.inspect/2 call almost anywhere
in your code, for example, in the middle of a pipeline.
You use the & notation when you want to send an anonymous function wrapping IO.inspect/2 as an argument to another function, e.g. Enum.map(list, &IO.inspect/1)
Piping requires function calls, but in the first case you were trying to send the data to a function reference/definition. That’s why the following works as @jwarlander said (because they’re calls to anonymous functions as you’ll notice with the .()):
[1, 2, 3] |> (&(IO.inspect &1)).()
[1, 2, 3] |> (fn x -> IO.inspect x end).()
I’ve written a bit more about it on my blog here (where you can also find other useful pattern snippets)