Jsdoc: Support for curried functions

Created on 21 Oct 2016  ·  52Comments  ·  Source: jsdoc/jsdoc

I've not found any documentation about how curried functions should be documented. Lets asume we have the following function:

const push = browserHistory => route => browserHistory.push(route)

Is this a valid documentation?

/**
 * @param {Object} browserHistory 
 * @param {string} route
 */
const push = browserHistory => route => browserHistory.push(route)

Most helpful comment

Does anyone actually have a solution for this instead of philosophical debates about the need of currying?

All 52 comments

I'm looking for the same thing. One option would be something like the following:

/**
 * @param {Object} browserHistory
 * @returns {function} a function accepting a single route parameter
 * @param {string} route
 * @returns {boolean} whatever browserHistory.push(route) returns
*/
const push = browserHistory => route => browserHistory.push(route)

But I'd like to know whether there's an official way to do it.

My IDE complains about the previous option, but seems OK with this one:

/**
 * @param {Object} browserHistory - thing
 * @returns {function} a function that accepts a single route parameter
 */
const push = browserHistory =>
  /**
   * @param {string} route - thing
   * @returns {boolean} whatever browserHistory.push(route) returns
   */
  route => browserHistory.push(route)

Ugly, though.

Do you actually use curried functions in real-world code?

@minexew Yes, I do actually use curried functions and partial application in real-world code.

FYI:

  • lodash, one of the most dependend-upon libraries in npm, has partial and curry methods.
  • Underscore has a partial method.
  • Native JavaScript has Function.prototype.bind(), which can be used for partial application.
  • JQuery has proxy, which can be used for partial application.

I prefer Ramda and friends for my functional programming in JavaScript needs at the moment, but the new ES6 arrow functions also make it quite easy to brew your own curried functions and/or partial application in a nice, terse syntax (see previous posts in this discussion for an example).

All of those examples take in a "normal" function and currify it (or perform partial application). That's indeed often useful.

What I don't comprehend why anybody would want to make a function curried by default, in a way that doesn't allow supplying more than one argument per call.

What I don't comprehend why anybody would want to make a function curried by default, in a way that doesn't allow supplying more than one argument per call.

For the same reason they'd want to curry an existing function. It makes more sense when you get deeper into functional programming.

Furthermore, some libraries (Ramda is one) offer a hybrid style of currying that lets you pass in one or several arguments at a time, and only executes the original function after all of the arguments it needs have been supplied (for non-variadic functions with an arity <= 10).

The function in the original post in this discussion is curried in a manner that requires one to call it twice, passing in one argument each time.

With Ramda-style currying, you be able to choose between passing in both arguments in one call or each argument in its own separate call.

Also, using the example function in the first post, if you're usually passing one parameter at a time to your functions, I don't see the problem with writing a function that is curried by default and calling it like this when you want/need to pass both arguments at the same time:

push( history )( rt )

Does anyone actually have a solution for this instead of philosophical debates about the need of currying?

Does anyone actually have a solution for this instead of philosophical debates about the need of currying?

Agreed. Would love a solution for how to document this.

This one makes Visual Studio Code provide proper IntelliSense:

/**
 * @param {Object} browserHistory
 * @returns {function(string): boolean} a function accepting a single route parameter
*/
const push = browserHistory => route => browserHistory.push(route);

However, if you're using JSDoc annotations to leverage type checking, it won't do. To deal with TypeScript and its Parameter 'route' implicitly has 'any' type. message, you'd have to use inline types:

const push = browserHistory => (/** @type {string} */ route) => browserHistory.push(route);

Any news on this? Functional programming rocks in JS, makes everything more readable, understandable and testable, but the docs have to be ok too...

tried the following, but then IntelliJ doesn't help me anymore on the returned result:

/**
 * Create model searcher
 *
 * As url, give the base one, query params will be appended automatically.
 *
 * @param {string} url - Base url template string with `context` as param
 * @param {Object.<number, string>} errorKeys - Like {httpStatus: translationKey}
 * @param {function(DTO): OBJ} deserializer
 * @param {$http} $http
 * @param {translate} translate
 * @returns {function(*): ModelSearcher}
 * @template DTO, OBJ
 */
export function modelSearcher(url, errorKeys, deserializer, $http, translate) {

    /**
     * @callback ModelSearcher
     * @param {Object} criteria
     * @returns Rx.Observable.<OBJ[]>
     * @template CRITERIA
     */

    return context => criteria => Rx.Observable
        .fromPromise($http.get(`${template(url)({context})}${queryStringFrom(criteria)}`))
        .catch(throwRxError(translate(errorKeys[500])))
        .map(getResponseData(deserializer));
}

I'm looking for a solution to Ramda curry, I love it, but js docs is bad =/

Example:

import { curry, filter, startsWith } from 'ramda';

/**
 * Get valid langKey in langs or return defaultLangKey
 * @func
 * @param {[String]} langs allowed lang keys ['en', 'fr', 'pt']
 * @param {String} defaultLangKey default browser language key
 * @returns {String} valid langKey
 */
const getValidLangKey = curry((langs, defaultLangKey, langKey) => {
  const currentLangKey = filter(l => startsWith(l, langKey), langs);
  return currentLangKey[0] || defaultLangKey;
});

export default getValidLangKey;

After adding curry() I lost all the types and descriptions.

I hope someone finds a good solution.

@karol-majewski This solution provides best IntelliSense in VS Code:

curriedfun

@asistapl That's correct, support for JSDoc has gotten better over last year. I believe we can apply your solution to the original problem like this:

// @ts-check

/**
 * @typedef {object} BrowserHistory
 * @property {(route: string) => void} push
 */

/**
 * @param {BrowserHistory} history
 * @returns {(route: string) => void}
 */
const push = history => route => history.push(route);

Would love to help with this one. arrow functions support will be a great feature 😿

Support in one IDE is one thing, I would be interested in support for the HTML output — in a way that makes the intent clear. For WebStorm I just wrote

/**
 * @param {MyTape} firstParam
 * @returns {function(function(SHA256Hash):boolean):function(SHA256Hash):Promise.<string>}
 */

which is a monster line (function that returns a function that returns a function). also, the @param only is for the outermost parameter of the very first function. All others are declared in the @return tag, which gets very crowded.

The examples above look okay because there is only one level. It should work with 5 levels too (just to give a high(er) number).

Really look forward to seeing this happen!

@minexew

What I don't comprehend why anybody would want to make a function curried by default, in a way that doesn't allow supplying more than one argument per call.

Because the rest of the code uses the function in different ways? like:

const doSomethingWith = _curry((configParam, dataParam) => {});
const doSomethingWithConfigA = doSomethingWith('lala');

doSomethingWithConfigA(5);
doSomethingWith('hey', 9);

The difficult part of this is adding JSDoc for doSomethingWith...

In functional programming, it is often done to config your function first with the first x params, and then execute it with the rest, but that's not always the case.

What about point-free functions that don't have _explicit_ arguments declaration?

const { add } = require('ramda');

/**
*  Adds 10 to `x`.
*  @param {number} x Any number
*  @returns {number} The given `x` number plus 10.
*/
const sum10 = add(10);

☝️ This, unfortunately, doesn't work out of the box. The param and returns metadata are being stripped from generated docs. Any ideas if this is even possible currently?

@nfantone , it does work, but you have to add @function in the docblock

@dietergeerts Worked like a charm. Thanks a million for pointing it out!

Still kinda awkward, if you ask me. I get the idea of wanting to infer types or match written docs with actual params to lint/give feedback on potentially bad tags - but _removing them entirely_ from the output, by default, seems a bit forced, honestly.

Y'all are wonderful, but I gave up trying to figure out how to get all these workarounds to work (insane nesting, jsdoc or babylon failing to recognize the async keyword, etc). For those who love types, I feel for you.

So I built a Hindley-Milner documentation library where you can use Markdown. Hopefully it helps y'all.

https://github.com/JesterXL/hm-doc

@JesterXL The issue has only been open since October 2016. Be patient!

@joelnet We get what we pay for...

@JesterXL , like @joelnet says, be patient. Also keep in mind that there are features coming in JS regarding currying and partial application, which could change the implementation of this. (https://github.com/tc39/proposal-partial-application)

For what it's worth, I use currying a lot, through the lodash helper, and I document such functions as:

@curried
@function
@param {string} a
@param {string} b
@returns {string}

export const test = _curry((a, b) => a + b);

My IDE (WebStorm) is fine with this + it ends up in the API docs.

Do you actually use curried functions in real-world code?

Yep, I use them to create higher ordered components in React, and other various libraries for React use them including ReactRouter ReactRedux just to name a few.

Do you actually use curried functions in real-world code?

Yes, I do.

Just as a note, with Haskell-like syntax for types, we could write something like this:

fn :: typeA -> typeB -> returnType

Alas, that is not how JavaScript parameter documentation is generally written.

It is also used in this on the most excellent How to Design Programs and Mostly Adequate Guide to FP books. It is nice because the notation is implicitly curried. Of course, it works better for certain languages than for others. Anyway, just wanted to mention it here. Someone may find it useful.

@FernandoBasso Yes, that Haskell-like syntax is super useful. And thanks for the tip on the book!

Any update?

Still not possible to use JDoc on curried function?
How can I use JDoc with this example: const pair = x => y => f => f(x)(y);

@BenjaminBrodwolf Well - nitpicking here, but that's not a curried function. That's a couple of function returning functions. So the "natural" way to jsdoc it, IMHO, would be:

/**
 * Expects an argument and returns a function.
 * @param {*} x First argument.
 * @returns {Function} A function that expects an argument `y` and returns a new
 *  function that asks for a curried function `f` and invokes it with both `x` and `y`.
 */
const pair = x => y => f => f(x)(y);

Or you could document each returned function individually.

@nfantone const pair = x => y => f => f(x)(y) is definitely a curried function.

That's a couple of function returning functions.

That's the definition of true currying. Here, from wikipedia (emphasis mine):

In mathematics and computer science, currying is the technique of translating the evaluation of a function that takes multiple arguments into evaluating a sequence of functions, each with a single argument. For example, a function that takes two arguments, one from X and one from Y, and produces outputs in Z, by currying is translated into a function that takes a single argument from X and produces as outputs functions from Y to Z.

You may be thinking of Ramda-style currying, which I've seen Ramda maintainers refer to as 'smart currying' to differentiate it from the actual currying @BenjaminBrodwolf 's code exemplifies.

Unfortunately, AFAIK, it's still not very convenient to document Ramda 'smart currying' OR true currying via jsdoc.

To be clear, I think 'classical' currying and 'smart' currying are both useful. I'd like to see support for documenting both flavors improved, and I think it's OK to use 'currying' in this discussion to refer to either or both styles.

@kurtmilam Fair enough. Thanks for the clarification.

However, I stand by what I said. I was not actually thinking about Ramda or what brought up as "smart currying". Here, let me explain.

When we say that f is a "curried function" we are typically expressing "we can think of another function g of which f is _its curried form_". That's it. There's not actual "telling" if a function by itself is "definitely curried", or "uncurried" or neither. A general statement such as that, makes little to no sense when context is removed.

So, in the context of JavaScript as a language, const pair = x => y => f => f(x)(y) are some function returning functions and, while I absolutely agree it could be improved, we do have a way of documenting that with jsdoc.

@nfantone , @kurtmilam

This was written for you guys: https://www.lesswrong.com/posts/7X2j8HAkWdmMoS8PE/disputing-definitions ("Disputing definitions")

The subject is how to document functions returning functions (returning functions). How you call this or that form is not even relevant in this particular context.

And similar discussions arise EVERY SINGLE TIME I see any discussion about functional programming things ALL those discussions devolve into people telling other people that they misunderstand some concept and that the _true_ definition is something else. The problem is, _definitions are subjective_. There is no god handing them down. It does not matter than someone somewhere wrote something down, even if they were first, or if they were famous. Human language mutates. People take something you say - and change it. The terms floating around functional programming are a prime example, where pretty much everybody forms their own ideas in their heads. I suggest to follow the advice in the linked blog post.

@lll000111 Thanks for the link to the article. I have read and bookmarked it, and largely agree with it.

I'd like to clarify that I'm not a linguistic prescriptivist, and support an inclusive understanding of the word 'currying' in the context of this discussion. What I find problematic is someone else prescriptively declaring that a function that is curried by the classical definition is, in fact, not curried at all, especially in a discussion wherein better support for currying is being requested and discussed.

The statement is both prescriptivist and incorrect, and since this issue is about support for curried functions, I think it makes sense to be clear about whether a specific form of function should be considered in the context of the discussion. Again, to be clear, I support an inclusive definition, and would like to see better support for classical currying as well as Ramda's 'smart' currying, which I also find useful and convenient.

As an aside, I'll point out that the article you linked includes a qualifier about when it may make sense to argue about definitions:

Whenever you feel tempted to say the words "by definition" in an argument that is not literally about pure mathematics

Currying is a mathematical term. It was coined by mathematicians and named after a mathematician (Haskell Curry). It seems even the author of the piece you linked might suffer an argument about the true definition of currying :D

Also, let me respond to this statement of yours:

The subject is how to document functions returning functions (returning functions). How you call this or that form is not even relevant in this particular context.

As stated above, my interest is not in arguing how we should call a particular form. Rather, it's in arguing for, in the context of this discussion, an inclusive definition which includes both classic and 'smart' currying (and potentially other styles of 'currying' of which I'm not yet aware).

@nfantone It's unclear why you're still arguing the point. const pair = x => y => f => f(x)(y) is the curried version of const pair = (x, y, f) => f(x)(y) and const pair = (x, y, f) => f(x)(y) is the uncurried version of const pair = x => y => f => f(x)(y), by (mathematical) definition. Neither the curried nor the uncurried version of the function is the 'true' version. They're just two different ways to express the function.

are some function returning functions

This is exactly what currying is, again, from Wikipedia:

transform a function with multiple arguments into a chain of single-argument functions ... This transformation is the process now known as currying.

Additional resources, for those who are interested:

Wow. This took a wild turn. Thanks both, @lll000111 and @kurtmilam for the time and fervent arguments.

@lll000111:

The subject is how to document functions returning functions (returning functions). How you call this or that form is not even relevant in this particular context.

100% agreed. Let's stick to that.

@kurtmilam Let's just agree to disagree for now. I don't believe any of the (really good) resources you've shared strongly translates to your original, simple comment.

On the topic, and since Ramda was mentioned before, an approach I usually take to document curried functions with jsdoc is to place the docs in the original/regular JS function and export the result of curry. Or, if I need the curried function inside the module I'm working (or happens to be a "private" function) just tag it with @function.

const { curry, pathSatisfies } = require('ramda');

/**
 * Checks whether the given predicate is satisfied for the property value
 * at `propName` under `lambdaEvent.requestContext.authorizer`.
 *
 * @function
 * @see https://ramdajs.com/docs/#pathSatisfies
 * @param {Function} predicate The boolean returning function to apply to the
 *  value of the `propName` property.
 * @param {String} propName The name of the property to evaluate.
 * @returns {Boolean} The result of invoking `predicate` with the value of the
 *  property named `propName` on the `lambdaEvent.requestContext.authorizer` object.
 */
const authorizerPropSatisfies = curry(function authorizerPropSatisfies(predicate, propName) {
  return pathSatisfies(predicate, ['lambdaEvent', 'requestContext', 'authorizer', propName]);
});

module.exports = authorizerPropSatisfies;

The name you’re looking for is high order functions, not currying. Just my two cents…

It would be nice if this would work with VS Code
image

On the topic, and since Ramda was mentioned before, an approach I usually take to document curried functions with jsdoc is to place the docs in the original/regular JS function and export the result of curry. Or, if I need the curried function inside the module I'm working (or happens to be a "private" function) just tag it with @function.

const { curry, pathSatisfies } = require('ramda');

