-
Notifications
You must be signed in to change notification settings - Fork 2
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
TIP: Result type #6
Comments
First of all, let me just say you did a fantastic job on the write-up of this concept! It's really insightful, crystal clear, and shows the amount of research you've done! Of the two suggested implementations, I agree the second one looks less boilerplate to use overall. However using both Result!(int,string) div(int a, int b)
{
if (b == 0) return err!string("Cannot divide by 0!");
else return ok!int(a/b);
} From what I've understood, the type you use for the templates Also I agree with most proposed utility functions to handle
|
...
return immutable(Result!(<typeA>,<typeB>)(Ok!(typeA)));
But it is, your example is wrong. The type specified is the opposing type. Also I may not have presented every usage of the template functions. Result!(int,string) fc1(...)
{
if (...) return ok!string(...); // must specify ErrT --> string;
else return err!int(...) // must specify the OkT --> int
}
// same as Result!()
Result!(void,void) fc2(...)
{
if (...) return ok(...); // no need to specify as void is default
else return err(...) // ditto
}
Yes. But that's not usefulness of these functions. Both auto strnum = ["12","23","1","45","6"];
// lets suppose parse function returns Result
auto val = strnum
.map!(str => str.parse!int)
.map!(resut => result.unwrap())
.fold!"a+b"; Well this is pretty neat! With a simple function we parsed, mapped and summed all numbers! However this would assert if parse went wrong! To overcome this we would have functions like auto strnum = ["12","invalidnumer","1","45","6"];
// lets suppose parse function returns Result
auto val = strnum
.map!(str => str.parse!int)
.map!(resut => result.unwrapOrDefault())
.fold!"a+b"; In case you didn't want to use replacement values for failed auto strnum = ["12","invalidnumer","1","45","6"];
// lets suppose parse function returns Result
auto val = strnum
.map!(str => str.parse!int)
.filter!(result => result.isOk())
.map!(resut => result.unwrap())
.fold!"a+b"; The other useful aspects using only the functions: auto a = somefuncwhichmayfail.unwrap(); // a = OkT or asserts
auto b = somefuncwhichmayfail.expect("oops"); // a = OkT or asserts with an extra message
No, not all functions would return the same. Although auto res = ok(ok(3)); // is of type Result!(Result!(int,void),void))
assert(!res.has(3)); // not true because `OkT == Result!(int,void)`
assert( res.has(ok(3))); // true
assert( res.has(Result!(int,void)(Ok!int(3))); // same as above
assert(res.flatten.has(3)); // now it's true as `OkT == int`
assert(ok(ok(3)) == res);
assert(ok(3) == res.flatten());
assert(ok(3) == res.flatten.flatten()); // when it's reduced to an atomic level it returns the same |
I'm against some things on this proposal, but overall I guess this should be the way to go. The implementation of some projection structs and the final proposed helper functions can lead to some confusion: auto ok(ErrT=void,OkT)(OkT value) { return immutable(Result!(OkT,ErrT))(Ok!OkT(value)); }
auto ok(OkT=void,ErrT)(ErrT value) { return immutable(Result!(OkT,ErrT))(Err!ErrT(value)); } Maybe you wanted the second one to be err(...) right? Anyway, my point is not the naming, and it's rather the template arguments. You can simply use T as a type on the function template. Should be, instead: auto ok(T)(T value) { return Result!(T,void))(Ok!T(value); }
auto err(T)(T value) { return Result!(void,T))(Err!T(value); } Another thing I'm against this implementation is the enforcement of the We shouldn't enforce the usage of immutable storage. The user should choose the qualifiers for its own data. Another point I want to touch on that we should be aware of is: The user could choose to use a non-copyable struct. See the implementation taken from Optional type: public auto some(T)(auto ref T value)
{
import std.traits : isCopyable;
static if (!isCopyable!T)
{
import std.functional : forward;
return optional!T(forward!value);
}
else
{
return optional!T(value);
}
} I would say that getters are not "Future additional content". In this case, getters are mandatory to get the Result projections and there's no other way to directly get them. Some final thoughts:
|
Yes the second function in the first example is wrong, should be Result!(int,string) div(int a, int b)
{
if (b == 0) return err("Cannot divide by 0!"); // Cannot implicitly convert type Result!(void,string) to Result!(int,string)
else return ok(a/b); // Cannot implicitly convert type Result!(int,void) to Result!(int,string)
} It might not be ideal but we no other choice. Both types must be specified and only one of them can be inferred by D. Maybe if noreturn gets accepted we can improve this, but right now there isn't any other choice.
The
auto strnum = "12";
// imagine parse!int returns Result!(int,string)
auto res = strnum.parse!int.map!(i => to!string(i*2)); // takes an `Ok`'s value and maps it
// res --> Result!(string,string)(Ok!string("24"));
assert(Ok!string("24") == res);
These are future functions which will enhance auto strnum = ["1","invalid","3","4"];
foreach (str; strnum)
{
str.parse!int.map!"i*2".match!(
(Ok!int ok) => ok.writeln();
(Err!string err) => err.writeln();
);
}
/* prints:
2
<Some parse error message>
6
8
*/
Once again we can have
Yes they are. if (res.isOk()) someval = res.unwrap(); Right now we don't have manners to |
Yes, I assumed that the type you specify after Result!(int,string) div(int a, int b)
{
if (b == 0) return err("Cannot divide by 0!"); // Cannot implicitly convert type Result!(void,string) to Result!(int,string)
else return ok(a/b); // Cannot implicitly convert type Result!(int,void) to Result!(int,string)
} The only thing that worries me about this constraint is the confusion it may cause to people outside the framework that wish to develop on it (I assume, from your examples above, that if someone is just using your framework, they will instead work with Also I noticed that the type for the error is a But yeah, this is my only gripe with the implementation, simply because it has the potential to be confusing. I understand this is a constraint from the language itself and there's not much it can be done though, so I'll leave it to that. |
I agree with this. Internally we should try to specify almost all the
I used |
The Result type
Concept
The
Result
type is a return type which serves to reduce the amount of exceptions thrown. It is composed by two valuesOk
which holds the type of a successful interaction andErr
which holds the type of an error. TheResult
type can either byOk
orErr
never both nor neither.Result
types can be anything assignable.Result
types can hold otherResult
. When a type isvoid
it means it doesn't hold anything, much like sending response with no body. This is useful, for example, when writing to a file; if something goes wrong anErr
with a type is returned, but if everything was successful andOk
is returned with no content.This is a simple example of what would be the use of the
Result
type. Instead of having a default return value or throwing an exception in case of failure, anErr
with a message is sent.Right now D doesn't offer ways to automatically infer a type to another without using
alias this
. This makes it complicated to work withResult
types. Using the assign operator only works by assigning a value directly.This would be the ideal case. If D's features allowed these types of implicit conversions there would be no complications. However this is not the case and as such the user must at least specify the opposing type of the first returned
Result
. If returningErr
theOk
must be specified and vice-versa.Solutions
Now that we stabilized the user must at least specify the type of the first returned
Result
we can advance to possible implementation solutions.Have Ok and Err implicitly convert to Result
The first solution is to have 3 working diferent type.
Result
,Ok
,Err
and each of them need to holdOkT
andErrT
but with the difference thatResult
defines and implementation for both,Ok
only defines implementation forOkT
makingErrT
a ghost type andErr
only defines implementation forErrT
makingOkT
a ghost type. BothOk
andErr
would have a value forOkT
andErrT
respectively only if it's type wasn'tvoid
. BothOk
andErr
would have analias this
to aResult!(OkT,ErrT)
with one of the types working as the ghost type. This implementation would make it possible to returnOk
orErr
directly in a function. The first type would be the used type and the last the ghost type.This gives a purpose to the usage of
Err
andOk
however it's not intuitive. To make it simpler we can use template function to auto infer the type for us removing some boilerplate code.However this can cause circular reference in the future and some confusion as well because we're saying
Ok
is aResult
type and not some other type. This causesResult
to storeOk
andErr
which are in factResult
types as well meaning theResult
is holding itself. This doesn't help when comparingOk
orErr
with aResult
as we have to pass both types, might as well theResult
type itself to compare.Result, Ok, Err are all different types
Using the helper functions above,
ok
anderr
, we can achieve a case were the user is misdirected in a sense to think bothOk
andErr
are being directly assigned toResult
in a function. If we change the behavior of such functions to return aResult
instead of anOk
orErr
we can maintain the same functionality while simplifyingOk
andErr
by removing the ghost type.The usage is the same with none possible future circular references. Comparison would be much easier to write and intuitive as we can directly compare if a
Result
is indeed andOk
orErr
.Additional content
expect
OkT expect(R : Result!(OkT,ErrT), OkT, ErrT)(auto ref R r, lazy string msg)
OkT
ifResult
isOk
otherwise the program asserts with a message and the content ofErr
expectErr
ErrT expectErr(R : Result!(OkT,ErrT), OkT, ErrT)(auto ref R r, lazy string msg)
expect
unwrap
OkT unwrap(R : Result!(OkT,ErrT), OkT, ErrT)(auto ref R r)
OkT
ifResult
isOk
otherwise the program asserts withErr
's messageunwrapErr
ErrT unwrapErr(R : Result!(OkT,ErrT), OkT, ErrT)(auto ref R r)
unwrap
flatten
Result!(OkT,ErrT) flatten(R : Result!(Result!(OkT,ErrT),ErrT), OkT, ErrT)(auto ref R r)
isOk
true
ifResult
isOk
, false otherwiseisErr
isOk
has
true
ifResult
isOk
and has the given valuehasErr
bool hasErr(R : Result!(OkT,ErrT), OkT, ErrT)(auto ref R r, ErrT value)
the inverser of
has
much more to come ...
Future addicional content
getOk
- returns anOptional
of typeOkT
getErr
- returns anOptional
of typeErrT
more to come ...
Proposal
The
Result
type would be defined as a template struct ofOkT
andErrT
holdingOk!OkT
andErr!ErrT
. These types would be stored in an union. The default type for bothOkT
andErrT
is void. Having the default type means no value is held withinOk
orErr
. To evaluate which type is being held byResult
and enum will be used. This helps prevent accessing when neitherOk
norErr
are assigned.Both
Ok
andErr
are struct which hold a type if not void.Helper functions return a
Result
type.When converting the instance to a string one of the following should print:
Result
is undefinedResult
isOk
Result
isErr
This implementation should never rely on the Garbage Collector and should always be @safe and pure.
References
The
Result
type is inspired by:Second try at Result type implementation
Rationale
Taking a look at the design concept from the first implementation attempt, some could definitely be improved. This second approach will focus only on the Result type itself without any other dependencies talked in last approach (e.g.
match
andOptional
). These will be worked upon when the time for it comes. Some key changes to this attempt are the removal ofOk
andErr
as types the user can interact with. Result should and must be some type of dynamic templated enum; a templated enum with fields which can hold or not variables of the types passed in the template. Rust uses this as their chosen type to implementations such asResult
,Option
,Either
. The Rust example for this new type is:Proposal
This isn't possible in D as seen in the first approach, so we need to simulate this feature using the utilities available
struct
which will hold the types andunion
which will hold the instances containing the values of such types. Every type in D has aninit
state, which by default is used to first assign the value of variable which isn't assigned anything on it's declaration. The same happens toenum
and it's.init
is the first field. Result will follow the same standard and have it's.init
state defined asOk
and the value initialized to it's.init
as well, instead of having an undefined state as proposed before.Both
ok
anderr
struct will now be defined inside Result and it's usage is internal making it completely absent from the user. It shouldn't be possible to ever interact withok
anderr
to obtain it's values, such ends must be obtain through Result'sunwrap
,expect
,unwrapErr
andexpectErr
functions. Another improvement is the way we can interact with Result within a function returning one. Depending on how the function is defined some helpers can be used to abstract completely theResult
type! If a function returning aResult
explicitly, meaning it's not anauto
return, then the helpersOk
,Err
andmakeResult
can be used. BothOk
andErr
are template functions which use as it's parameters__FUNCTION__
and__MODULE__
to obtain it's result type making it possible to construct aResult
of that same type without the need to explicitly pass the unused type as before. However this helpers can only be used within functions declared in the global scope, with any other function returningResult
declared inside another function, object, type, won't possible to use these. To solve such an issue themakeResult
mixin template is provided. Once again, the function must have it's return type explicitly declared! Any other function returning aResult
not following these rules will have to either use the static initialize Result functions or the same helpers as beforeok
anderr
.Can use
Ok
andErr
directly:With makeResult mixin template:
Using
ok
anderr
helpers:Using the static initialize functions:
Extra
This second approach comes with a concept implementation of
Result
. It also brings the implementationof
unwrap
andexpect
for bothOk
andErr
types.Both return the
OkT
orErrT
type if valid and assert if invalid; these assertions will work in-release
mode.Corrections
After a few head scratches with
Ok
andErr
trying to reach it's maximum potential, a new improvement has been made. Now functions declared inside another scope will work without. Keep in mind this will keep failing with types declared within those functions or imported from another module. These cases the mixin have to be used if you wish to abstractResult
.So with these changes, this will work now:
These won't work:
Both above will keep working with
makeResult
mixin template.The text was updated successfully, but these errors were encountered: