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
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 63 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -469,8 +469,59 @@ namespace atoms {
When it comes to NIFs, errors often indicate unexpected failures and
raising an exception makes sense, however you may also want to handle
certain errors gracefully by returning `:ok`/`:error` tuples, similarly
to usual Elixir functions. Fine provides `Ok<Args...>` and `Error<Args...>`
types for this purpose.
to usual Elixir functions. Fine provides `Result<T, E>` coupled with
`fine::Ok<Args...>` and `fine::Error<Args...>` for this purpose.

```c++
// A successful value can be returned directly:
fine::Result<int64_t, std::string> example()
{
// Here, a `uint16_t` is implicitly converted to `fine::Ok<int64_t>`.
return UINT16_C(42);
}
// {:ok, 42}

// A successful value can be explicitly typed:
fine::Result<int64_t, std::string> example()
{
// Here, `fine::Ok<int>` is implicitly converted to `fine::Ok<int64_t>`.
return fine::Ok/*<int>*/(201702);
}
// {:ok, 201_702}

// An error must be explicitly typed using `fine:Error<Args...>`:
fine::Result<int64_t, std::string> example()
{
return fine::Error<std::string>("something went wrong");
}
// {:error, "something went wrong"}

// An error can be implicitly converted if the underlying type supports it:
fine::Result<int64_t, std::string> example()
{
// Here, `fine::Error<const char[26]>` is implicitly converted to `fine::Error<std::string>`.
return fine::Error("something else went wrong");
}
// {:error, "something else went wrong"}

// The error type can be `void`:
fine::Result<int64_t, void> example()
{
return fine::Error();
}
// :error

// The result type can be `void`:
fine::Result<void, std::string> example()
{
// With a `void` result type, `fine::Ok<>` must be returned.
return fine::Ok();
}
// :ok
```

`fine::Result<T, E>` is built on the `Ok<Args...>` and `Error<Args...>` types,
which allow for more than 1 value in the encoded tagged tuples:

```c++
fine::Ok<>()
Expand All @@ -479,29 +530,29 @@ fine::Ok<>()
fine::Ok<int64_t>(1)
// {:ok, 1}

fine::Ok<int64_t, bool>(2, true)
// {:ok, 2, true}

fine::Error<>()
// :error

fine::Error<std::string>("something went wrong")
// {:error, "something went wrong"}
fine::Error<std::string, int64_t>("something went wrong", 42)
// {:error, "something went wrong", 42}
```

You can use `std::variant` to express a union of possible result types
a NIF may return:
using `fine::Ok<Args...>` and `fine::Error<Args...>` directly if needed:

```c++
std::variant<fine::Ok<int64_t>, fine::Error<std::string>> find_meaning(ErlNifEnv *env) {
if (...) {
return fine::Error<std::string>("something went wrong");
std::variant<fine::Ok<int64_t, int64_t>, fine::Error<std::string, int64_t, int64_t>> divmod(ErlNifEnv *env, int64_t a, int64_t b) {
if (b == 0) {
return fine::Error<std::string>("division by zero", a, b);
}

return fine::Ok<int64_t>(42);
return fine::Ok<int64_t, int64_t>(a / b, a % b);
}
```

Note that if you use a particular union frequently, it may be convenient
to define a type alias with `using`/`typedef` to keep signatures brief.

## Synchronization

Erlang is a multi-process environment where each process is guaranteed to be
Expand Down
100 changes: 98 additions & 2 deletions c_include/fine.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -142,28 +142,118 @@ class Term {
ERL_NIF_TERM term;
};

template <typename T, typename E> class Result;

// Represents a `:ok` tagged tuple, useful as a NIF result.
template <typename... Args> class Ok {
public:
Ok(const Args &...items) : items(items...) {}
explicit Ok(Args... items) : items(std::move(items)...) {}

private:
friend struct Encoder<Ok<Args...>>;
template <typename T, typename E> friend class Result;

std::tuple<Args...> items;
};

// Represents a `:error` tagged tuple, useful as a NIF result.
template <typename... Args> class Error {
public:
Error(const Args &...items) : items(items...) {}
explicit Error(Args... items) : items(std::move(items)...) {}

private:
friend struct Encoder<Error<Args...>>;
template <typename T, typename E> friend class Result;

std::tuple<Args...> items;
};

// Represents a `{:ok, T}` or `{:error, E}` result type, useful as a NIF
// result.
template <typename T, typename E> class Result {
private:
using StorageType = std::variant<Ok<T>, Error<E>>;

public:
template <typename U,
typename = std::enable_if_t<std::is_constructible_v<T, U &&>>>
Result(U &&value) : storage{Ok<T>{std::forward<U>(value)}} {}

template <typename U,
typename = std::enable_if_t<std::is_constructible_v<T, U &&>>>
Result(fine::Ok<U> value)
: storage{Ok<T>{std::move(std::get<0>(std::move(value).items))}} {}

template <typename U,
typename = std::enable_if_t<std::is_constructible_v<E, U &&>>>
Result(fine::Error<U> value)
: storage{Error<E>{std::move(std::get<0>(std::move(value).items))}} {}

private:
friend struct Encoder<Result<T, E>>;

StorageType storage;
};

// Represents a `{:ok, T}` or `:error` result type, useful as a NIF result.
template <typename T> class Result<T, void> {
private:
using StorageType = std::variant<Ok<T>, Error<T>>;

public:
template <typename U,
typename = std::enable_if_t<std::is_constructible_v<T, U &&>>>
Result(U &&value) : storage{Ok<T>{std::forward<U>(value)}} {}

template <typename U,
typename = std::enable_if_t<std::is_constructible_v<T, U &&>>>
Result(fine::Ok<U> value)
: storage{Ok<T>{std::move(std::get<0>(std::move(value).items))}} {}

Result(fine::Error<> value) : storage{std::move(value)} {}

private:
friend struct Encoder<Result<T, void>>;

StorageType storage;
};

// Represents a `:ok` or `{:error, E}` result type, useful as a NIF result.
template <typename E> class Result<void, E> {
private:
using StorageType = std::variant<Ok<>, Error<E>>;

public:
Result(fine::Ok<> value) : storage{std::move(value)} {}

template <typename U,
typename = std::enable_if_t<std::is_constructible_v<E, U &&>>>
Result(fine::Error<U> value)
: storage{Error<E>{std::move(std::get<0>(std::move(value).items))}} {}

private:
friend struct Encoder<Result<void, E>>;

StorageType storage;
};

// Represents a `:ok` or `:error` result type, useful as a NIF
// result.
template <> class Result<void, void> {
private:
using StorageType = std::variant<Ok<>, Error<>>;

public:
Result(fine::Ok<> value) : storage{std::move(value)} {}

Result(fine::Error<> value) : storage{std::move(value)} {}

private:
friend struct Encoder<Result<void, void>>;

StorageType storage;
};

namespace __private__ {
template <typename T> struct ResourceWrapper {
T resource;
Expand Down Expand Up @@ -914,6 +1004,12 @@ template <typename... Args> struct Encoder<Error<Args...>> {
}
};

template <typename T, typename E> struct Encoder<Result<T, E>> {
static ERL_NIF_TERM encode(ErlNifEnv *env, const Result<T, E> &result) {
return fine::encode(env, result.storage);
}
};

namespace __private__ {
class ExceptionError : public std::exception {
public:
Expand Down
24 changes: 24 additions & 0 deletions test/c_src/finest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,30 @@ fine::Ok<int64_t> codec_ok_int64(ErlNifEnv *, int64_t term) {
}
FINE_NIF(codec_ok_int64, 0);

fine::Result<int64_t, std::string>
codec_result_int64_string_ok_explicit(ErlNifEnv *, int64_t term) {
return fine::Ok<int64_t>{term};
}
FINE_NIF(codec_result_int64_string_ok_explicit, 0);

fine::Result<int64_t, std::string>
codec_result_int64_string_error_explicit(ErlNifEnv *, std::string term) {
return fine::Error<std::string>{term};
}
FINE_NIF(codec_result_int64_string_error_explicit, 0);

fine::Result<int64_t, std::string>
codec_result_int64_string_ok_implicit(ErlNifEnv *, int64_t term) {
return static_cast<int32_t>(term);
}
FINE_NIF(codec_result_int64_string_ok_implicit, 0);

fine::Result<int64_t, std::string>
codec_result_int64_string_error_conversion(ErlNifEnv *) {
return fine::Error{"constant string"};
}
FINE_NIF(codec_result_int64_string_error_conversion, 0);

fine::Error<> codec_error_empty(ErlNifEnv *) { return fine::Error(); }
FINE_NIF(codec_error_empty, 0);

Expand Down
4 changes: 4 additions & 0 deletions test/lib/finest/nif.ex
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ defmodule Finest.NIF do
def codec_ok_int64(_term), do: err!()
def codec_error_empty(), do: err!()
def codec_error_string(_term), do: err!()
def codec_result_int64_string_ok_explicit(_term), do: err!()
def codec_result_int64_string_error_explicit(_term), do: err!()
def codec_result_int64_string_ok_implicit(_term), do: err!()
def codec_result_int64_string_error_conversion(), do: err!()

def resource_create(_pid), do: err!()
def resource_get(_resource), do: err!()
Expand Down
10 changes: 10 additions & 0 deletions test/test/finest_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,16 @@ defmodule FinestTest do
assert NIF.codec_error_empty() == :error
assert NIF.codec_error_string("this is the reason") == {:error, "this is the reason"}
end

test "ok result" do
assert NIF.codec_result_int64_string_ok_explicit(42) == {:ok, 42}
assert NIF.codec_result_int64_string_ok_implicit(201_703) == {:ok, 201_703}
end

test "error result" do
assert NIF.codec_result_int64_string_error_explicit("some error") == {:error, "some error"}
assert NIF.codec_result_int64_string_error_conversion() == {:error, "constant string"}
end
end

describe "resource" do
Expand Down