-
Notifications
You must be signed in to change notification settings - Fork 12
feat: prefer result over throwing exceptions #70
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
base: main
Are you sure you want to change the base?
Conversation
include/sframe/result.h
Outdated
| T MoveValue() { return std::move(*value_); } | ||
|
|
||
| private: | ||
| SFrameError error_; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Following the example of Rust's Result<T, E>, it would be slightly tempting to be generic over the error type. OTOH, Rust crates almost always specialize to Result<T> anyway, so we could just skip the intermediate step.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should also be std::optional and we should remove the none variant, though see the comments about variant below.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we need to keep none variant due to that an caller could call MoveError/error even though it's success. In that case we need to construct an empty error which reason should rather preferably be none
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is the goal here to emulate rust's Result? Or to emulate RTCError from webrtc? I thought kacper had said the latter, which explains these choices. I don't have a strong opinion one way or another, but would say that we should definitely choose one and be consistent with that.
EDIT: I actually intended this to go under the discussion for ok vs is_ok, but I guess it's a question in general.
include/sframe/result.h
Outdated
|
|
||
| private: | ||
| SFrameError error_; | ||
| std::optional<T> value_; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be more correct to use std::variant<T, SFrameError> here, since it would force you to have exactly one of value/error populated, not neither or both. The syntax for accessing variants is hideous (std::visit, etc.), but we could isolate inside of this class's methods so that it didn't bother calling code.
include/sframe/result.h
Outdated
|
|
||
| bool ok() const { return error_.ok(); } | ||
|
|
||
| const T& value() const { return *value_; } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Strongly prefer std::get over operator* because it throws if the optional is not present. That won't be any better if built without exceptions, but in other environments, it fails safer.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Slightly inclined to mimic the Rust syntax here, with unwrap to request the value and unwrwap_err to request the error.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Switched all places to std::get
include/sframe/result.h
Outdated
| template<typename U> | ||
| friend class Result; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: Consider moving to the bottom.
Do we actually need this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch it's not, leftover from the original on which i was basing. Removed
include/sframe/result.h
Outdated
|
|
||
| const T& value() const { return *value_; } | ||
|
|
||
| T& value() { return *value_; } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also add T&& value() &&?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure if we need this access operator at all. Since i would rather use MoveValue() in all places. Maybe it's better to remove MoveValue, and make only T&& value() && ??
include/sframe/result.h
Outdated
| return std::move(error_); | ||
| } | ||
|
|
||
| bool ok() const { return error_.ok(); } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: Mimicking Rust would have this as is_ok(), and possibly a parallel is_err().
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Replaced existing ok() with is_ok, and added is_err, not sure though if we need the latter
include/sframe/result.h
Outdated
| // Helper functions to create Results | ||
| template<typename T> | ||
| Result<T> | ||
| Ok(const T& value) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like where you're going with this, but to match C++ style, these should probably be ok and err. You could also make them static functions on Result<T>, which would give you Result<T>::ok(value), but unfortunately, you would have to include the <T> because of the limits of C++ type inference.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
... though now I see that you need to do that for Err anyway, so yeah, I would probably go with Result<T>::err().
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
src/header.cpp
Outdated
| // Handle error - for now, throw or return default | ||
| throw std::runtime_error("Failed to decode uint"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You might want to define a macro for this, something like:
#defin CHECK(rv) if (!(rv).ok()) { return rv; }
See also: https://searchfox.org/firefox-main/search?q=%23define+ENSURE&path=&case=true®exp=false
|
|
||
| private: | ||
| SFrameErrorType type_ = SFrameErrorType::none; | ||
| std::string message_; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Part of the "which model should we emulate" discussion, but it appears that the message is optional so I wonder if this should be optional?
include/sframe/result.h
Outdated
| T MoveValue() { return std::move(std::get<T>(data_)); } | ||
|
|
||
| private: | ||
| std::variant<SFrameError, T> data_; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
An example of being consistent: if we are going to emulate rust's result, I think we should model the type first here.
| if (!is_length) { | ||
| // Nothing to read; value is already in config byte | ||
| return { value_or_length, data }; | ||
| return Result<std::tuple<uint64_t, input_bytes>>::ok( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we omit the explicit template types in these return values since they're specified in the signature?
| const auto value = decode_uint(data.subspan(0, size)); | ||
| auto value_result = decode_uint(data.subspan(0, size)); | ||
| if (!value_result.is_ok()) { | ||
| return value_result.MoveError(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think I'm following the need for the MoveError method/call here (probably just my c++ skills are too eroded). Why can't we just return value_result here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
value_result have type sframe::Result<uint64_t>
this function expects to return Result<std::tuple<uint64_t, input_bytes>> so it doesnt match.
I return here with MoveError(), to trigger implicit error creation from move constructor.
To be able to return value_result i would need to define conversion between those two return types.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah ha, ok, didn't notice there was a conversion happening there.
| public: | ||
| template<class... Args> | ||
| void emplace(Args&&... args) | ||
| Result<void> emplace(Args&&... args) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This changes the signature of the method. Probably will need to define new method to perform injecton
No description provided.