Pipe dreams | a pipe design question

I have a question about making logic more pipeable.

In my controller I have
Note use of Accounts.send_invite(user)

def create(conn, %{"user" => user_params}) do
    case Accounts.invite_user(user_params) do
      {:ok, user} ->
        Accounts.send_invite(user) # This feels out of place. 

        conn
        |> put_flash(:info, "Invitation sent for #{user.email}")
        |> redirect(to: user_path(conn, :show, user))
      {:error, %Ecto.Changeset{} = changeset} ->
        ...
    end
  end

Heres the methods from accounts.

  def invite_user(attrs \\ %{}) do
    %User{}
    |> User.invite_changeset(attrs)
    |> Repo.insert()
  end

  def send_invite(%User{} = user) do
    Email.send_invite(user) |> Mailer.deliver_later()
  end

I at first had send_invite like so, so I could pipe it but I really don’t like it.

 def invite_user(attrs \\ %{}) do
    %User{}
    |> User.invite_changeset(attrs)
    |> Repo.insert()
    |> send_invite()
  end

  defp send_invite({:error, %Ecto.Changeset{} = changeset}), do: {:error, changeset}
  defp send_invite({:ok, %User{} = user}) do
    Email.send_invite(user) |> Mailer.deliver_later()
    {:ok, %User{} = user}
  end

How would you make this code more “pipeable” if thats even a word.

You example seems perfectly reasonable. If you used the exceptional library then it could become this though:

use Exceptional # somewhere up top

  def invite_user(attrs \\ %{}) do
    %User{}
    |> User.invite_changeset(attrs)
    |> Repo.insert()
    ~> send_invite()
  end

  defp send_invite(%User{} = user) do
    Email.send_invite(user) |> Mailer.deliver_later()
    {:ok, %User{} = user}
  end

What ~> does is like |> except it does two additional things. First, if the value being piped in is some kind of error value (:error, {:error, whatever}, or an exception struct) then it skips calling the function altogether. And the second thing it does is ‘unwrap’ the value to pass it in straight, so something like 42 will be passed in as 42, but {:ok, 42} will be passed in as 42 into the function. I quite like exceptional and use it quite a bit, cleans up code quite a lot. ^.^

It has a lot of other helpers, including one that replaces |> with an enhanced version that include ~> too (not really recommended as it is not elixir’y, that functionality is disabled by default), and things like normalize that normalizes a value from any representation to a value/exception and constructs to force raise if an error and others as well (~> and >>> are the only operators by default, >>> is the same as ~> but it raises if an error instead of skipping, I prefer to use |> ensure() to do that explicitly).

2 Likes

Yup, the code is totally fine. There is no need to bring external dependencies.

@polygonpusher if you find yourself needing to match on :ok and :error multiple times, then also consider using the with special form.

3 Likes