Skip to content
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

Open
iK4tsu opened this issue Feb 10, 2021 · 7 comments · May be fixed by #8
Open

TIP: Result type #6

iK4tsu opened this issue Feb 10, 2021 · 7 comments · May be fixed by #8
Labels
core enhancement New feature or request TIP

Comments

@iK4tsu
Copy link
Contributor

iK4tsu commented Feb 10, 2021

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 values Ok which holds the type of a successful interaction and Err which holds the type of an error. The Result type can either by Ok or Err never both nor neither. Result types can be anything assignable. Result types can hold other Result. When a type is void 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 an Err with a type is returned, but if everything was successful and Ok is returned with no content.

Result!(int, string) div(int a, int b)
{
	if (b == 0)
		return err!int("Cannot divide by 0!");
	else
		return ok!string(a/b);
}

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, an Err 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 with Result types. Using the assign operator only works by assigning a value directly.

struct Result(OkT=void,ErrT=void)
{
	this(Ok!OkT ok) { ... }
	this(Err!ErrT err) { ... }
	void opAssign(Ok!OkT ok) { ... }
	void opAssign(Err!ErrT err) { ... }
	...
}
struct Ok(T) { ... }
struct Err(T) { ... }

Result!(int,string) res = Ok!int(3); // ok
Result!(int,string) res = Result!(int,string)(Err!string("")); // ok

// cannot infer Err!string to Result!(int,string)
Result!(int,string) div(int a, int b)
{
	if (b == 0) return Err!string("Cannot divide by 0!");
	else return Ok!int(a/b);
}

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 returning Err the Ok 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 hold OkT and ErrT but with the difference that Result defines and implementation for both, Ok only defines implementation for OkT making ErrT a ghost type and Err only defines implementation for ErrT making OkT a ghost type. Both Ok and Err would have a value for OkT and ErrT respectively only if it's type wasn't void. Both Ok and Err would have an alias this to a Result!(OkT,ErrT) with one of the types working as the ghost type. This implementation would make it possible to return Ok or Err directly in a function. The first type would be the used type and the last the ghost type.

struct Ok(OkT,GhostT=void)
{
	Result!(OkT,GhostT) res() @property { return Result!(OkT,GhostT)(this); }
	OkT value;
	alias res this;
}

struct Err(ErrT,GhostT=void)
{
	Result!(GhostT,ErrT) res() @property { return Result!(GhostT,ErrT)(this); }
	ErrT value;
	alias res this;
}

Result!(int,string) div(int a, int b)
{
	if (b == 0) return Err!(string,int)("Cannot divide by 0!");
	else return Ok!(int,string)(a/b);
}

Result!(void,string) fc()
{
	if (anerrorshouldbereturned) return Err!string("Error!"); // no specification needed
	else return Ok!(void,string)();
}

This gives a purpose to the usage of Err and Ok 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.

Ok!(OkT,GhostT) ok(GhostT=void,OkT)(OkT t)
{
	return Ok!(OkT,GhostT)(t);
}

Err!(ErrT,GhostT) err(GhostT=void,ErrT)(ErrT t)
{
	return Err!(ErrT,GhostT)(t);
}

Result!(int,string) div(int a, int b)
{
	if (b == 0) return err!int("Cannot divide by 0!");
	else return ok!string(a/b);
}

However this can cause circular reference in the future and some confusion as well because we're saying Ok is a Result type and not some other type. This causes Result to store Ok and Err which are in fact Result types as well meaning the Result is holding itself. This doesn't help when comparing Ok or Err with a Result as we have to pass both types, might as well the Result type itself to compare.

Result!(int,string) res = ok!string(3);
assert(Ok!(int,string)(3) == res); // not ideal
assert(ok!string(3) == res); // yes we could compare like this, but not the point

Result, Ok, Err are all different types

Using the helper functions above, ok and err, we can achieve a case were the user is misdirected in a sense to think both Ok and Err are being directly assigned to Result in a function. If we change the behavior of such functions to return a Result instead of an Ok or Err we can maintain the same functionality while simplifying Ok and Err by removing the ghost type.

Result!(OkT,ErrT) ok(ErrT=void,OkT)(OkT t)
{
	return Result!(OkT,ErrT)(Ok!OkT(t));
}

Result!(OkT,ErrT) err(OkT=void,ErrT)(ErrT t)
{
	return Result!(OkT,ErrT)(Err!ErrT(t));
}

Result!(int,string) div(int a, int b)
{
	if (b == 0) return err!int("Cannot divide by 0!");
	else return ok!string(a/b);
}

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 and Ok or Err.

Result!(int,string) res = ok!string(3);
assert(Ok!int(3) == res); // true :)

Additional content

