Array.prototype.ap

There's a pattern I sometimes use when needing to generate several bits of data from the same object using functions.

Consider this piece of code:

const person = {
  firstName: 'Thomas',
  lastName: 'Edison',
  dob: new Date(1847, 1, 11)
};
const [fullName, age] = [combineNames, calculateAgeFromDob].map((fn) => fn(person));

Here, I have an array of functions and will process each with the same argument(s). The above line could be rewritten like this:

const [fullName, age] = [combineNames, calculateAgeFromDob].ap(person);

This idea was inspired by some advice I received from @bergus in the summer of 2022, and follows the "well-known ap method of applicative functors":

Example:

const [slug, date, wordCount, charCount] = [buildSlug, getDaysSince, getWordCount, getCharCount].ap(article);
// same as
const [slug, date, wordCount, charCount] = [buildSlug(article), getDaysSince(article), getWordCount(article), getCharCount(article)];

Conceptually, the implementation would be comparable to this:

Array.prototype.ap = function(...args) {
    return this.map(fn => fn(...args));
}

I have a stage 0 proposal for this open here:

Part of the reason we've declined to do Applicative so far is that mapping a list of functions over a single argument is only one possibly usage of ap - in general, it lets you combine list of functions and multiple args, invoking the combination of all of them. For example, giving [plus, mul], [1, 2], and [30, 40], aping these together would give [plus(1, 30), plus(1, 40), plus(2, 30), plus(2, 40), mul(1, 30), mul(1, 40), mul(2, 30), mul(2, 60)] (aka [31, 41, 32, 42, 30, 40, 60, 80] - in general, it takes an list of functions and some number of lists of arguments, inverts them into a list of (func, arg1, arg2, ...) tuples, and invokes each of those tuples, returning the final list of results. (And of course, by "lists" here I mean any Applicative.)

If you limit this purely to a list of functions and a single argument, then the benefits largely disappear. As you note, it's just a slightly shorter way to spell an existing .map() expression at that point.

2 Likes

@tabatkins Thanks for the extra insight. fwiw my primary use case is using one arg and destructuring the returned values out to different variables, associated with the result of each function.

const [slug, date, wordCount, charCount] = [buildSlug, getDaysSince, getWordCount, getCharCount].ap(article);

In most cases, I have a number of functions that all expect the same complex type for their parameter(s). One common of this would be a user object, for which you might have functions to calculate the user's full name, age, obfuscated card number, etc.

One particular use case I had involved several helper functions I had which I called repeatedly when setting up and managing an active chess board.

This is the source:

const getPieceName = (piece) => piece.dataset.pieceName;
const getPieceKey = (piece) => piece.dataset.pieceKey;
const getPieceColor = (piece) => piece.dataset.pieceColor;
const getStartPos = (piece) => piece.dataset.startPosition;
const getCurrentPos = (piece) => piece.dataset.currentPosition;
const getPieceAlg = (piece) => piece.style.getPropertyValue('--position').trim();
const getPiecePos = (piece) => algToPos(piece.style.getPropertyValue('--position').trim());
const getMoveCount = (piece) => Number(piece.dataset.moveCount);
const checkIfCurrentPiece = (piece) => getPieceColor(piece) === currentTurn;
const checkIfOpponentPiece = (piece) => getPieceColor(piece) === opponentTurn;

With an ap method, I could destructure this information like so:

const [
  pieceName,
  pieceKey,
  pieceColor,
  startPos,
  currentPos,
  pieceAlg,
  piecePos,
  moveCount,
  isCurrentPiece,
  isOpponentPiece,
] = [
  getPieceName,
  getPieceKey,
  getPieceColor,
  getStartPos,
  getCurrentPos,
  getPieceAlg,
  getPiecePos,
  getMoveCount,
  checkIfCurrentPiece,
  checkIfOpponentPiece,
].ap(piece);

Granted, I think some sort of functional destructuring, initially discussed here, would be better fit for this, which I believe is covered by @rbuckton's proposal-extractors proposal: GitHub - tc39/proposal-extractors: Extractors for ECMAScript.

In that case, that might look like this:

const [
  getPieceName(pieceName),
  getPieceKey(pieceKey),
  getPieceColor(pieceColor),
  getStartPos(startPos),
  getCurrentPos(currentPos),
  getPieceAlg(pieceAlg),
  getPiecePos(piecePos),
  getMoveCount(moveCount),
  checkIfCurrentPiece(isCurrentPiece),
  checkIfOpponentPiece(isOpponentPiece),
] = piece;

@rbuckton Could you please indicate if I got the syntax right here? If that is accepted, I will rescind this proposal, but I was encouraged in my functional destructuring proposal to propose ap directly as array methods might be easier to justify.

If the syntax is wrong and would actually need to be […] = Array(10).fill(piece), then I might admittedly still see significant value in an ap method, or some other way to functionally destructure from a single argument.

Yeah, for that use-case, Extractors should do the job.

1 Like

@rbuckton Could you please indicate if I got the syntax right here? If that is accepted, I will rescind this proposal, but I was encouraged in my functional destructuring proposal to propose ap directly as array methods might be easier to justify.

