tsdoc icon indicating copy to clipboard operation
tsdoc copied to clipboard

Add @yields tag

Open ghost opened this issue 4 years ago • 12 comments

This is supported by JSDoc to describe generators. This is useful because generators can both return and yield things.

ghost avatar May 04 '20 20:05 ghost

Here's an example of a TypeScript generator:

/**
 * - yields numbers
 * - returns strings
 * - can be passed in booleans
 */
function* counter(): Generator<number, string, boolean> {
    let i: number = 0;
    while (true) {
        let b: boolean = yield i++;
        if (b) {
            break;
        }
    }
    return "done!";
}

var iter = counter();
var curr = iter.next()
while (!curr.done) {
    console.log(curr.value);
    curr = iter.next(curr.value === 5)
}
console.log(curr.value.toUpperCase());

// prints:
//
// 0
// 1
// 2
// 3
// 4
// 5
// DONE!

The Generator<number, string, boolean> type annotation already communicates that this function is a generator. It also describes the 3 different types associated with the iterator. It might be useful for the documentation website to present individual descriptions of the <number, string, boolean> components, but such a feature wouldn't be specific to generators.

[edited -- in my original reply I confused @yields with @generator]

octogonz avatar May 04 '20 21:05 octogonz

From the JSDoc manual, it seems the above API would get documented like this:

// JSDoc example
/**
 * @yields {number} increasing integer values; the final value is the text string `DONE!`
 */
function* counter();

With TSDoc, it seems like we can do it like this:

// TSDoc example
/**
 * @returns a generator producing a sequence of increasing integer values; 
 * the final value is the text string `DONE!`
 * @remarks
 * The generator's `next()` function accepts a `boolean` parameter;
 * if this parameter is not `true`, then the sequence ends.
 */
function* counter(): Generator<number, string, boolean>;

This seems reasonably clear using the existing TSDoc tags.

octogonz avatar May 04 '20 21:05 octogonz

Here's an example of a signature that may not be as immediately clear from the types:

/**
 * @param IDs for data
 * @yields data rows returned from the API
 * @returns IDs which did not return any results
 */
function callApiForData(ids: number[]): Generator<Data, number[]>;

Using the syntax you describe, this would IMHO be not as clear:

/**
 * @param ids - IDs for data
 * @returns a generator which yields the data rows from the API and returns the IDs which had no results
 */
function callApiForData(ids: number[]): Generator<Data, number[]>;

Having these separate parameters to begin with is why we don't document functions like:

/**
 * @param arguments - a simple string-string map, the key to set the value for, and the value to set at the key
 */
function setKey(obj: Partial<Record<string, string>>, key: string, value: string): void;

And instead document them like:

/**
 * @param obj - simple string-string map
 * @param key - the key to set the value for
 * @param value - the value to set at the key
 */
function setKey(obj: Partial<Record<string, string>>, key: string, value: string): void;

ghost avatar May 04 '20 21:05 ghost

I see, so you are proposing that @returns documents the final DONE! value from my example, whereas @yields documents the individual iterator results. Would there be a way to document the next() parameter?

octogonz avatar May 04 '20 23:05 octogonz

I see, so you are proposing that @returns documents the final DONE! value from my example, whereas @yields documents the individual iterator results.

BTW this does /not/ seem to be how the JSDoc tag works. The JSDoc manual seems to imply that @returns always documents the return value of the function (i.e. the generator object). It does NOT document the final value of the generator. From this manual page:

The @returns tag documents the value that a function returns. If you are documenting a generator function, use the @yields tag instead of this tag.

octogonz avatar May 04 '20 23:05 octogonz

Oh, huh, didn't realise that was how they documented it.

From my perspective, it still makes sense that the "final" value of the generator is part of the returns. You could technically say @returns a generator which returns ..., but to me that's like saying @returns a promise for ...; I tend to omit the latter as well because the async-ness is already clear from the type.

If we were to document the next parameter separately we could do something like @takes, or the @yields parameter could do double-duty for that:

/**
 * @param first - first number to double
 * @yields twice the first amount, then twice every amount passed to it
 */
function * doubler(first: number): Generator<number, void, number> {
    let curr = first;
    while (true) {
        curr = 2 * yield curr;
    }
}

