Introduction
A question that has been asked multiple times on the forum is whether it is possible to define async functions that return a parametric type such as, for example,
func f<T>(..) : async T { .. };
The answer, both for async T
and async* T
is no. It is not currently possible in Motoko and will not be possible for a while. We describe here in this post the workaround that we have successfully used whenever we needed async generics. It occurred for us when writing test frameworks. If you have encountered this question as well then please share your use cases for async generics in the comments.
Example
As an example, say that we would like to write a module with the following function in it. As is typical for modules that serve as libraries (packages), the function returns async*
.
module M {
public func f<T>(x : T) : async* [T] {
// some code here
[x, x]
};
};
Then we would like to use the module like this:
import M "<path to M>";
let f = M.f<Nat>;
await* f(0); // => [0, 0]
The above code will fail with the error “async has non-shared content type [T]”.
Workaround
The idea of the workaround is to divide f
into two functions
func f_call(x : T) : async* () { .. };
func f_result(x : T) : [T] { .. };
which have to be called in immediate succession. The first function performs the actual code from f
and stores the result, the second function reads out the result and returns it. The first function is asynchronous but has a non-parametric return type. The second function has a parametric return type but is synchronous.
Helper package
In code this looks as follows. The workaround starts by importing the package generics
from mops. It contains the following very small module which provides a class to buffer the result.
module Generics {
public class Buf<T>() {
var result_ : ?T = null;
public func set(x : T) {
result_ := ?x;
};
public func get() : T {
switch (result_) {
case (?x) {
result_ := null;
x;
};
case (null) Debug.trap("no result present");
};
};
};
};
Writing your library
Then we write our own module like this:
import Generics "mo:generics";
module M {
public class f_<T>() {
let buf = Generics.Buf<[T]>();
public func call(x : T) : async* () {
// some code here
buf.set([x, x]);
};
public func result() : [T] = buf.get();
};
};
As we can see, the module comes with a little bit of overhead. The function f
has been wrapped in a class f_
. The original function f
is now the class method call
. And this is the code that has been wrapped around it:
public class f_<T>() {
let buf = Generics.Buf<[T]>();
..
public func result() : [T] = buf.get();
};
The above 4 lines of boilerplate need to be repeated for each public asynchronous function exported by the module. A name has to be chosen for the class. For consistency, we recommend to name the class f_
when the function name is f
. The type parameters and the return type, here [T]
, need to be substituted. The function and class can have more than one type parameter.
Using the library
Now say we want to use the module with type Nat
substituted for the type parameter T
. Then the code looks like this:
import M "..";
let f = M.f_<Nat>() |> (
func(x : Nat) : async* [Nat] {
await* _.call(x);
_.result();
}
);
await* f(0);
Again, we see a little bit of overhead. Originally, we wanted to define f like this:
let f = M.f<Nat>;
Instead, we have to instantiate the class f_
and then define the desired function f
by wrapping around that class. So M.f<Nat>
has become the expression
M.f_<Nat>() |> (
func(x : Nat) : async* [Nat] {
await* _.call(x);
_.result();
}
);
The boilerplate code can be copy-pasted for each function that the application code wants to use. All we have to adjust is the function signature in the second line and the the name f
and type in the first line.
Safety
If the above wrapper is consistently used then there cannot be any problems. However, if you break up the wrapper and separate out the calls to f_.call
and f_.result
then it is possible to make mistakes such as calling f_call
twice in a row or f_.result
twice in a row. However, such mistakes are caught by the helper class and lead to a trap.
Links
You can try out this code in the live playgound here.
References: