View Source Without (Without v0.2.0)

Without is a tiny module to ease the usage of result tuple.

Using case once is convenient, using it more than twice, makes the code less readable, consider this snippet

case find_user(email) do
  {:ok, user} -> 
    case find_friends(user) do
      {:ok, friends} -> "#{user} is friends of #{Enum.join(",", friends)}"
      {:error, :friends_not_found} -> "#{user} doesn't have any friends"
    end
  {:error, :user_not_found} -> "user not found"
end

You might ask maybe with could help on that?

with {:ok, user} <- find_user(email),
     {:ok, friends} <- find_friends(user) do
    "#{user} is friends of #{Enum.join(",", friends)}"

else
  {:error, :user_not_found} -> "user not found"
  {:error, :friends_not_found} -> "#{user} doesn't have any friends"
end

But the issue here is that variable user is not available in the last else case!

Now that you feel the pain, let me introduce you to Without!

email
|> Without.fmap_ok(&find_user/1, assign: :user)
|> Without.fmap_ok(&find_friends/1)
|> Without.fmap_error(fn error, assigns ->
  case error do
  :user_not_found -> "user not found"
  :friends_not_found, assigns -> "#{assigns[:user]} doesn't have any friends" 
end)
|> Without.fresult

If you are a functional programming aficionado, it might resemble monadic error handling.

One more thing! Without is a lazy operator. It means your pipeline won't be executed until Without.fresult/1 is called. It gives you the flexibility to build your pipeline by passing it around different module/functions and execute them on the last step.

WORD OF CAUTION: Use the following snippet with cautious, I haven't used this technique and might make your code complex and unreadable

def fetch_user(user, %Without{} = without) do
  without
  # Fetch it from somewhere if only previous steps are :ok!
  |> Without.fmap_ok(fn -> {:ok, user} end, assign: :user)
end

def fetch_friends(%Without{} = without) do
  without
  # Fetch it from somewhere if only previous steps are :ok!
  |> Without.fmap_ok(
    fn _, assigns -> {:ok, ["#{assigns[:user]}-f01", "#{assigns[:user]}-f01"]} end,
    assign: :friends
  )
end

def render(%Without{} = without) do
  without
  |> Without.fmap_ok(fn friends ->
    conn = render(conn, friends: friends)
    {:ok, conn}
  end)
  |> Without.fmap_error(fn error ->
    Logger.error("failed to make external calls due to #{error}")
    conn = render(conn, error: error)
    {:ok, conn}
  end)
end

def index(conn, _) do
  required_external_calls = Without.finit(nil)
  # do other things.....
  required_external_calls = fetch_user("milad", required_external_calls)
  # do something else ...
  required_external_calls = fetch_friends(required_external_calls)

  {:ok, conn} = 
    required_external_calls
    |> render()
    |> Without.fresult()

  conn
end

Summary

Types

error()

@type error() :: {:error, any()}

map_func()

@type map_func() :: (-> result()) | (any() -> result()) | (any(), map() -> result())

ok()

@type ok() :: {:ok, any()}

options()

@type options() :: [{:assign, atom()}]

result()

@type result() :: error() | ok()

t()

@type t() :: %Without{
  assigns: map(),
  result: :ok | :error,
  steps: list(),
  value: any()
}

Functions

finit(value)

@spec finit(ok() | error() | any()) :: t()

fmap_error(context, func)

@spec fmap_error(t(), map_func()) :: t()

fmap_ok(context, func, opts \\ [])

@spec fmap_ok(any() | t(), map_func(), options()) :: t()

fresult(context)

@spec fresult(t()) :: {:ok, any()} | {:error, any()}