/**
 * Checks whether the given predicate is satisfied for the property value
 * at `propName` under `lambdaEvent.requestContext.authorizer`.
 *
 * @function
 * @see https://ramdajs.com/docs/#pathSatisfies
 * @param {Function} predicate The boolean returning function to apply to the
 *  value of the `propName` property.
 * @param {String} propName The name of the property to evaluate.
 * @returns {Boolean} The result of invoking `predicate` with the value of the
 *  property named `propName` on the `lambdaEvent.requestContext.authorizer` object.
 */
const authorizerPropSatisfies = curry(function authorizerPropSatisfies(predicate, propName) {
  return pathSatisfies(predicate, ['lambdaEvent', 'requestContext', 'authorizer', propName]);
});

module.exports = authorizerPropSatisfies;

@MartinMuzatko

It would be nice if this would work with VS Code

Then the issue should be submitted to that team, no?

These are the issues for the documentation generator. First sentence from the README for this repo right here is:

An API documentation generator for JavaScript.

If someone wants to work something with tool X I think it's better to ask the guys responsible for tool X. The author of this repo here can do nothing at all for VScode or whatever other IDE or tool.

Of course!
I just wondered why VS Code is not doing it right, since they use JSDOC internally to create these previews. Maybe I was doing something wrong or they plain don't do it right yet.
I'll create an issue in their repo

@karol-majewski This solution provides best IntelliSense in VS Code:

curriedfun

It seems that this syntax:

/**
 * @param {BrowserHistory} history
 * @returns {(route: string) => void}
 */
const push = history => route => history.push(route);

Does not work well for JSDoc to Markdown generators like jsdoc-to-markdown and markdox. These tools cannot parse these {(route: string) => void} expressions (it would be great they could).