Extractors do not quite work this way. First, the syntax const [ /*whatever*/ ] = piece implies that piece must be an iterable. Second, each of the functions you're using must return an array, i.e.:

(piece) => [piece.dataset.pieceName]
// etc.

This is generally far less efficient than just writing:

const pieceName = getPieceName(piece),
      pieceKey = getPieceKey(piece),
     ...;

The way an extractor is ideally supposed to work is to act as the opposite of construction. E.g., something like this:

class Piece {
  constructor(pieceName, pieceKey, pieceColor, ...) {
    this.pieceName = pieceName;
    this.pieceKey = pieceKey;
    this.pieceColor = pieceColor;
    ...
  }
  static [Symbol.customMatcher](piece) {
    return piece instanceof Piece && [piece.pieceName, piece.pieceKey, piece.pieceColor, ...];
  }
}

Which would allow you to write:

const piece = new Piece("name", "key", "color", ...);
const Piece(pieceName, pieceKey, pieceColor) = piece;

Now one thing you could do is build an adapter over a piece instead, and use normal destructuring:

function makeAdapter(hooks) {
  const weakValue = new WeakMap();
  const prototype = Object
    .entries(hooks)
    .map(([key, callback]) => [key, { get() { return callback(read(this)); } }])
    .reduce((obj, [key, descriptor]) => Object.defineProperty(obj, key, descriptor), {});

  function read(obj) {
    const value = weakValue.get(obj);
    if (!value) throw new Error("Wrong target");
    return value;
  }

  function wrap(subject) {
    const obj = Object.create(prototype);
    weakValue.set(obj, subject);
    return obj;
  }

  return {
    wrap,
    [Symbol.customMatcher](subject) {
      return [wrap(subject)];
    }
  };
}

const PieceAdapter = makeAdapter({
  pieceName: getPieceName,
  pieceKey: getPieceKey,
  pieceColor: getPieceColor,
  // ...etc.
});

// normal destructuring, works today
const { pieceName, pieceKey, pieceColor, } = PieceAdapter.wrap(piece);

// extractors
const PieceAdapter({ pieceName, pieceKey, pieceColor }) = movePiece;

// extractors + nesting
const { move, piece: PieceAdapter({ pieceName, pieceKey, pieceColor }) } = movePiece;

Another option would be to use something like GitHub - tc39/proposal-joint-iteration: a TC39 proposal to synchronise the advancement of multiple iterators and a simple helper:

function * repeat(value) { for (;;) yield value; }
const ap = (fns, value) = Iterator.zipToArrays([fns, repeat(value)]).map(([fn, value]) => fn(value))

const [
  pieceName,
  pieceKey,
  pieceColor,
] = ap([
  getPieceName,
  getPieceKey,
  getPieceColor,
], piece);

Or even simpler:

const ap = (fns, value) = fns.map(fn => fn(value));

const [
  pieceName,
  pieceKey,
  pieceColor,
] = ap([
  getPieceName,
  getPieceKey,
  getPieceColor,
], piece);

Thanks, @rbuckton! Those are all great suggestions. I think I learned a thing or two too :wink:


One other option that @theScottyJam mentioned here in my earlier Functional Destructuring proposal was to use the extensions proposal, like this:

for (const article of apiResponse) {
  const {
    title,
    subtitle,
    content,
    category,
    ::buildSlug: slug,
    ::getDaysSince: date,
    ::getWordCount: wordCount,
    ::getCharCount: charCount,
  } = article;
}

In the example we looked at above, it might look like this:

const {
  ::getPieceName: pieceName,
  ::getPieceKey: pieceKey,
  ::getPieceColor: pieceColor,
  ::getStartPos: startPos,
  ::getCurrentPos: currentPos,
  ::getPieceAlg: pieceAlg,
  ::getPiecePos: piecePos,
  ::getMoveCount: moveCount,
  ::checkIfCurrentPiece: isCurrentPiece,
  ::checkIfOpponentPiece: isOpponentPiece,
} = piece;

If this works as he suggests, this should do exactly what I'm looking for here.

@hax would you mind shedding some light on this, seeing as you appear to be the author of that proposal?

or even simpler still,

const [
pieceName,
pieceKey,
pieceColor,
] = [
getPieceName,
getPieceKey,
getPieceColor,
].map(x => piece(x));

@ljharb Do you mean .map(x => x(piece))?

How would the other way around work? (genuinely asking)

That was in the original brief, as what I was trying to avoid, the primary inspiration for this proposal.

ah yes sorry, that's what i meant :-) i'd probably write .map(fn => fn(piece)) then.

About extensions proposal, the extensions proposal does not include destructuring because I was not sure whether it's a good idea that time, especially it only relate to extension getter, seems not need to include it in the core proposal.

However, this is largely similar to the GitHub - tc39/proposal-destructuring-private: A proposal integrate private fields and destructuring proposal (which allows destructuring of private fields). So if the extensions proposal advances to the next stage, we could also have a corresponding proposal-destructuring-extension-getter. Alternatively, if by then the proposal-destructuring-private has already reached stage 3 or stage 4, it could be directly integrated into the extensions proposal.