ghost avatar May 05 '20 00:05 ghost

I agree with not using @returns at all for generators. We don't say for class constructors @returns a new instance of the class. That's just an inherit trait of their operation.

Here is an actual example of something I've developed recently and how I'd like to document it.

* addApps(...appConfigs: Array<AppConfig>): Generator<[boolean, AppConfig]> {
  // Validate given configs to make sure they're of the right structure at runtime
  // Check if the given set of apps would exceed the max limit, throw if true
  // while appConfigs is not 0, shift the top item and add it
    yield [this.addApp(config), config];
  // end while
}

I believe a solid way of documenting this is like so:

/**
* Add multiple application configs to the list of available ones.
* 
* @params appConfigs An application configuration to add.
* 
* @throws // A few of these
*
* @yield An array where index 0 is the boolean status of the add operation. Index 1 is the configuration for the operation.
*/

This documentation is very clear in what the developer should expect when using the API. The fact that it says @yields and is typed as a Generator should be all the information a consuming developer would need to know to know calling it will return a generator object.

Returning a generator is an explicit part of the language given the syntax. I'd rather not need to document that aspect in written documentation when with the AST generating documentation I can infer that and do something to call it out in a patterned way on the docs.

It seems like when generating docs saying "This is a generator function, 'Returns: Generator'" is plenty for that. The important part alone is clarifying the yield expectations and how developers can use your API in the main description.

I do feel as if something missing currently from structured docs is the next parameter and if your generator supports that. There is no guarantee that your next parameter takes the same type as your construction parameters. Perhaps there is room to clearly document that aspect to generators with a tag. Perhaps something like:

/**
* @next {Type if not explicit in code} Some description of what the parameter does if used.
* @yields Some tuple data.
*/

Garbee avatar Aug 24 '20 15:08 Garbee

Yeah, I agree with the discussion, which in summary essentially seems to be:

/**
 * @yields {IndividualTypeYielded} i.e., `yield value;`
 * @next {TypePassedInWithIteratorNextCall} I.e., `it.next(SomeObject)`
 * @returns {ResultThatWillAccompanyIteratorDone} i.e., `return value` in generator which iterator gets as `{value, done: true}`
 */

It is a pity that this approach might not have parity with how an async function probably should continue to use Promise in the return: @returns {Promise<SomeType>}, in case there were different classes. Actually, maybe the generator returns should look like: @returns {Generator<ValueTypeThatWillAccompanyIteratorDone>}, both for the sake of parity, and in case one wishes to reuse specific Generator types (I'm coming at this from more of a jsdoc angle than TS, but I imagine that may work with TS as well as being clear for regular JS).

brettz9 avatar Jan 21 '21 00:01 brettz9

Why do you need to specify the type using { }? Isn't it already captured in the TypeScript type signature for the function?

octogonz avatar Jan 21 '21 02:01 octogonz

@octogonz: I was indicating that for just these reasons:

  1. To quickly summarize what type of type it was
  2. To indicate the pattern that regular jsdoc would use.

brettz9 avatar Jan 21 '21 02:01 brettz9

I see. So based on this discussion, could someone propose a spec for @yields? (A concise statement of the allowed syntax variations, what they mean, and some usage examples.) Then we can add it.

octogonz avatar Mar 09 '21 06:03 octogonz

Before giving specific specs, might it be viable to allow an alternative to @returns {Generator<...>} which could define a new global template such as: @returns {GeneratorFunction<ReturnType>}.

While this would still require specifying something (i.e., GeneratorFunction) which is not the value actually accompanying the return, it has the following advantages:

  1. It is consistent with the likes of async functions whose return is not a Promise but whose @returns ought to be a Promise of a given type.
  2. The user would not need to specify the yield type (delegating that instead to @yields).

To keep things simple, this proposed GeneratorFunction global template need not support users specifying a type therein for incoming next() calls, but if the user wished to document this type, they could either add this with the proposed new @next tag or use the old, full signature, Generator<yieldType, returnType, nextType> (see this comment or https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-6.html#stricter-generators ).

brettz9 avatar Mar 10 '21 14:03 brettz9