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

Viability of achieving this in userland libraries #45

Open
peey opened this issue Apr 9, 2021 · 6 comments
Open

Viability of achieving this in userland libraries #45

peey opened this issue Apr 9, 2021 · 6 comments

Comments

@peey
Copy link

peey commented Apr 9, 2021

If we treat the pipelines proposal as a separate proposal and disregard the pipelines-related examples, I'm wondering if this proposal can entirely be achieved through libraries (and if not, then what what are the key issues that prevent it)

e.g. it should be possible to write a library that allows you to write

import {_, partial} from "a-partial-application-library" // _ is a symbol, partial is like Function.call but that recognizes the symbol _ and performs partial application
const addTen = partial(add, _, 10) // instead of add(?, 10)
const another = partial(f, _, 10, "hi", _, "yo") // instead of f(?, 10, "hi", ?, "yo")

it may even be possible to support #5

// design 1

import {_0, _1, partial} from "a-partial-application-library" // _0 might be an alias for _
const another = partial(f, _1, 10, "hi", _0, "yo") // instead of f(?1, 10, "hi", ?0, "yo")

// design 2
import {_, partial} from "a-partial-application-library"
const another = partial(f, _[1], 10, "hi", _[0], "yo") // instead of f(?1, 10, "hi", ?0, "yo")

and to support #7

function add({x,y}) {
    return x + y;
}

// design 1 - dumb implementation
import {_, partial} from "a-partial-application-library" 

const plus2 = partial(add, {y : 2, x: _}) // instead of add({y:2, ...});
// the above is dumb because it'll cause the implementation in "partial" to do a nested object walk through all arguments  to find the key that's equal to `_`

// design 2 - slightly smarter
import {_, partial} from "a-partial-application-library"  // this time _ is callable

const plus2 = partial(add, _({y : 2})) // _ wraps the optional arg and marks first positional argument to add as having been partially applied
console.log(plus2({x : 3})) // and when {x: 3} is passed, partial performs Object.assign({}, {y:2}, {x: 3}) and passes that as an arg to add

// design 3 - perhaps clearer?

// we can use a different name than _ for these "args which have been supplied but incompletely" (avoiding the word "partial" because that's being used for "args which have not been supplied at all")
import {_, incomplete, partial} from "a-partial-application-library"  // this time _ is callable

const plus2 = partial(add, incomplete({y : 2})) 
console.log(plus2({x : 3}))

const other = partial(someFunc, _, 3, incomplete({x : 2}), _, "hi")

// we can also support fixed-length arrays by using 

incomplete(_, 1) // to denote first element of array is missing, when user passes [0] this arg will resolve to [0, 1] before being sent to the function
incomplete(1, _, 3) // to denote middle element is missing, user passes [2] and this arg resolves to [1, 2, 3] before being sent to the function
incomplete(1, _, 3, _, 5) // when user passes [2, 4], function gets [1, 2, 3, 4, 5] 
incomplete(1, _, 3, _, 5, _rest) // when user passes [2, 4] function gets [1,2, 3, 4, 5] and when user passes [2, 4, 6, 7] the function gets [1,2,3,4,5,6,7] 

// nesting can be achieved, though you might prefer the shorter name _ over "incomplete" here
incomplete(1, _, incomplete(_, "y", _)) // user passes [2, ["x", "z"]] and function gets [1, 2, ["x", "y", "z"]
incomplete(1, _, incomplete({'key1': 'v1'})) // user passes [2, {"key2": "v2"}] and function gets [1, 2, {"key1": "v1", "key2": "v2"}

Through these examples, I'm not saying that this proposal is not needed. Indeed some of the syntax clunkiness in a userland library would go away if this were a language feature. I'm just hoping that this leads to a discussion of he core language limitations that prevent this proposal from happening as a userland library, and to discuss if this proposal addresses those limitations well.

A discussion of how this can (even partially) be achieved in a userland library may also support development of this feature in transpilers (babel, typescript) and allow it to reach users faster.

@noppa
Copy link

noppa commented Apr 9, 2021

Lodash's _.partial works pretty much like that (without some of the more advanced features you envision in the last example).

const f = (a, b, c) => { console.log(a, b, c) }
const incomplete = _.partial(f, 1, _, 3)
incomplete(2)
// logs 1, 2, 3

there are also similar functions in other popular utility libraries.

Indeed some of the syntax clunkiness in a userland library would go away

IMO this is a big benefit of the operator.
Ease of use is a "break it or make it" quality of a utility feature like this.

I practically never use the partial helper functions because at the end of the day, writing

import {_, partial} from 'a-partial-application-library'
const plus2 = partial(plus, _, 2)

is more cumbersome and arguably even less readable than your everyday

const plus2 = _ => plus(_, 2)

Also their static typing with TS/Flow often leaves much to be desired.

@aminnairi
Copy link

aminnairi commented Apr 23, 2021

Here is my proposal polyfill. It only supports partial application from left to right, just like in a functional programming language like Haskell or Elm. One could also write a flip function if there is a need to do partial application backward.

/**
 * @description Partial polyfill
 * @author Amin NAIRI <https://github.com/aminnairi/>
 * @version 1.0.0
 * @example https://replit.com/@amin_nairi/LividEffectiveConversions#index.js
 */
const partial = (callback, ...initialArguments) => {
  return (...additionalArguments) => {
    const allArguments = [...initialArguments, ...additionalArguments];

    if (allArguments.length >= callback.length) {
      return callback(...allArguments);
    }

    return partial(callback, ...allArguments);
  };
};

const add = partial((first, second) => {
  return first + second;
});

const range = partial((from, to) => {
  return [...Array(to - from + 1)].map((_, index) => {
    return index + from;
  });
});

const fromFiveTo = range(5);
const increment = add(1);
const decrement = add(-1);

console.log(fromFiveTo(10)); // [5, 6, 7, 8, 9, 10]
console.log(range(1, 5)); // [1, 2, 3, 4, 5]
console.log(add(2, 3)); // 5
console.log(increment(4)); // 5
console.log(decrement(0)); // -1

Not sure if this is really necessary to do partial application backward for an argument a in a function f(a, b) like f(?, b). I find it really odd and never had any problem doing so. Haskell does a great job defining function argument order so that it is rarely needed to use flip on a function. The data comes last and the settings come first.

import {storageGet} from "./library/api/storage-get.mjs";
import {storageSet} from "./library/api/storage-set.mjs";

storageSet(localStorage, "search", "");
storageGet(localStorage, "search").whenOk(searchValue => console.log(searchValue));
storageGet(localStorage, "items").whenOk(storageSet(localStorage, "savedItems"));

@lazarljubenovic
Copy link

I'm by no means an expert on this, but I assume another huge benefit of this being part of the language is performance. Engines would be able to optimize partial application and/or pipes as if they were regular function calls, and not create the mess of intermediate functions.

@aminnairi
Copy link

aminnairi commented Apr 23, 2021

True, Rambda has a placeholder for doing that with R.__ but of course, this adds some runtime overhead (just like the solution I proposed above) and thus slows down the interpretation of the script.

Though this won't add too much overhead for most of the Web applications (to not say practically invisible for a non-power user) out there that do not have performance in mind, this can be critical for other range of domains like Gaming or Geolocation and such.

In the end, achieving this in a userland library should be viable if there is no need for performance and we can achieve a pretty slick API with partial application and function composition.

import {compose} from "partials/compose.mjs";
import {map, mapWithIndex} from "partials/map.mjs";
import {reduce} from "partials/map.mjs";
import {isModulo} from "partials/utils.mjs";
import {or} from "partials/logical.mjs";

const isValidFrenchCompanyIdentifier = compose([
  map(compose([Number, or(0)])),
  mapWithIndex(doubleIfOddIndex),
  map(sumDigits),
  reduce(sum),
  isModulo(10)
]);

isValidFrenchCompanyIdentifier("732829320"); // true
isValidFrenchCompanyIdentifier("732829321"); // false

@fabiosantoscode
Copy link

@lazarljubenovic yes indeed!

Terser could also know how to inline bound functions, since it can't know for sure that .bind() wasn't modified in the function prototype.

When combining with pipelining, I think the performance cost of ? would disappear for most pipelining cases. As well as end the debate over on that proposal regarding first-argument vs curried.

@make-github-pseudonymous-again

Terser could also know how to inline bound functions, since it can't know for sure that .bind() wasn't modified in the function prototype.

I don't know of how much of a niche this is, but the current proposal indeed would allow some light version of static metaprogramming without the need of a tool such as babel. Although one could argue that this is already achieved by using arrow functions to reorder/fix parameters (minus this).

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

6 participants