Skip to content

Commit d8c5fe9

Browse files
committed
Add fine::Result type
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.
1 parent b3dee74 commit d8c5fe9

File tree

5 files changed

+199
-14
lines changed

5 files changed

+199
-14
lines changed

README.md

Lines changed: 63 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -469,8 +469,59 @@ namespace atoms {
469469
When it comes to NIFs, errors often indicate unexpected failures and
470470
raising an exception makes sense, however you may also want to handle
471471
certain errors gracefully by returning `:ok`/`:error` tuples, similarly
472-
to usual Elixir functions. Fine provides `Ok<Args...>` and `Error<Args...>`
473-
types for this purpose.
472+
to usual Elixir functions. Fine provides `Result<T, E>` coupled with
473+
`fine::Ok<Args...>` and `fine::Error<Args...>` for this purpose.
474+
475+
```c++
476+
// A successful value can be returned directly:
477+
fine::Result<int64_t, std::string> example()
478+
{
479+
// Here, a `uint16_t` is implicitly converted to `fine::Ok<int64_t>`.
480+
return UINT16_C(42);
481+
}
482+
// {:ok, 42}
483+
484+
// A successful value can be explicitly typed:
485+
fine::Result<int64_t, std::string> example()
486+
{
487+
// Here, `fine::Ok<int>` is implicitly converted to `fine::Ok<int64_t>`.
488+
return fine::Ok/*<int>*/(201702);
489+
}
490+
// {:ok, 201_702}
491+
492+
// An error must be explicitly typed using `fine:Error<Args...>`:
493+
fine::Result<int64_t, std::string> example()
494+
{
495+
return fine::Error<std::string>("something went wrong");
496+
}
497+
// {:error, "something went wrong"}
498+
499+
// An error can be implicitly converted if the underlying type supports it:
500+
fine::Result<int64_t, std::string> example()
501+
{
502+
// Here, `fine::Error<const char[26]>` is implicitly converted to `fine::Error<std::string>`.
503+
return fine::Error("something else went wrong");
504+
}
505+
// {:error, "something else went wrong"}
506+
507+
// The error type can be `void`:
508+
fine::Result<int64_t, void> example()
509+
{
510+
return fine::Error();
511+
}
512+
// :error
513+
514+
// The result type can be `void`:
515+
fine::Result<void, std::string> example()
516+
{
517+
// With a `void` result type, `fine::Ok<>` must be returned.
518+
return fine::Ok();
519+
}
520+
// :ok
521+
```
522+
523+
`fine::Result<T, E>` is built on the `Ok<Args...>` and `Error<Args...>` types,
524+
which allow for more than 1 value in the encoded tagged tuples:
474525
475526
```c++
476527
fine::Ok<>()
@@ -479,29 +530,29 @@ fine::Ok<>()
479530
fine::Ok<int64_t>(1)
480531
// {:ok, 1}
481532
533+
fine::Ok<int64_t, bool>(2, true)
534+
// {:ok, 2, true}
535+
482536
fine::Error<>()
483537
// :error
484538
485-
fine::Error<std::string>("something went wrong")
486-
// {:error, "something went wrong"}
539+
fine::Error<std::string, int64_t>("something went wrong", 42)
540+
// {:error, "something went wrong", 42}
487541
```
488542
489543
You can use `std::variant` to express a union of possible result types
490-
a NIF may return:
544+
using `fine::Ok<Args...>` and `fine::Error<Args...>` directly if needed:
491545
492546
```c++
493-
std::variant<fine::Ok<int64_t>, fine::Error<std::string>> find_meaning(ErlNifEnv *env) {
494-
if (...) {
495-
return fine::Error<std::string>("something went wrong");
547+
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) {
548+
if (b == 0) {
549+
return fine::Error<std::string>("division by zero", a, b);
496550
}
497551
498-
return fine::Ok<int64_t>(42);
552+
return fine::Ok<int64_t, int64_t>(a / b, a % b);
499553
}
500554
```
501555
502-
Note that if you use a particular union frequently, it may be convenient
503-
to define a type alias with `using`/`typedef` to keep signatures brief.
504-
505556
## Synchronization
506557
507558
Erlang is a multi-process environment where each process is guaranteed to be

c_include/fine.hpp

Lines changed: 98 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,28 +142,118 @@ class Term {
142142
ERL_NIF_TERM term;
143143
};
144144

145+
template <typename T, typename E> class Result;
146+
145147
// Represents a `:ok` tagged tuple, useful as a NIF result.
146148
template <typename... Args> class Ok {
147149
public:
148-
Ok(const Args &...items) : items(items...) {}
150+
explicit Ok(Args... items) : items(std::move(items)...) {}
149151

150152
private:
151153
friend struct Encoder<Ok<Args...>>;
154+
template <typename T, typename E> friend struct Result;
152155

153156
std::tuple<Args...> items;
154157
};
155158

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

161164
private:
162165
friend struct Encoder<Error<Args...>>;
166+
template <typename T, typename E> friend struct Result;
163167

164168
std::tuple<Args...> items;
165169
};
166170

