dogweather

dogweather

Rust is too low-level for most of my work, but I’m envious of the strong fp influence in its design. E.g., the verbose way to react to an error — and bubble it up — looks like this. The gen2() function also returns this Result Union/Either type:

fn caller() -> Result<String, String> {
    let r = gen2();
    match r {
        Ok(i) => Ok(i.to_string()),
        Err(e) => Err(e),
    }
}

To me, that looks a lot like the usual Elixir way of matching on {:ok, ...} and {:error, ...}. Rust, though, has this nice UX syntactic support with the ? operator. This code is equivalent to the above:

fn caller() -> Result<String, String> {
    let n = gen2()?;
    Ok(n.to_string())
}

(Source: https://betterprogramming.pub/rust-error-handling-84e7bd169e47)

Pretty damn nice, IMO. When I was doing a lot of Rust coding, error handling was a breezy, low-boilerplate experience — yet the code was as robust as ever. Invocations with ? like above can be chained together with the . operator.

I’m imagining syntactic support like this that’d work seamlessly with the existing :ok/:error convention, just like the Rust syntax works with Result.

Brainstorming and adapting the example from Writing Predictable Elixir Code with Reducers | AppSignal Blog … here’s the original code:

options = %{conn: conn}
 
with {:ok, payment} <- fetch_payment_information(params, options),
     {:ok, user}    <- fetch_user(conn),
     {:ok, address} <- fetch_address(%{user: user, params: params}, options),
     {:ok, order}   <- create_order(%{user: user, address: address, payment: payment}, options)
  do
  conn
  |> put_flash(:info, "Order completed!")
  |> redirect(to: Routes.order_path(conn, order))
else
  {:error, error_description} ->
    conn
    |> put_flash(:error, parse_error(error_description))
    |> render("checkout.html")
end

Let’s say we want to refactor this, extracting the with clause to a new function, but use a new => operator:

def process_payment(params, options) do
  payment <= fetch_payment_information(params, options),
  user    <= fetch_user(conn),
  address <= fetch_address(%{user: user, params: params}, options),

  order   <= create_order(%{user: user, address: address, payment: payment}, options)
end

Like Rust’s ?, this <= would automatically “unwrap” the return values, directly giving access to the :ok result. But if any of these returns :error it’ll automatically return the first found :error result. In a nutshell, it’d de-sugar to a with/else-construct.

The refactored caller could be standard un-sugared Elixir since it’s top-level and wants to handle the error, not bubble it up. But now it’s more readable:

options = %{conn: conn}
 
case process_payment(params, options) do
  {:ok, order} do
    conn
    |> put_flash(:info, "Order completed!")
    |> redirect(to: Routes.order_path(conn, order))
  {:error, error_description} ->
    conn
    |> put_flash(:error, parse_error(error_description))
    |> render("checkout.html")
end

This has been vetted and used in Rust for years. So even though it hides some info and does some things automatically, I think the Rust experience has validated it as a positive addition to the language.

Along with <= I was thinking about an unwrapping version of the pipe |>: This new version would transparently handle functions that return the :ok/:error tuple, enabling them to be used in pipelines with functions that don’t. IMO this second syntax could make code even cleaner, e.g. if the new pipe is !>:

username
|> to_lower
!> get_user_from_database
|> format_address

First 8 of 8 Posts Switch mode

cmo

cmo

While with is a little more verbose, it has the benefit of being more explicit and more flexible, in that you can match on anything - not just an ok tuple.

F# has the result computation expression which I loved because I found working with the result monad to be a total PITA without it.

mpope

mpope

One thing that is often missed at a glance with with is the inability to specify ordered fallback handling. For example, say there is a response that is {:error, "arbitrary_error_string"} that is returned by two functions my_fun_1 and my_fun_2. However, I as the application developer want two different rollback operations based on if my_fun_1 fails or if my_fun_2 fails, and they’re part of two separate 3rd party libraries, so I have no control over what they return (a scenario I’ve ran into). They both happen to return an error tuple that lacks uniqueness.

What I see lacking from this feature is something similar: a generic ordered error handler. However, it seems to have a much higher potential to fulfill that void if error handler functions were added:

payment <= fetch_payment_information(params, options), &handle_fetch_payment_error/1
user    <= fetch_user(conn), &handle_fetch_user_error/1
address <= fetch_address(%{user: user, params: params}, options), &handle_fetch_address_error/1
order   <= create_order(%{user: user, address: address, payment: payment}, options), &handle_create_order_error/1

But in theory this would just rely on pattern matching in the head of the handle_x_error functions, which is already a well defined feature of the language:

user = fetch_user(conn) |> handle_fetch_user_error()

def handle_fetch_user_error({:error, db_error_struct}) do
...
end
def handle_fetch_user_error({:ok, user}), do: user
al2o3cr

al2o3cr

I don’t follow having “no control” - what about wrapping them in private functions?

Two thoughts:

  • “cleaner” is subjective, IMO the ! vs | visual distinction is very small
  • you can already write functions like this if you want with plain |>, but it means that downstream functions all grow a {:error, _} pass-through case
ityonemo

ityonemo

Elixir is sort of categorically different from rust in that failure is more than just a option, it’s a strategy. Having error tuples exposed and handled is a signal that you can tolerate an error in this location and you’re willing to deal with it, versus triggering a crash and giving up. Hiding that, IMO, loses a signal that qualitatively tells you something about the code that is flowing through your system.

opsb

opsb

There’s a couple of libraries that provide macros towards this goal. Off the top of my head OK Elixir – ok v0.2.0 might be be appealing?

mpope

mpope

True, wrapping them in private functions would have been differentiable.

krasenyp

krasenyp

Can’t you use something like the following to discriminate between error tuples of my_fun_1 and my_fun_2?

with {:mf1, {:ok, _}} <- my_fun_1(...),
     {:mf2, {:ok, _}} <- my_fun_2(...) do
  ...whatever...
else
  {:mf1, {:error, _}} -> rollback_my_fun_1(...)
  {:mf2, {:error, _}} -> rollback_my_fun_2(...)
end
mpope

mpope

These are both valid workarounds, but they still work-around a lack of syntax for ordered error handling in with. This could be out of scope for with, having to create a bunch of transient terms to shoe-horn this into a language feature could be just abusing the feature. The original proposal has the potential to make ordered error handling cleaner.

— All posts loaded —

Where Next? Top

Trending in Discussions Top

AstonJ
As the title says, please share what you’ve been up to with Elixir. Whether that’s been learning it, looking into it, making stuff with i...
2977 91898 914
New
AstonJ
The obligatory hello world thread! Who are you and where are you from? :stuck_out_tongue:
4616 55835 594
New
byu
@chrismccord : I just saw the Extract AGENTS.md from Phoenix.new into phx.new generator commit to the phoenix project. My initial shotgu...
New
arcanemachine
I was working on an Ecto migration and I needed a timestamp. So, for the nth time, I looked up the different data types for timestamps, a...
New
alexslade
Fly’s CEO posted this recently - Turn And Face The Strange · The Fly Blog It says that Fly is going all-in on sprites, which is a worry ...
New
Herve37
We’re evaluating API mocking tools for OpenAPI-based projects and would love to hear what other teams are using. We’re particularly inte...
New
matt-savvy
Is there a word for the ~> symbol used in Version strings? Do you also just call it a Squiggle Arrow™ ?!
New

Other Trending Topics Top

JesseHerrick
Hey, I’m Jesse and I’m the main contributor behind Dexter, a full-featured, lightning-fast Elixir LSP optimized for large codebases. It s...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
Damirados
Hello everyone. After busy few months I am happy to announce v0.1.0 of Emerge &amp; Solve. They are GUI (Emerge) and State management (S...
New
netoum
Corex is an accessible, unstyled UI component library for Phoenix that integrates Zag.js state machines using Vanilla JavaScript and Live...
New
ausimian
Emily is an Elixir library that runs Nx computations on Apple’s MLX. Install it as the default Nx backend and Nx, defn, Axon, Nx.Serving,...
New
wintermeyer
There are three potential reasons for members of this forum to have a look at https://vutuv.de You are tired or annoyed of LinkedIn. Yo...
New

We're in Beta

About us Mission Statement