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: Rust Error Handling. How to safely and properly handle… | by Abhishek Gupta | Better Programming)
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