Photo by Cris DiNoto on Unsplash
While developing canister smart contracts for Juno in Rust, I must admit that I’m not yet prominent at writing code in this programming language. As a matter of fact, I even consider myself a noob. That’s why when I have to use new patterns, it sometimes took me a bit of time to figure out how. This was exactly the case when I was looking to pass an async function as a callback parameter to another function.
As there is no better documentation for my future self than blogging, let me share the solution I discovered during this intriguing process.
Solution
My interest in this solution was sparked by the need to refactor my code base and implement a pattern where an existing function would transform into a factory, with certain parts of it becoming dynamic while retaining its asynchronous characteristic.
To schematize this idea in this article, let’s envision two distinct functions: an incrementor and a square function, both accepting the same parameters and returning the same result.
type MyParam = u64;
type MyResult = u64;
async fn inc(
value: MyParam,
) -> MyResult {
value + 1
}
async fn square(
value: MyParam,
) -> MyResult {
value.pow(2)
}
Now that the functions share a common trait, we can proceed to implement the function that accepts them as parameters.
To be able to pass a function or closure as a parameter of another function, we need one of the following traits (source):
FnOnce are functions that can be called once
FnMut are functions that can be called if they have &mut access to their environment
Fn are functions that can be called if they only have & access to their environment
Furthermore, due to the asynchronous nature of the function we aim to pass, it should be specified that it returns a Future
. A future represents an asynchronous computation obtained by using async
.
Finally, while it could be inlined, for readability reasons, I found it convenient to use where
constraints.
use std::future::Future;
async fn execute<F, Fut>(
f: F,
value: MyParam,
) -> MyResult
where
F: FnOnce(MyParam) -> Fut,
Fut: Future<Output = MyResult>,
{
f(value).await
}
You might find the above pretty obvious if you are slightly more experienced than me in Rust, but believe me, it wasn’t that straightforward to implement such a pattern within an existing codebase because, of course, I brute-forced the implementation in my project instead of trying it first in a sample repo. 🤪
Anyway, with both the parameters and the function that accepts them ready, we can showcase their execution by calling them in a main function, for example.
#[tokio::main]
async fn main() {
let value1 = execute(inc, 1).await;
let value2 = execute(square, 5).await;
println!("{} {}", value1, value2);
}
Summary
In case you would like to run the snippet in the Rust Playground, here’s the entire code above, all together:
use std::future::Future;
type MyParam = u64;
type MyResult = u64;
async fn inc(
value: MyParam,
) -> MyResult {
value + 1
}
async fn square(
value: MyParam,
) -> MyResult {
value.pow(2)
}
async fn execute<F, Fut>(
f: F,
value: MyParam,
) -> MyResult
where
F: FnOnce(MyParam) -> Fut,
Fut: Future<Output = MyResult>,
{
f(value).await
}
#[tokio::main]
async fn main() {
let value1 = execute(inc, 1).await;
let value2 = execute(square, 5).await;
println!("{} {}", value1, value2);
}
Just passing a function
You might be looking as well how to pass a synchronous function as parameter. This is also possible. We can adapt the previous solution by removing the async
nature and leveraging the dyn
keyword to highlight that calls to methods on the associated Trait are dynamically dispatched.
type MyParam = u64;
type MyResult = u64;
fn inc(
value: MyParam,
) -> MyResult {
value + 1
}
fn square(
value: MyParam,
) -> MyResult {
value.pow(2)
}
fn execute(
f: &dyn Fn(MyParam) -> MyResult,
value: MyParam,
) -> MyResult
{
f(value)
}
fn main() {
let value1 = execute(&inc, 1);
let value2 = execute(&square, 5);
println!("{} {}", value1, value2);
}
Thank you for reading! Follow me on Twitter for more exciting coding content.
David