Result

A Result represents the result of an operation that can fail. It's generic over two types: a success type and a failure type (called Ok and Err). In order to maintain type safety, the contents of the Result are private. They're accessed through lambdas with the Match(), IfOk() or IfErr() methods.

In proper computer science terms: Result is a sum type, with simulated pattern matching through the Match(), IfOk()and IfErr() methods.

The Match() method, for example, works like this:

IMVU.Result<int, string> result = FunctionThatMightFail();
result.Match(
ok => Success(ok),
err => Debug.Log(err)
);

The Match() method will call the appropriate lambda, depending on which type of value the Result actually stores. This means you can't accidentally access the wrong value.

There is also a variant of the Match() method which returns a value, like so:

IMVU.Result<int, string> result = FunctionThatMightFail();
int output = result.Match(
ok => ok,
err => { Debug.Log(err); return 0; }
);

Note that the return types of the lambdas passed to Match() must be the same, because Match() itself can only have one return type.

If you only care about one branch of the Match() you can use IfOk() or IfErr() instead. The first is given a lambda that will only be called if there is an Ok value, and the second a lambda that will only be called if there is an Err value. For example:

IMVU.Result<int, string> result = FunctionThatMightFail();
result.IfOk(ok => Success(ok));
result.IfErr(err => Debug.Log(err));

Like with the Match() method, these have variants that will return a value. However, since they're not complete (that is, there isn't a lambda for every case), they also need to provide a default value, which will be returned if the lambda isn't called. Of course, the type of the default value must be the same as the return type of the lambda. For example:

IMVU.Result<int, string> result = FunctionThatMightFail();
int output = result.IfOk(ok => ok, 0);

You might be wondering why we would use this to return errors, instead of just throwing exceptions. The reason is that exceptions greatly increase the code size of Unity's WebGL builds. When we turn off exceptions we still needed a way to propogate errors. Having adopted Result to do this, we found it a very useful pattern.

The Result class also contains IsOk() and IsErr(), but it's recommended that these not be used outside of tests.

If you find yourself wanting to generate a Result, it has two static factory functions, one that takes an Ok and one that takes an Err. Examples of creating a Result:

var ok = IMVU.Result<int, string>.Ok(42);
var err = IMVU.Result<int, string>.Err("Error!");

Promise

A Promise represents a value that may not exist yet. This is designed to function very much like a JavaScript promise, even mirroring its API as closely as possible. Think of it as an asynchronous Result whose success and failure types are called Accept and Reject. The difference is that when you give it lambdas to be called for the different types, those lambdas won't be called until the promise is resolved. If the promise is already resolved, they'll be called immediately. Because of this, we refer to these lambdas as callbacks.

If you want to give the promise just a success callback, you can use the Then() method. For example:

AsyncFunction().Then(
accept => Success()
);

The Then() method also has a form that accepts both success and failure callbacks, like so:

AsyncFunction().Then(
accept => Success(),
reject => Debug.Log(reject)
);

If you only want an error callback, you can use the Catch() method. For example:

AsyncFunction().Catch(
reject => Debug.Log(reject)
);

Note that Then() and Catch() always return another promise. This allows you to chain promises, like so:

AsyncFunction().Then(
accept => AsyncFunction2()
).Then(
accept => AsyncFunction3()
).Catch(
reject => Debug.Log(reject)
);

Note that if any of the promises in this chain fail, they will fall through to the Catch() at the end. Successes will likewise fall through if there are multiple Catch() calls chained with a Then() at the end. The rules for what kind of promise is created are a bit complicated, but the results are generally fairly intuitive.

Callbacks must return one of three things:

  • Nothing
  • A Result
  • A Promise

If only an Accept callback is provided, then the Reject type of the new Promise is the same as the old Promise. This allows a Reject to cascade through to the next Reject callback. Likewise, if only a Reject callback is provided, then theAccept type of the new Promise is the same as the old Promise. This allows an Accept to cascade through to the nextAccept callback. If both callbacks are provided to Then(), then their return types must match, and that type returns the full type of the resulting Promise.

When nothing or a Result is returned, the resulting promise is resolved as soon as the callback completes. If a Promise is returned, then the resulting Promise is resolved whenever this promise resolves.

Promise also provides a function called Branch, which is useful when you want to do multiple things in one step of a promise chain, and if either of them fail you want to fall through to a common Catch. It's usage is as follows:

AsyncFunction().Then(
accept => AsyncFunction2(accept)
).Branch(
accept => ThingThatMayFail1(accept),
accept => ThingThatMayFail2(accept)
).Catch(
reject => Debug.Log(reject)
);

Both of the lambdas in Branch will be called with the same value, and if either of them returns a value that would be translated as a Reject, then the Promise returned by Branch uses that same Reject, causing it to hit the Catch.

Finally, Promise provides a static function called All. This takes a list of Promises of the same type, and returns aPromise of a list of that type. If any one of the passed-in Promises is rejected, the new Promise is rejected with the first rejection value it gets. If they're all accepted, the new Promise is accepted with a List of all their values. For instance, you can take a List<Promise<int, string>> and turn it into a Promise<List<int>, string>, like so:

Promise<int, string>.All(promiseList);

If this all seems complicated, don't worry too much about it. It works out to be pretty intuitive. See theFriendCollection class used by the Friends demo for an example of all of these features working together.