GPT teaches me how to make my logic sync and async at the same time with trampolines
Tags: javascript, asynchronous, programming, llm
Published: 2024-04-11 22:03:48

How can I have one implementation of some complex logic that can call out to sync or async hooks where the async one needs to be properly awaited each time?

I told ChatGPT to teach me that. Below is our (heavily/extensively/drastically/substantially/profoundly redacted and fixed) chat log.

--- 
> in javascript, I have a function that's accepting a hook as input, runs a few synchronous operations, calls the hook and runs a few synchronous operations on the results. You can represent the operations as simple mathematical operations for the examples. I need an implementation that shares only one implementation of the operations but has two versions of the function - one that's asynchronous and accepts an async hook and awaits it, the other fully synchronous and accepting a synchronous hook.
> I was told to use generators and to build a trampoline. I need an example of what that could look like with an explanation of the trampoline.

*//the prompt was longer and contained snippets from tips I got from humans*

--- 

To implement the solution you're looking for, we'll start by creating a generic function that can perform operations both synchronously and asynchronously, depending on the hook it receives. This function will be a generator, allowing us to use `yield` to handle both synchronous and asynchronous operations in a unified way.

The key concept here is the use of a "trampoline" function. A trampoline in this context is a mechanism that iteratively executes generator functions without growing the call stack. This is crucial for handling potentially deep or infinite recursion without causing a stack overflow. For asynchronous operations, the trampoline will use promises to handle the asynchronous yields.

Let's start with an example of the operations and the generator function:

```javascript
// Generator function accepting a hook
function* operationsWithHook(hook, input) {
  let result = input * 2; // First operation
  result = yield hook(result); // Call the hook, which can be sync or async
  result = result * 2; // Operation on the hook's result
  return result;
}
```

Now, let's implement two trampoline functions, one for synchronous execution and another for asynchronous execution:

```javascript
// Synchronous trampoline
function syncTrampoline(generatorFunc, ...args) {
  let iterator = generatorFunc(...args);
  let result = iterator.next();
  while (!result.done) {
    result = iterator.next(result.value);
  }
  return result.value;
}


// Asynchronous trampoline
async function asyncTrampoline(generatorFunc, ...args) {
  let iterator = generatorFunc(...args);
  let result = iterator.next();
  while (!result.done) {
    const val = await result.value
    result = iterator.next(val);
  }
  return result.value;
}
```

With these trampolines, you can now execute your generator function either synchronously or asynchronously, depending on the nature of the hook you're passing. Here's how you would use them:

```javascript
// Synchronous hook
function syncHook(x) {
  return x + 10;
}

// Asynchronous hook
async function asyncHook(x) {
  await new Promise(resolve => setTimeout(resolve, 101));
  return x + 10;
}


// Execute synchronously
const syncResult = syncTrampoline(operationsWithHook, syncHook, 5);
console.log('done:',syncResult); // Output will depend on operations and hook

// Execute asynchronously
asyncTrampoline(operationsWithHook, asyncHook, 5).then((result) => {
  console.log('done:',result); // Output will depend on operations and hook
});
```

This approach allows you to maintain a single implementation of your operations and seamlessly switch between synchronous and asynchronous execution by choosing the appropriate trampoline function. The generator function acts as a bridge, using `yield` to pause execution at the hook call, which can then be handled either synchronously or asynchronously by the trampoline.

----

> One more complication. operationsWithHook calls itself recursively if the current result is below N. How would I wire that up?

---

To handle recursion within your `operationsWithHook` generator function, especially when deciding whether to recurse based on the result of calling the hook, you can use `yield*` to delegate to another generator function. This allows the current generator to yield values from another generator, effectively creating a recursive loop.

Let's modify the `operationsWithHook` function to include a condition for recursion if the result of calling the hook and multiplying it remains below constant `N`.

```javascript
// Modified generator function with recursion
N=1000
function* operationsWithHook(hook, input) {
  let result = input * 2; // First operation
  result = yield hook(result); // Call the hook, which can be sync or async
  result = result * 2; // Operation on the hook's result
  // Check if the result is divisible by N, and if so, recurse
  if (result < N) {
    result = yield* operationsWithHook(hook, result); // Recurse with yield*
  }
  return result;
}
```

The `yield*` expression is used for delegating to another generator function, in this case, `operationsWithHook` itself, which allows for recursion. This works seamlessly with both synchronous and asynchronous hooks because the trampoline functions are designed to handle the iteration over generator functions, including when they delegate to other generators or themselves.

The trampoline functions (`syncTrampoline` and `asyncTrampoline`) do not need to be modified to support this recursion. They will continue to work as before, driving the generator to completion, whether it involves recursion or not.

The same execution code as before can be used to execute the generator function either synchronously or asynchronously, depending on the nature of the hook you're passing.

This approach allows the `operationsWithHook` function to call itself recursively under certain conditions, with the trampoline functions handling the execution flow, whether synchronous or asynchronous.



## Complete example file

> I put it together and added logging so you can see the output and understand the flow.

```js


N=1000
function* operationsWithHook(hook, input) {
  let result = input * 2; // First operation
  result = yield hook(result); // Call the hook, which can be sync or async
  result = result * 2; // Operation on the hook's result
  console.log(Date.now(), [input, result])
  // Check if the result is divisible by N, and if so, recurse
  if (result < N) {
    result = yield* operationsWithHook(hook, result); // Recurse with yield*
  }
  return result;
}

// Synchronous trampoline
function syncTrampoline(generatorFunc, ...args) {
  let iterator = generatorFunc(...args);
  let result = iterator.next();
  while (!result.done) {
    result = iterator.next(result.value);
  }
  return result.value;
}

// Asynchronous trampoline
async function asyncTrampoline(generatorFunc, ...args) {
  let iterator = generatorFunc(...args);
  let result = iterator.next();
  while (!result.done) {
    console.log(Date.now(),result.value)
    const val = await result.value
    console.log(Date.now(), ' ->',result.value)
    result = iterator.next(val);
  }
  return result.value;
}
// Synchronous hook
function syncHook(x) {
  return x + 10;
}

// Asynchronous hook
async function asyncHook(x) {
  await new Promise(resolve => setTimeout(resolve, 101));
  return x + 10;
}

// Execute synchronously
const syncResult = syncTrampoline(operationsWithHook, syncHook, 5);
console.log(syncResult); // Output will depend on operations and hook

// Execute asynchronously
asyncTrampoline(operationsWithHook, asyncHook, 5).then((result) => {
  console.log(result); // Output will depend on operations and hook
});
```

## Disclaimer and license

Note: I have modified the response from our benevolent AI overlord to my liking in a bunch of places, so now it's copyrightable, which allows me to release it under [CC BY-NC-SA](https://creativecommons.org/licenses/by-nc-sa/4.0/)

I mean, I had to modify it because asyncTrampoline was broken, it's not just for copyright :D