Did anyone experience the same and come with a workaround when using such tools? How did you end up documenting your callbacks and thunks (higher-order function's function return value)?

@MartinMuzatko

It would be nice if this would work with VS Code

It does work! Have you tried enabling ts-check for your sources?

image

@nfantone How do you enable ts-check in VS Code? Thank you!

@tonix-tuft You can either add // @ts-check at the top of each .js file or set "javascript.implicitProjectConfig.checkJs": true on Settings to validate all sources.

there's still a discussion about if it worth doing this or it was decided that it should be supporting currying definitions?

Unfortunately, the solution above shows all parameters to be of type any 😭

Screen Shot 2020-07-01 at 1 15 16 PM

Cross post from: https://github.com/microsoft/tsdoc/issues/240#issuecomment-652582106

This is the only solution that worked for me:

const { curry, pathSatisfies } = require('ramda');

/**
 * Checks whether the given predicate is satisfied for the property value
 * at `propName` under `lambdaEvent.requestContext.authorizer`.
 *
 * @function
 * @see https://ramdajs.com/docs/#pathSatisfies
 * @param {Function} predicate The boolean returning function to apply to the
 *  value of the `propName` property.
 * @param {String} propName The name of the property to evaluate.
 * @returns {Boolean} The result of invoking `predicate` with the value of the
 *  property named `propName` on the `lambdaEvent.requestContext.authorizer` object.
 */

const authorizerPropSatisfies = curry(
  /** @type {(predicate: function, propName: string) => boolean} */
  (predicate, propName) => pathSatisfies(predicate, ['lambdaEvent', 'requestContext', 'authorizer', propName]),
);

module.exports = authorizerPropSatisfies;

Screen Shot 2020-07-01 at 1 38 40 PM

The tradeoff: You have to declare types twice, once inside jsdoc and once just above the function declaration (and inside curry())

However, this doesn't feel completely unnatural as it resembles function declarations found in many FP languages

@simonwjackson Ah, yes. You're absolutely right - I did not typed the arguments in my original example for the inner function. From your suggestion, I noted that if you omit the type definitions on the top function, vscode would infer them just fine nonetheless.

/**
 * Checks whether the given predicate is satisfied for the property value
 * at `propName` under `lambdaEvent.requestContext.authorizer`.
 *
 * @function
 * @see https://ramdajs.com/docs/#pathSatisfies
 * @param predicate The boolean returning function to apply to the
 *  value of the `propName` property.
 * @param propName The name of the property to evaluate.
 * @returns The result of invoking `predicate` with the value of the
 *  property named `propName` on the `lambdaEvent.requestContext.authorizer` object.
 */
const authorizerPropSatisfies = curry(
  /** @type {(predicate: (value: any) => boolean, propName: string) => (obj: Object) => boolean} */
  (predicate, propName) =>
    pathSatisfies(predicate, ['lambdaEvent', 'requestContext', 'authorizer', propName])
);

This way, there's no type duplication - at the probable expense of some reading clarity. I also improved some of the definitions so they are closer to their @types/ramda counterparts.

image

@nfantone This is great! I wonder if any of the documentation generators (TypeDoc) will pick up the types as well.

@simonwjackson Good question. Would have to try that - although my hunch says they won't. Let me know if you do!

This one makes Visual Studio Code provide proper IntelliSense:

/**
 * @param {Object} browserHistory
 * @returns {function(string): boolean} a function accepting a single route parameter
*/
const push = browserHistory => route => browserHistory.push(route);

However, if you're using JSDoc annotations to leverage type checking, it won't do. To deal with TypeScript and its Parameter 'route' implicitly has 'any' type. message, you'd have to use inline types:

const push = browserHistory => (/** @type {string} */ route) => browserHistory.push(route);

This solution worked for me with Vim, CoC and coc-tsserver. Intellisense and parameter hints working perfectly. I'm very grateful! Thanks.

Just wanted to share a @callback driven approach that I've been using. It preserves the type information pretty well and lets you keep the JSDoc blocks outside of your function definitions:

/**
 * @callback compareToRecordOrIdCb
 * @param {GlideRecord_proto|string} record A GlideRecord or sys ID string
 * @return {string}
 */

/** @type {function(Function): compareToRecordOrIdCb} */
const compareToRecordOrId = (operation) => (record) => {
    if (isGlideRecord(record)) {
        throwIfNoQueryResult(record);
        return operation(record.getUniqueValue());
    } else {
        return operation(record);
    }
};

/**
 * Provides a query string that will filter for exactly the given record
 */
export const exactlyThisRecord = compareToRecordOrId(hasSysId);

The main downside of this approach is that you will only see the callback type intellisense once you're inside of the actual parameter list, that info is missing when viewed in the finder list:

Parameter List (shows info):
1599070129

Finder List (info missing):
1599070111

Keep in mind that @callback will only remember the description for @param tags, so you'll need to split your @return tag into two. You use one to document the return type inside of @callback and the other to document the return description inside of your main JSDoc block.

Here's a full and practical example with that fix applied:

/**
 * @callback compareToRecordOrIdCb
 * @param {GlideRecord_proto|string} record A GlideRecord or sys ID string
 * @return {string}
 */

/** @type {function(Function): compareToRecordOrIdCb} */
const compareToRecordOrId = (operation) => (record) => {
    if (isGlideRecord(record)) {
        throwIfNoQueryResult(record);
        return operation(record.getUniqueValue());
    } else {
        return operation(record);
    }
};

/**
 * Provides a query string that will filter for exactly the given record
 *
 * @return A query string to be used with query functions or GlideRecord.addEncodedQuery
 *
 * @throws {BadTableError} When record is a GlideRecord and is not configured for a valid table name
 * @throws {MissingRecordError} when record is a GlideRecord and is not indexed at any record
 */
export const exactlyThisRecord = compareToRecordOrId(hasSysId);

/**
 * Provides a query string that will filter out the given record
 *
 * @return A query string to be used with query functions or GlideRecord.addEncodedQuery
 *
 * @throws {BadTableError} When record is a GlideRecord and is not configured for a valid table name
 * @throws {MissingRecordError} when record is a GlideRecord and is not indexed at any record
 */
export const notThisRecord = compareToRecordOrId(notSysId);

Note how I'm also manually providing @throws in each main JSDoc block. That's because @callback only cares about the type signature. VS Code won't acknowledge most tags if you put them there (like @throws in this example)

Was this page helpful?
0 / 5 - 0 ratings

Related issues

tunderdomb picture tunderdomb  ·  14Comments

yordis picture yordis  ·  17Comments

anselmbradford picture anselmbradford  ·  14Comments

kripod picture kripod  ·  26Comments

pedronasser picture pedronasser  ·  88Comments