`&Kernel.then/2` - am I abusing it?

Ever since &Kernel.then/2 (Kernel — Elixir v1.14.0-dev) was introduced, I have been using it more and more, to the point where almost all of my LiveView functions look like this:

def mount(_params, _session, socket) do
  socket
  |> assign(%{something: value})
  |> then(&{:ok, &1})
end

I like doing this because I find that using pipes everywhere makes it easier to keep track of diffs in the code. It also lets me drop an IO.inspect anywhere for quick insight. I also think it looks super neat.

Tell me honestly - is this horrible? If you saw started a new job on a legacy project and saw this then everywhere - what would you think?

I like it but, for professional projects, I care about future devs, so I’m doing a survey here. :slight_smile:

7 Likes

I like it. I have

def ret_noreply(socket), do: {:noreply, socket}
def ret_ok(socket), do: {:ok, socket}

helpers to pipe into.

I think future devs will forgive us.

4 Likes

Personally I think using Kernel.then/2 makes code a lot cleaner in many circumstances, so since we’ve moved to Elixir 1.12 I have also started using it in many places.

1 Like

This style is very similar to Point-free style/tacit programming. So while it sometimes feels nice to express logic as pipelines, lacking variable names, like your example, can make it hard to reason about what’s happening. This can be mitigated by naming intermediary functions properly.

Particularly, I’ve been using Kernel.tap/2 way more, as it essentially screams “I’m performing a side-effect!”.

don’t see this problem in liveview handlers. Not much logic here anyways. Most of the time some event is fed in some context-module, state changed, assigned, returned.

I used it a lot to perform pub_sub broadcast (I mean the tap). However it if you have conditional returns (aka or/error), I have to pattern match. In this case I prefer use « with » to no deal with the error case.

That bring me to the point : I’m the only one who think a tap_with/2 and then_with/2 could be cool?

Ps : I do the same for then(&{:noreply, $1})
PS2: sorry on phone code style is a pain x)

2 Likes

Sorry if it’s really obvious (to me it’s not :sweat_smile:): What would you expect those to look like / do?

1 Like

I want to hate this but I actually really like it. And in fact, I like @Sebb’s idea even more, though I would take it a step further in terseness and just call them ok/1 and noreply/1. Is this the rubyist in me poking through too much?

The reason why I like this so much is based on how the formatter works. I very often get tripped up because I will move the last line in an assign pipeline:

{:ok,
  socket
  |> assign(:foo, foo)
  |> assign(:bar, bar)
  |> assign(:baz, baz)}

It’s so easy to miss that closing }. I never thought it would be a big deal but the frequency in which I switch pipe parts around or even just remove the last piece is actually often enough that it annoys the crap out of me. This fixes that and, as an always-nice-added-bonus, removes a level of indentation.

The only reason I would say that then is nicer is because it’s a little more explicit in that it doesn’t require implementing a helper.

1 Like

For what it’s worth, I have a noreply/1 as well. :slight_smile:

I like both then and tap so much that I’ve “backported” then to our codebase:

defmodule Future do
  @moduledoc """
  Code backported from future Elixir versions.
  """

  @doc """
  Pipes `value` to the given `fun` and returns the `value` itself.
  Useful for running synchronous side effects in a pipeline.
  ## Examples
      iex> tap(1, fn x -> x + 1 end)
      1
  Most commonly, this is used in pipelines. For example,
  let's suppose you want to inspect part of a data structure.
  You could write:
      %{a: 1}
      |> Map.update!(:a, & &1 + 2)
      |> tap(&IO.inspect(&1.a))
      |> Map.update!(:a, & &1 * 2)
  """
  @doc since: "1.12.0"
  defmacro tap_impl(value, fun) do
    quote bind_quoted: [fun: fun, value: value] do
      _ = fun.(value)
      value
    end
  end

  # Does not require "require Future".
  def tap(value, fun), do: tap_impl(value, fun)

  @doc """
  Pipes `value` into the given `fun`.
  In other words, it invokes `fun` with `value` as argument.
  This is most commonly used in pipelines, allowing you
  to pipe a value to a function outside of its first argument.
  ### Examples
      iex> 1 |> then(fn x -> x * 2 end)
      2
      iex> 1 |> then(fn x -> Enum.drop(["a", "b", "c"], x) end)
      ["b", "c"]
  """
  @doc since: "1.12.0"
  defmacro then_impl(value, fun) do
    quote do
      unquote(fun).(unquote(value))
    end
  end

  # Does not require "require Future".
  def then(value, fun), do: then_impl(value, fun)
end

It seems to be a good fit for the use case in the original post - then(&{:ok, &1}). I think it might work well with Enum.reduce_while when you don’t need the accumulator at the end of the processing. It can also replace piping into case:

...
|> foo()
|> then(fn
  :x -> ...
  :y -> ...
end)

But I think the same concerns apply here as with piping into case: if it’s a pipeline of low-level transformations, then it forces you to read the whole code to understand what’s happening, as opposed to using intermediate variables potentially explain things better.

Hello sorry for delay

changeset
|> MyApp.Repo.insert()
|> tap_with(fn {:ok, data} -> IO.puts("Hello") end)

So if MyApp.Repo.insert return {:error, _}, the tap_with/2 will not pattern match so will not be executed.

Currently we could have

changeset
|> MyApp.Repo.insert()
|> tap(fn
  {:ok, data) -> IO.puts("ok")
  _ -> nil # match every other case
end)

I found it weird, so I keep using this:

with {:ok, data} <- MyApp.Repo.insert(changeset) do
  IO.puts("ok")
  {:ok, data}
end)
2 Likes

There’s no need for tap/2 here. IO.inspect/1 returns the passed argument.

1 Like

That’s true, but the passed argument in this case is a single field, not the full map. :slight_smile:

2 Likes

I use Kernel.then plenty as well. But I’m the author of OkThen, so I use Result.then a lot in pipelines as well, and Result.from to wrap in an {:ok, value} tuple :stuck_out_tongue:

1 Like