171+
// Represents a `{:ok, T}` or `{:error, E}` result type, useful as a NIF
172+
// result.
173+
template <typename T, typename E> class Result {
174+
private:
175+
using StorageType = std::variant<Ok<T>, Error<E>>;
176+
177+
public:
178+
template <typename U,
179+
typename = std::enable_if_t<std::is_constructible_v<T, U &&>>>
180+
Result(U &&value) : storage{Ok<T>{std::forward<U>(value)}} {}
181+
182+
template <typename U,
183+
typename = std::enable_if_t<std::is_constructible_v<T, U &&>>>
184+
Result(fine::Ok<U> value)
185+
: storage{Ok<T>{std::move(std::get<0>(std::move(value).items))}} {}
186+
187+
template <typename U,
188+
typename = std::enable_if_t<std::is_constructible_v<E, U &&>>>
189+
Result(fine::Error<U> value)
190+
: storage{Error<E>{std::move(std::get<0>(std::move(value).items))}} {}
191+
192+
private:
193+
friend struct Encoder<Result<T, E>>;
194+
195+
StorageType storage;
196+
};
197+
198+
// Represents a `{:ok, T}` or `:error` result type, useful as a NIF result.
199+
template <typename T> class Result<T, void> {
200+
private:
201+
using StorageType = std::variant<Ok<T>, Error<T>>;
202+
203+
public:
204+
template <typename U,
205+
typename = std::enable_if_t<std::is_constructible_v<T, U &&>>>
206+
Result(U &&value) : storage{Ok<T>{std::forward<U>(value)}} {}
207+
208+
template <typename U,
209+
typename = std::enable_if_t<std::is_constructible_v<T, U &&>>>
210+
Result(fine::Ok<U> value)
211+
: storage{Ok<T>{std::move(std::get<0>(std::move(value).items))}} {}
212+
213+
Result(fine::Error<> value) : storage{std::move(value)} {}
214+
215+
private:
216+
friend struct Encoder<Result<T, void>>;
217+
218+
StorageType storage;
219+
};
220+
221+
// Represents a `:ok` or `{:error, E}` result type, useful as a NIF result.
222+
template <typename E> class Result<void, E> {
223+
private:
224+
using StorageType = std::variant<Ok<>, Error<E>>;
225+
226+
public:
227+
Result(fine::Ok<> value) : storage{std::move(value)} {}
228+
229+
template <typename U,
230+
typename = std::enable_if_t<std::is_constructible_v<E, U &&>>>
231+
Result(fine::Error<U> value)
232+
: storage{Error<E>{std::move(std::get<0>(std::move(value).items))}} {}
233+
234+
private:
235+
friend struct Encoder<Result<void, E>>;
236+
237+
StorageType storage;
238+
};
239+
240+
// Represents a `:ok` or `:error` result type, useful as a NIF
241+
// result.
242+
template <> class Result<void, void> {
243+
private:
244+
using StorageType = std::variant<Ok<>, Error<>>;
245+
246+
public:
247+
Result(fine::Ok<> value) : storage{std::move(value)} {}
248+
249+
Result(fine::Error<> value) : storage{std::move(value)} {}
250+
251+
private:
252+
friend struct Encoder<Result<void, void>>;
253+
254+
StorageType storage;
255+
};
256+
167257
namespace __private__ {
168258
template <typename T> struct ResourceWrapper {
169259
T resource;
@@ -914,6 +1004,12 @@ template <typename... Args> struct Encoder<Error<Args...>> {
9141004
}
9151005
};
9161006

1007+
template <typename T, typename E> struct Encoder<Result<T, E>> {
1008+
static ERL_NIF_TERM encode(ErlNifEnv *env, const Result<T, E> &result) {
1009+
return fine::encode(env, result.storage);
1010+
}
1011+
};
1012+
9171013
namespace __private__ {
9181014
class ExceptionError : public std::exception {
9191015
public:

test/c_src/finest.cpp

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,30 @@ fine::Ok<int64_t> codec_ok_int64(ErlNifEnv *, int64_t term) {
171171
}
172172
FINE_NIF(codec_ok_int64, 0);
173173

174+
fine::Result<int64_t, std::string>
175+
codec_result_int64_string_ok_explicit(ErlNifEnv *, int64_t term) {
176+
return fine::Ok<int64_t>{term};
177+
}
178+
FINE_NIF(codec_result_int64_string_ok_explicit, 0);
179+
180+
fine::Result<int64_t, std::string>
181+
codec_result_int64_string_error_explicit(ErlNifEnv *, std::string term) {
182+
return fine::Error<std::string>{term};
183+
}
184+
FINE_NIF(codec_result_int64_string_error_explicit, 0);
185+
186+
fine::Result<int64_t, std::string>
187+
codec_result_int64_string_ok_implicit(ErlNifEnv *, int64_t term) {
188+
return static_cast<int32_t>(term);
189+
}
190+
FINE_NIF(codec_result_int64_string_ok_implicit, 0);
191+
192+
fine::Result<int64_t, std::string>
193+
codec_result_int64_string_error_conversion(ErlNifEnv *) {
194+
return fine::Error{"constant string"};
195+
}
196+
FINE_NIF(codec_result_int64_string_error_conversion, 0);
197+
174198
fine::Error<> codec_error_empty(ErlNifEnv *) { return fine::Error(); }
175199
FINE_NIF(codec_error_empty, 0);
176200

test/lib/finest/nif.ex

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ defmodule Finest.NIF do
3737
def codec_ok_int64(_term), do: err!()
3838
def codec_error_empty(), do: err!()
3939
def codec_error_string(_term), do: err!()
40+
def codec_result_int64_string_ok_explicit(_term), do: err!()
41+
def codec_result_int64_string_error_explicit(_term), do: err!()
42+
def codec_result_int64_string_ok_implicit(_term), do: err!()
43+
def codec_result_int64_string_error_conversion(), do: err!()
4044

4145
def resource_create(_pid), do: err!()
4246
def resource_get(_resource), do: err!()

test/test/finest_test.exs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,16 @@ defmodule FinestTest do
217217
assert NIF.codec_error_empty() == :error
218218
assert NIF.codec_error_string("this is the reason") == {:error, "this is the reason"}
219219
end
220+
221+
test "ok result" do
222+
assert NIF.codec_result_int64_string_ok_explicit(42) == {:ok, 42}
223+
assert NIF.codec_result_int64_string_ok_implicit(201_703) == {:ok, 201_703}
224+
end
225+
226+
test "error result" do
227+
assert NIF.codec_result_int64_string_error_explicit("some error") == {:error, "some error"}
228+
assert NIF.codec_result_int64_string_error_conversion() == {:error, "constant string"}
229+
end
220230
end
221231

222232
describe "resource" do

0 commit comments

Comments
 (0)