Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for curried functions #1286

Open
akkie opened this issue Oct 21, 2016 · 52 comments
Open

Support for curried functions #1286

akkie opened this issue Oct 21, 2016 · 52 comments

Comments

@akkie
Copy link

akkie commented Oct 21, 2016

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)
@kurtmilam
Copy link

kurtmilam commented Nov 24, 2016

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.

@kurtmilam
Copy link

kurtmilam commented Nov 24, 2016

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.

@minexew
Copy link

minexew commented Dec 2, 2016

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

@kurtmilam
Copy link

kurtmilam commented Dec 6, 2016

@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).

@minexew
Copy link

minexew commented Dec 6, 2016

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.

@kurtmilam
Copy link

kurtmilam commented Dec 6, 2016

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 )

@Velinsky
Copy link

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

@jsatk
Copy link

jsatk commented Apr 28, 2017

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.

@karol-majewski
Copy link

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);

@dietergeerts
Copy link

dietergeerts commented Sep 6, 2017

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));
}

@angeloocana
Copy link

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.

@maciekmaciej
Copy link

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

curriedfun

@karol-majewski
Copy link

@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);

@eyalcohen4
Copy link

eyalcohen4 commented Dec 26, 2017

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

@lll000111
Copy link

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).

@babakness

This comment was marked as spam.

@dietergeerts
Copy link

dietergeerts commented Apr 3, 2018

@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.

@nfantone
Copy link

nfantone commented Jul 4, 2018

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?

@dietergeerts
Copy link

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

@nfantone
Copy link

nfantone commented Jul 5, 2018

@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.

@JesterXL
Copy link

JesterXL commented Oct 3, 2018

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

@joelnet
Copy link

joelnet commented Oct 10, 2018

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

@lll000111
Copy link

@joelnet We get what we pay for...

@dietergeerts
Copy link

dietergeerts commented Oct 15, 2018

@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.

@phillt
Copy link

phillt commented Nov 19, 2018

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.

@FernandoBasso
Copy link

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

Yes, I do.

@BenjaminBrodwolf
Copy link

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);

@nfantone
Copy link

@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.

@kurtmilam
Copy link

kurtmilam commented Dec 20, 2019

@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.

@nfantone
Copy link

nfantone commented Dec 21, 2019

@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.

@lll000111

This comment was marked as off-topic.

@kurtmilam

This comment was marked as off-topic.

@nfantone

This comment was marked as off-topic.

@nfantone
Copy link

nfantone commented Dec 21, 2019

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;

@jirutka
Copy link

jirutka commented Dec 21, 2019

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

@MartinMuzatko
Copy link

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;

@lll000111
Copy link

@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.

@MartinMuzatko
Copy link

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

@tonix-tuft
Copy link

tonix-tuft commented Apr 18, 2020

@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)?

@nfantone
Copy link

nfantone commented May 2, 2020

@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

@tonix-tuft
Copy link

tonix-tuft commented May 2, 2020

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

@nfantone
Copy link

nfantone commented May 4, 2020

@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.

@renatobenks
Copy link

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

@simonwjackson
Copy link

simonwjackson commented Jul 1, 2020

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: microsoft/tsdoc#240 (comment)

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

@nfantone
Copy link

nfantone commented Jul 2, 2020

@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

@simonwjackson
Copy link

simonwjackson commented Jul 2, 2020

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

@nfantone
Copy link

nfantone commented Jul 2, 2020

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

@FernandoBasso
Copy link

FernandoBasso commented Aug 2, 2020

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.

@chaorace
Copy link

chaorace commented Sep 2, 2020

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)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests