diff --git a/README.md b/README.md index 6574678..4fdeb86 100644 --- a/README.md +++ b/README.md @@ -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` and `Error` -types for this purpose. +to usual Elixir functions. Fine provides `Result` coupled with +`fine::Ok` and `fine::Error` for this purpose. + +```c++ +// A successful value can be returned directly: +fine::Result example() +{ + // Here, a `uint16_t` is implicitly converted to `fine::Ok`. + return UINT16_C(42); +} +// {:ok, 42} + +// A successful value can be explicitly typed: +fine::Result example() +{ + // Here, `fine::Ok` is implicitly converted to `fine::Ok`. + return fine::Ok/**/(201702); +} +// {:ok, 201_702} + +// An error must be explicitly typed using `fine:Error`: +fine::Result example() +{ + return fine::Error("something went wrong"); +} +// {:error, "something went wrong"} + +// An error can be implicitly converted if the underlying type supports it: +fine::Result example() +{ + // Here, `fine::Error` is implicitly converted to `fine::Error`. + return fine::Error("something else went wrong"); +} +// {:error, "something else went wrong"} + +// The error type can be `void`: +fine::Result example() +{ + return fine::Error(); +} +// :error + +// The result type can be `void`: +fine::Result example() +{ + // With a `void` result type, `fine::Ok<>` must be returned. + return fine::Ok(); +} +// :ok +``` + +`fine::Result` is built on the `Ok` and `Error` types, +which allow for more than 1 value in the encoded tagged tuples: ```c++ fine::Ok<>() @@ -479,29 +530,29 @@ fine::Ok<>() fine::Ok(1) // {:ok, 1} +fine::Ok(2, true) +// {:ok, 2, true} + fine::Error<>() // :error -fine::Error("something went wrong") -// {:error, "something went wrong"} +fine::Error("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` and `fine::Error` directly if needed: ```c++ -std::variant, fine::Error> find_meaning(ErlNifEnv *env) { - if (...) { - return fine::Error("something went wrong"); +std::variant, fine::Error> divmod(ErlNifEnv *env, int64_t a, int64_t b) { + if (b == 0) { + return fine::Error("division by zero", a, b); } - return fine::Ok(42); + return fine::Ok(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 diff --git a/c_include/fine.hpp b/c_include/fine.hpp index 15d0384..f488ebe 100644 --- a/c_include/fine.hpp +++ b/c_include/fine.hpp @@ -142,13 +142,16 @@ class Term { ERL_NIF_TERM term; }; +template class Result; + // Represents a `:ok` tagged tuple, useful as a NIF result. template class Ok { public: - Ok(const Args &...items) : items(items...) {} + explicit Ok(Args... items) : items(std::move(items)...) {} private: friend struct Encoder>; + template friend class Result; std::tuple items; }; @@ -156,14 +159,101 @@ template class Ok { // Represents a `:error` tagged tuple, useful as a NIF result. template class Error { public: - Error(const Args &...items) : items(items...) {} + explicit Error(Args... items) : items(std::move(items)...) {} private: friend struct Encoder>; + template friend class Result; std::tuple items; }; +// Represents a `{:ok, T}` or `{:error, E}` result type, useful as a NIF +// result. +template class Result { +private: + using StorageType = std::variant, Error>; + +public: + template >> + Result(U &&value) : storage{Ok{std::forward(value)}} {} + + template >> + Result(fine::Ok value) + : storage{Ok{std::move(std::get<0>(std::move(value).items))}} {} + + template >> + Result(fine::Error value) + : storage{Error{std::move(std::get<0>(std::move(value).items))}} {} + +private: + friend struct Encoder>; + + StorageType storage; +}; + +// Represents a `{:ok, T}` or `:error` result type, useful as a NIF result. +template class Result { +private: + using StorageType = std::variant, Error>; + +public: + template >> + Result(U &&value) : storage{Ok{std::forward(value)}} {} + + template >> + Result(fine::Ok value) + : storage{Ok{std::move(std::get<0>(std::move(value).items))}} {} + + Result(fine::Error<> value) : storage{std::move(value)} {} + +private: + friend struct Encoder>; + + StorageType storage; +}; + +// Represents a `:ok` or `{:error, E}` result type, useful as a NIF result. +template class Result { +private: + using StorageType = std::variant, Error>; + +public: + Result(fine::Ok<> value) : storage{std::move(value)} {} + + template >> + Result(fine::Error value) + : storage{Error{std::move(std::get<0>(std::move(value).items))}} {} + +private: + friend struct Encoder>; + + StorageType storage; +}; + +// Represents a `:ok` or `:error` result type, useful as a NIF +// result. +template <> class Result { +private: + using StorageType = std::variant, Error<>>; + +public: + Result(fine::Ok<> value) : storage{std::move(value)} {} + + Result(fine::Error<> value) : storage{std::move(value)} {} + +private: + friend struct Encoder>; + + StorageType storage; +}; + namespace __private__ { template struct ResourceWrapper { T resource; @@ -914,6 +1004,12 @@ template struct Encoder> { } }; +template struct Encoder> { + static ERL_NIF_TERM encode(ErlNifEnv *env, const Result &result) { + return fine::encode(env, result.storage); + } +}; + namespace __private__ { class ExceptionError : public std::exception { public: diff --git a/test/c_src/finest.cpp b/test/c_src/finest.cpp index ddb3cd2..1736329 100644 --- a/test/c_src/finest.cpp +++ b/test/c_src/finest.cpp @@ -171,6 +171,30 @@ fine::Ok codec_ok_int64(ErlNifEnv *, int64_t term) { } FINE_NIF(codec_ok_int64, 0); +fine::Result +codec_result_int64_string_ok_explicit(ErlNifEnv *, int64_t term) { + return fine::Ok{term}; +} +FINE_NIF(codec_result_int64_string_ok_explicit, 0); + +fine::Result +codec_result_int64_string_error_explicit(ErlNifEnv *, std::string term) { + return fine::Error{term}; +} +FINE_NIF(codec_result_int64_string_error_explicit, 0); + +fine::Result +codec_result_int64_string_ok_implicit(ErlNifEnv *, int64_t term) { + return static_cast(term); +} +FINE_NIF(codec_result_int64_string_ok_implicit, 0); + +fine::Result +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); diff --git a/test/lib/finest/nif.ex b/test/lib/finest/nif.ex index 371ade0..367cbf2 100644 --- a/test/lib/finest/nif.ex +++ b/test/lib/finest/nif.ex @@ -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!() diff --git a/test/test/finest_test.exs b/test/test/finest_test.exs index dc3bc9b..d91e959 100644 --- a/test/test/finest_test.exs +++ b/test/test/finest_test.exs @@ -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