Actually, the main point to consider this for is if one wants to pass the entire return dictionary of a function as the input to another function. In that case, each individual key-value pair is resolved to an individual port, but also the entire dictionary contents are wrapped using __result__.
In the case that a function doesn't return a dictionary, should the return value of a function always map to a special sourcePort, e.g., __result__?
-> To pass it as an input to the next task requires a name. To make it beginner-friendly, we do this automatically.
What if the function does return a dictionary, we automatically resolve the keys to individual sourceHandles (as suggested by the PWD JSON snippets we have). Then, should we also return the full dictionary wrapped in __result__, such that this:
def test(x, y):
return {"prod": x * y, "div": x / y}
yields the sourceHandles: prod, div, and __result__. Or, as we don't really have types, what if we don't know that the return of the function is a dictionary, we still wrap it in __result__? What if the user then defines a function that also contains the key __result__:
def test(x, y):
return {"prod": x * y, "div": x / y, "__result__": True}
This will except at runtime, as reserved keyword being used. Function might be written by somebody else and we don't have control. Thus, use more convoluted dunder-result. In the workflow definition, one could have an additional key global_workflow_output, such that it can be automatically renamed.
For comparison, when decorating functions with WorkGraph decorators, the mapping of function outputs to ports is handled in the following way:
- Function doesn't return a dictionary, but some other data type → return value is wrapped in a dictionary with the default key "result".
- Function does return a dictionary, but output names are not explicitly given via the decorator → The whole dictionary is wrapped in another dictionary with the default key "result". In both cases, the function is only assigned a single output port.
- Function does return a dictionary, and the outputs are explicitly provided in the decorator, e.g.,
@task(outputs=["prod", "div"]) → one output port is generated for each explicitly provided output, with the value returned contained in the dictionary at the respective key being the data object that is passed via the port, where the port name is the key of the dictionary (as defined also explicitly in the decorator).
Actually, the main point to consider this for is if one wants to pass the entire return dictionary of a function as the input to another function. In that case, each individual key-value pair is resolved to an individual port, but also the entire dictionary contents are wrapped using
__result__.In the case that a function doesn't return a dictionary, should the return value of a function always map to a special
sourcePort, e.g.,__result__?-> To pass it as an input to the next task requires a name. To make it beginner-friendly, we do this automatically.
What if the function does return a dictionary, we automatically resolve the keys to individual
sourceHandles (as suggested by the PWD JSON snippets we have). Then, should we also return the full dictionary wrapped in__result__, such that this:yields the
sourceHandles:prod,div, and__result__. Or, as we don't really have types, what if we don't know that the return of the function is a dictionary, we still wrap it in__result__? What if the user then defines a function that also contains the key__result__:This will except at runtime, as reserved keyword being used. Function might be written by somebody else and we don't have control. Thus, use more convoluted dunder-result. In the workflow definition, one could have an additional key
global_workflow_output, such that it can be automatically renamed.For comparison, when decorating functions with WorkGraph decorators, the mapping of function outputs to ports is handled in the following way:
@task(outputs=["prod", "div"])→ one output port is generated for each explicitly provided output, with the value returned contained in the dictionary at the respective key being the data object that is passed via the port, where the port name is the key of the dictionary (as defined also explicitly in the decorator).