Skip to content

Add fine::Result type #12

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed

Conversation

brodeuralexis
Copy link
Contributor

Built on top of fine::Ok<Args...> and fine::Error<Args...>, fine::Result<T, E> offers more user-friendly ergonomics to handling result types from NIF functions by allowing implicit conversion of success and error types when possible.

While similar in behaviour to std::expected<T, E>, the goal of fine::Result<T, E> is not to replace it. fine::Result<T, E>, by design, only supports construction and assignment to prevent peeking into its state.

@josevalim
Copy link

This is probably something users can roll on their own implementations if it makes sense based on the domain they are working on, right?

Built on top of `fine::Ok<Args...>` and `fine::Error<Args...>`,
`fine::Result<T, E>` offers more user-friendly ergonomics to handling
result types from NIF functions by allowing implicit conversion of
success and error types when possible.

While similar in behaviour to `std::expected<T, E>`, the goal of
`fine::Result<T, E>` is not to replace it.  `fine::Result<T, E>`, by
design, only supports construction and assignment to prevent peeking
into its state.
@brodeuralexis
Copy link
Contributor Author

This is probably something users can roll on their own implementations if it makes sense based on the domain they are working on, right?

Yes, but the current version of fine recommends an alias:

template <typename T, typename E>
using Result<T, E> = std::variant<fine::Ok<T>, fine::Error<E>>;

While this works, the ergonomics leave a lot to be desired:

Result<int64_t, std::string> doesntCompile(ErlNifEnv *)
{
  return 42;
}
// error: could not convert '42' from 'int' to 'finest::Result<int64_t, std::string>'

Result<int64_t, std::string> doesntCompile(ErlNifEnv *)
{
  return fine::Ok { 42 };
};
// error: could not convert 'fine::Ok<int>(42)' from 'fine::Ok<int>' to 'finest::Result<int64_t, std::string>'

Result<int64_t, std::string> compiles(ErlNifEnv *)
{
  return fine::Ok<int64_t> { 42 };
}
Result<int64_t, std::string> doesntCompile(ErlNifEnv *)
{
  return fine::Error { "something went wrong" };
};
// error: could not convert 'fine::Error<const char*>(((const char*)"something went wrong"))' from 'fine::Error<const char*>' to 'finest::Result<int64_t, std::string>'

Result<int64_t, std::string> compiles(ErlNifEnv *)
{
  return fine::Error<std::string> { "something went wrong" }; 
}
Result<int64_t, std::string> difficultToParseError(ErlNifEnv *)
{
  Result<int64_t, std::string> result;
  return result.
}
// error: around 50 lines of hard to underestand templated compile errors boiling down to:
// you can't default initialize a std::variant if the first element cannot be default initialized.

I believe that fine should provide fine::Result<T, E> to its users:

  1. {:ok, T} | {:error, E} is ubiquitous in Elixir/Erlang.
  2. The usage of fine::Result<T, E> is simple to understand, even for beginners.
  3. The implementation of fine::Result<T, E> is tricky to get correct because of perfect forwarding and implicit conversion rules.
  4. std::expected<T, E> is in the standard, but not available with C++17

With fine::Result<T, E> as implemented, the conversions above will work. The difficult to parse error is now simply:

// error: no matching function for call to 'fine::Result<int64_t, std::string>::Result()'

Alternative:

Conditional Compilation of fine::Decoder and fine::Encoder specializations for std::expected<T, E> when C++23 is detected.
We already have logic to detect the version from __cplusplus or _MSVC_LANG.

@brodeuralexis
Copy link
Contributor Author

brodeuralexis commented Aug 13, 2025

Closing this in favor of #13, which reworks fine::Ok and fine::Error to allow implicit conversions, even through containers like std::variant when possible.

Now, users can define their own Result<T, E> as std::variant<fine::Ok<T>, fine::Error<T>>, and expect conversions to work correctly, even with multiple tuple values:

std::variant<fine::Ok<int64_t, std::string>, fine::Error<>> example(ErlNifEnv *) {
    return fine::ok(/* int */ 42, /* const char* */ "fine");
}

The only thing that is not supported from this PR is the ability to convert a T to Result<T, E> through fine::Ok<T>:

std::variant<fine::Ok<int64_t>> example(ErlNifEnv *) {
    return static_cast<int64_t>(42); // error: could not convert 'term' from 'int64_t' {aka 'long int'} to 'std::variant<fine::Ok<long int>>
}

But I also believe the clarity of fine::ok and fine::error being explicit makes the code more readable.

@jonatanklosko
Copy link
Member

But I also believe the clarity of fine::ok and fine::error being explicit makes the code more readable.

Definitely agreed! #13 looks good :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants