Skip to content

TIP: Result type #6

@iK4tsu

Description

@iK4tsu

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions