6

When using trio and nursery objects, how do you capture any value that was returned from a method?

Take this example from the trio website:

async def append_fruits():
    fruits = []
    fruits.append("Apple")
    fruits.append("Orange")
    return fruits

async def numbers():
    numbers = []
    numbers.append(1)
    numbers.append(2)
    return numbers

async def parent():
    async with trio.open_nursery() as nursery:
        nursery.start_soon(append_fruits)
        nursery.start_soon(numbers)

I modified it so that each method returns a list. How would you capture the return value so that I could print them?

2 Answers2

9

Currently, there is no built-in mechanism for this. Mostly because we haven't figured out how we would even want it to work, so if you have some suggestions that would be helpful :-).

The thing is, with regular functions, there's exactly one obvious place to access the return value – the caller is waiting, so you hand them the return value, done. With concurrent functions, the caller isn't waiting, so you also need some way to specify where to return it to, when to return it, if there are multiple functions you have to keep track of which one is returning a value, and so on. It's not as simple a concept.

What do you want to do with the return values? Do you want to, say, print them immediately when each function returns? In that case the simplest thing is to do it directly from the tasks:

async def print_fruits():
    print(await fruits())

async def print_numbers():
    print(await numbers())

async with trio.open_nursery() as nursery:
    nursery.start_soon(print_fruits)
    nursery.start_soon(print_numbers)

You could even factor this into a helper function:

async def call_then_print(fn):
    print(await fn())

async with trio.open_nursery() as nursery:
    nursery.start_soon(call_then_print, fruits)
    nursery.start_soon(call_then_print, numbers)

Or maybe you want to put them in a data structure to look at later?

results = {}

async def store_fruits_in_results_dict():
    results["fruits"] = await fruits()

async def store_numbers_in_results_dict():
    results["numbers"] = await numbers()

async with trio.open_nursery() as nursery:
    nursery.start_soon(store_fruits_in_results_dict)
    nursery.start_soon(store_numbers_in_results_dict)

# This is after the nursery block, so we know that the dict is fully filled in:
print(results["fruits"])
print(results["numbers"])

You can imagine fancier versions of those too – for example, sometimes when you run a lot of tasks in parallel you want to capture exceptions, not just return values, so that some tasks can still succeed even if some of them fail. For that you can use a try/except around each individual function, or the outcome library. Or when each operation finishes you could put its return value into a trio.Queue, so that another task can process the results as they're finished. But hopefully this gives you a good starting point :-)

Nathaniel J. Smith
  • 9,038
  • 4
  • 35
  • 46
  • Any ETA yet on when you will have figured this out? How you want it to work I mean? Because having a simple future that'd be guaranteed to be filled after the nursery block as a return value of a modified `start_soon()` would be completely sufficient for a start and a huge boon to the functionality of trio. – Christian Jan 27 '20 at 11:19
  • 1
    +1, some kind of simple `trio.Future` object would be very useful here. I guess essentially what `outcome.Outcome` is. – shadowtalker May 11 '20 at 18:56
  • Is there an open issue about this, by any chance? I think i would expect each call of `start_soon` to return a future... By the way, it looks to me that nurseries are just nested event loops with extra guaranties. – Alexey Jul 31 '20 at 19:47
1

In this case, simply create the arrays in the parent and pass each to the child that needs it.

More generally, pass an object to the tasks; they can set an attribute on it. You might also add an Event so that the parent can wait for the results to be available.

Matthias Urlichs
  • 1,789
  • 14
  • 26