expect

  • OkT expect(R : Result!(OkT,ErrT), OkT, ErrT)(auto ref R r, lazy string msg)
  • returns OkT if Result is Ok otherwise the program asserts with a message and the content of Err
Result!(int,string) res = err("an error");
res.expect("this will exit"); // exits with "this will exit: an error"

expectErr

  • ErrT expectErr(R : Result!(OkT,ErrT), OkT, ErrT)(auto ref R r, lazy string msg)
  • the inverse of expect

unwrap

  • OkT unwrap(R : Result!(OkT,ErrT), OkT, ErrT)(auto ref R r)
  • returns OkT if Result is Ok otherwise the program asserts with Err's message
Result!(int,string) res = err("emergency exit");
res.unwrap(); // exits with "emergency exit"

unwrapErr

  • ErrT unwrapErr(R : Result!(OkT,ErrT), OkT, ErrT)(auto ref R r)
  • the inverse of unwrap

flatten

  • Result!(OkT,ErrT) flatten(R : Result!(Result!(OkT,ErrT),ErrT), OkT, ErrT)(auto ref R r)
  • converts from Result!(Result!(OkT,ErrT), ErrT) to Result!(OkT, ErrT)
Result!(Result!(int,void),void) res = ok(ok(3));
Result!(int,void) fres = res.flatten();

isOk

  • bool isOk(R : Result!(OkT,ErrT), OkT, ErrT)(auto ref R r)
  • returns true if Result is Ok, false otherwise
auto res = ok(3);
assert(res.isOk());

isErr

  • bool isErr(R : Result!(OkT,ErrT), OkT, ErrT)(auto ref R r)
  • the inverse of isOk

has

  • bool has(R : Result!(OkT,ErrT), OkT, ErrT)(auto ref R r, OkT value)
  • returns true if Result is Ok and has the given value

hasErr

  • 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 an Optional of type OkT

  • getErr - returns an Optional of type ErrT

  • more to come ...

Proposal

The Result type would be defined as a template struct of OkT and ErrT holding Ok!OkT and Err!ErrT. These types would be stored in an union. The default type for both OkT and ErrT is void. Having the default type means no value is held within Ok or Err. To evaluate which type is being held by Result and enum will be used. This helps prevent accessing when neither Ok nor Err are assigned.

struct Result(OkT=void,ErrT=void)
{
	...

private:
	union
	{
		Ok!OkT okpayload;
		Err!ErrT errpayload;
	}

	enum State { undefined, ok, err }
	immutable State state;
}

Both Ok and Err are struct which hold a type if not void.

struct Ok(OkT) { static if (!is(OkT == void)) OkT t; }
struct Err(ErrT) { static if (!is(ErrT == void)) OkT t; }
alias Ok() = Ok!void; // allow usage with Ok!() to define an empty type
alias Err() = Err!void; // ditto

Helper functions return a Result type.

auto ok(ErrT=void,OkT)(OkT value) { return immutable(Result!(OkT,ErrT))(Ok!OkT(value)); }
auto err(OkT=void,ErrT)(ErrT value) { return immutable(Result!(OkT,ErrT))(Err!ErrT(value)); }

When converting the instance to a string one of the following should print:

  • Result!(...) - if Result is undefined
  • Ok!(...)(...) - if Result is Ok
  • Err!(...)(...) - if Result is Err

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 and Optional). These will be worked upon when the time for it comes. Some key changes to this attempt are the removal of Ok and Err 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 as Result, Option, Either. The Rust example for this new type is:

enum Result<T, U>
{
	Ok(T),
	Err(U)
}

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 and union which will hold the instances containing the values of such types. Every type in D has an init 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 to enum and it's .init is the first field. Result will follow the same standard and have it's .init state defined as Ok and the value initialized to it's .init as well, instead of having an undefined state as proposed before.

struct Result(OkT,ErrT)
{
	...
private:
	union Payload
	{
		ok _ok = ok.init;
		err _err;
	}

	struct ok {
		...
		static if (!is(OkT == void)) OkT _handle;
	}

	struct err {
		...
		static if (!is(ErrT == void)) ErrT _handle;
	}

	enum State { Ok, Err, }

	State _state = State.Ok;
	Payload _payload;
}

Both ok and err 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 with ok and err to obtain it's values, such ends must be obtain through Result's unwrap, expect, unwrapErr and expectErr 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 the Result type! If a function returning a Result explicitly, meaning it's not an auto return, then the helpers Ok, Err and makeResult can be used. Both Ok and Err are template functions which use as it's parameters __FUNCTION__ and __MODULE__ to obtain it's result type making it possible to construct a Result 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 returning Result declared inside another function, object, type, won't possible to use these. To solve such an issue the makeResult mixin template is provided. Once again, the function must have it's return type explicitly declared! Any other function returning a Result not following these rules will have to either use the static initialize Result functions or the same helpers as before ok and err.

Can use Ok and Err directly:

module some.module.path;

Result!(int, string) func(int a)
{
	if (a > 0) return Ok(3);
	else return Err("error!");
}

Result!(void, string) func(int a)
{
	if (a > 0) return Ok();
	else return Err("error!");
}

With makeResult mixin template:

Result!(int, string) func(int a)
{
	mixin makeResult;
	if (a > 0) return Ok(3);
	else return Err("error!");
}

Result!(void, string) func(int a)
{
	mixin makeResult;
	if (a > 0) return Ok();
	else return Err("error!");
}

Using ok and err helpers:

auto func(int a)
{
	if (a > 0) return ok!string(3);
	else return err!int("error!");
}

auto func(int a)
{
	if (a > 0) return ok!string();
	else return err!void("error!");
}

Using the static initialize functions:

auto func(int a)
{
	if (a > 0) return Result!(int,string).Ok(3);
	else return Result!(int,string).Err("error!");
}

auto func(int a)
{
	if (a > 0) return Result!(void,string).Ok();
	else return Result!(void,string).Err("error!");
}

Extra

This second approach comes with a concept implementation of Result. It also brings the implementation
of unwrap and expect for both Ok and Err types.

void main()
{
	assert(3 == ok!string(3).unwrap);
	assert(5 == err!void(5).unwrapErr);
}

Both return the OkT or ErrT type if valid and assert if invalid; these assertions will work in -release mode.

Corrections

After a few head scratches with Ok and Err 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 abstract Result.

So with these changes, this will work now:

module my.path;

void main()
{
	Result!(int,string) test(int a)
	{
		if (a > 0) return Ok(a);
		else return Err("error");
	}

	assert(test(3).isOk);
}

These won't work:

module my.path;

void main()
{
	struct Foo {};
	Result!(Foo,string) test()
	{
		if (a > 0) return Ok(Foo());
		else return Err("error");
	}
}
module my.path;
import some.other : Type;

void main()
{
	Result!(Type,string) test()
	{
		if (a > 0) return Ok(Type());
		else return Err("error");
	}
}

Both above will keep working with makeResult mixin template.

@iK4tsu iK4tsu added enhancement New feature or request core TIP labels Feb 10, 2021
@rsubtil
Copy link

rsubtil commented Feb 11, 2021

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 ok and Ok for two different things could be very confusing, as they have different return values. But, maybe you could get away with using the same type twice?

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 ok and err is not used, and in theory can be anything; if that is correct, I'd say it might as well be the type of the thing that's coming afterwards. This would make code less confusing to both read and write, but the problem doesn't truly go away either. But I'm assuming there's no way to implement this feature without a compromise anyways.

Also I agree with most proposed utility functions to handle Results, although I'm not seeing the usefulness of some:

  • expect and unwrap look to do the same thing; halt program execution if Result is an Err, with expect accepting a message to present, ala assert style. But in that case, I don't see why a simple assert("This should work", r.unwrap()); wouldn't be enough for this.
  • What is the purpose of flaten? Even if I have nested Results, I'm only interested in the final, top-level Result; wouldn't using all the other functions (isOK(), isErr(), has(), etc...) already provide the same value? (in other words, isn't result.flaten.isOK() == result.isOK() always true?)

@iK4tsu
Copy link
Contributor Author

iK4tsu commented Feb 11, 2021

However using both ok and Ok for two different things could be very confusing, as they have different return values.

Ok is the type itself. This type is not supposed to be used outside Result unless it's used for instantiating one or for comparison. The helper ok says that it returns an Ok Result. While the Result type is one of Ok or Err not the same applies to Ok. A Result may be an Ok, however an Ok is not a Result. So ok is directed to Ok but not directly. Using ok is the same as doing:

...
return immutable(Result!(<typeA>,<typeB>)(Ok!(typeA)));

I'd say it might as well be the type of the thing that's coming afterwards.

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
}

expect and unwrap look to do the same thing; halt program execution if Result is an Err, with expect accepting a message to present, ala assert style. But in that case, I don't see why a simple assert("This should work", r.unwrap()); wouldn't be enough for this.

Yes. But that's not usefulness of these functions. Both unwrap and expect assert if the Result is Err with the difference that unwrap asserts with Err's value and expect assert with a message following the Err's value. The useful feature is that both return OkT. This make chaining simpler as well as making the code cleaner. Other thing is that asserting or throwing shouldn't be the purposed of this, the objective is to avoid such endings. Look at this case for example:

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 unwrapOr and unwrapOrDefault which would take an OkT value and return it if Result was Err. Yes I did not mention such functions.

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 Results the you could filter them as well.

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

What is the purpose of flaten? Even if I have nested Results, I'm only interested in the final, top-level Result; wouldn't using all the other functions (isOK(), isErr(), has(), etc...) already provide the same value? (in other words, isn't result.flaten.isOK() == result.isOK() always true?)

No, not all functions would return the same. Although isOk and isErr would return the same output, functions like has, unwrap, expect, etc, would not.

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

@ljmf00
Copy link
Member

ljmf00 commented Feb 11, 2021

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 immutable qualifier on Result state and on the return value of the helper functions.

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.
We could also have two versions of getters. getOk returns an optional, but if the user gives a default by using the same name or a more descriptive name like getOkOr(value) we could get T instead of Optional!T.

Some final thoughts:

  • We could name the payload union to do something like payload.ok instead of okpayload.

@ljmf00 ljmf00 changed the title Result type TIP: Result type Feb 11, 2021
@iK4tsu
Copy link
Contributor Author

iK4tsu commented Feb 11, 2021

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); }

Yes the second function in the first example is wrong, should be err. Your suggestion, as explained above, does not work. You're returning a different type from the function's ReturnType.

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.

Another thing I'm against this implementation is the enforcement of the immutable qualifier on Result state and on the return value of the helper functions.
We shouldn't enforce the usage of immutable storage. The user should choose the qualifiers for its own data.

The State doesn't have anything to do with the payload. Once a Result is established, then it stays like that. The objective of this feature is to be used as a ReturnValue not anything else. I'm only enforcing immutability on the Result itself, not it's content. If you want to change to content, either you construct a new Result with the new value, or you extract it from Result to a new variable and work that way. Result should only hold something and not have the ability to let the user manipulate its contents. You should never be able to have an Ok Result and then assign an Err to it! Once again if something like that happens just construct another Result. Some functions to construct another Result are in my mind.

  • map which would transform Result(T,ErrT) --> Result(U,ErrT)
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);
  • mapError
  • mapOr
  • mapOrElse

These are future functions which will enhance Result but can't be implemented at the moment. And since you brought up Optional (which isn't a valid proposal, I'll get there), let me follow the same route and show an example with match.

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
*/

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:

Once again we can have Result immutability with this.

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.
We could also have two versions of getters. getOk returns an optional, but if the user gives a default by using the same name or a more descriptive name like getOkOr(value) we could get T instead of Optional!T.

Yes they are. Optional is a concept which would be introduced in Taurus but right now, it isn't implement, that for, we cannot enforce it as a dependency. Optional is another theme/feature, and this one should function 100% independently from it. The Additional functions have options to get such values, unwrap, expect, and as mentioned above we could implement unwrapOr and unwrapOrDefault. So if you want to get OkT from an Ok Result safely then you would do:

if (res.isOk()) someval = res.unwrap();

Right now we don't have manners to filter or map or pull any fancy ways to get the value. It has to be this way. Optional conversation is valid when it's implementation arrives. But you can't decide what is mandatory with non existent dependencies at the moment.

@rsubtil
Copy link

rsubtil commented Feb 13, 2021

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.

Yes, I assumed that the type you specify after ok/err could be anything, but from the example you posted above, I now realize it's not possible:

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 Ok/Err types to compare any Results).

Also I noticed that the type for the error is a string in all your examples; do you wish to enforce this, or would the user be able to use another type if he wishes to? Because if you intend for it to always be a string, you could probably simplify the definition of Result and implicitly solve this issue.

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.

@iK4tsu
Copy link
Contributor Author

iK4tsu commented Feb 13, 2021

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 Ok/Err types to compare any Results).

I agree with this. Internally we should try to specify almost all the ReturnTypes so it's much easier to understand the output. If we say what type the function returns Result(X,Y) then with some basic and raw assumptions it's much more easier to figure out what each ok and err do even if not familiar with the lib or the concept. But once again lets hope noreturn gets accepted quickly and its usage applies to this case.

Also I noticed that the type for the error is a string in all your examples; do you wish to enforce this, or would the user be able to use another type if he wishes to? Because if you intend for it to always be a string, you could probably simplify the definition of Result and implicitly solve this issue.

I used string, but it's not mandatory. Any type will do. I opted for this because it's easier to understand the concept, and it will be a common error type. Others are void, enum. Each module will have a set of errors grouped in an enum and that will be the most used type internally. Instead of exception hierarchies or singled out exceptions we would replace it with enums.

@iK4tsu iK4tsu linked a pull request Mar 4, 2021 that will close this issue
@iK4tsu
Copy link
Contributor Author

iK4tsu commented Mar 4, 2021

@ljmf00 @ev1lbl0w updated proposal.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
core enhancement New feature or request TIP
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants