0

I'm practicing partial application of a function, that is, fixing function arguments. I've learned two ways to achieve it:

  1. By currying the original function first.
  2. By using .bind() method.

In the following example I'm going to show that only the first strategy, i.e., by currying first, works. My question is why using .bind() doesn't work.

Example

Consider the following data:

const genderAndWeight = {
  john: {
    male: 100,
  },
  amanda: {
    female: 88,
  },
  rachel: {
    female: 73,
  },
  david: {
    male: 120,
  },
};

I want to create two utility functions that reformat this data into a new object:

  • function A -- returns people names as keys and weights as values
  • function B -- returns people names as keys and genders as values

Because these two functions are expected to be very similar, I want to create a master function, and then derive two versions out of it, thereby honoring the DRY principle.

// master function
const getGenderOrWeightCurried = (fn) => (obj) =>
  Object.fromEntries(Object.entries(obj).map(([k, v]) => [k, fn(v)]));

The heart of this solution is what I'm going to supply to fn parameter. So either

const funcA = (x) => Number(Object.values(x)); // will extract the weights

or

const funcB = (x) => Object.keys(x).toString(); // will extract the genders

And now doing partial application:

const getWeight = getGenderOrWeightCurried(funcA);
const getGender = getGenderOrWeightCurried(funcB);

Works well:

console.log({
  weight: getWeight(genderAndWeight),
  gender: getGender(genderAndWeight),
});
// { weight: { john: 100, amanda: 88, rachel: 73, david: 120 },
//   gender: 
//    { john: 'male',
//      amanda: 'female',
//      rachel: 'female',
//      david: 'male' } }

So far so good. The following way uses .bind() and doesn't work


// master function
const getGenderOrWeightBothParams = (fn, obj) =>
  Object.fromEntries(Object.entries(obj).map(([k, v]) => [k, fn(v)]));

// same as before
const funcA = (x) => Number(Object.values(x));
const funcB = (x) => Object.keys(x).toString();

// partial application using .bind()
const getWeight2 = getGenderOrWeightBothParams.bind(funcA, null);
const getGender2 = getGenderOrWeightBothParams.bind(funcB, null);

// log it out to console
console.log({weight: getWeight2(genderAndWeight), gender: getGender2(genderAndWeight)})

TypeError: fn is not a function


It's also worth noting that in a different scenario, .bind() does allow partial application. For example:

const mySum = (x, y) => x + y;
const succ = mySum.bind(null, 1);
console.log(succ(3)); // => 4
Emman
  • 3,695
  • 2
  • 20
  • 44
  • 5
    Incorrect use of `.bind`. `getGenderOrWeightBothParams.bind(funcA, null)` should be `getGenderOrWeightBothParams.bind(null, funcA)`. Otherwise you are binding `fn` to `null`. The first argument passed to `.bind` will become the `this` value. In your "different scenario" you are doing it correctly (`null` is passed first (assigned to `this`), `1` is assigned to `x`). – Felix Kling Apr 20 '22 at 19:37
  • 3
    [MDN documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind). The signature of bind is `.bind(, , , ...)`, **not** `.bind(, , ...)`. Maybe you think in your last example that you are binding `null` to `x` and `1` to `y` but that's not the case. You are binding `null` to `this` and `1` to `x`. How did you think `.bind` works? – Felix Kling Apr 20 '22 at 19:40
  • I see. Yes, `x` and `y` are interchangeable in `mySum()`. – Emman Apr 20 '22 at 19:43
  • @FelixKling, maybe it's beyond the scope of the post, but as we're in the context -- when will I ever want to bind non-`null` to `this`? In other words, am I to conclude that *"the way to use `.bind()` for partial application is to pass `null` as first arg, always"*? – Emman Apr 20 '22 at 19:49
  • 1
    E.g. when you want to pass a method as a callback to another function. E.g. `someElement.addEventListener('click', someObject.someMethod.bind(someObject))`. The more general answer is that there are situations where you want `this` to have a specific value but it's not you who calls the function so you cannot control with which `this` value it will be called. – Felix Kling Apr 20 '22 at 20:22

1 Answers1

2

where it comes from

Currying and partial application are of functional heritage and so using them outside of this context will prevent you from receiving their full benefit and likely be a source of self-inflicted confusion.

The proposed data structure is riddled with issues, the largest being that data is mixed across both values and keys of the data object. Names, genders, and weights are all values. name, gender, and weight are keys. This changes your data to this sensible shape where it also takes on a sensible name, people.

currying

pick accomplishes its goal easily because name, gender, and weight are all semantically adjacent, ie they are all keys to pick from an object. When data is mixed across values and keys, it makes it harder to navigate the structure and introduces unnecessary complexity into your program.

const people = [
  { name: "john", gender: "male", weight: 100 },
  { name: "amanda", gender: "female", weight: 88 },
  { name: "rachel", gender: "female", weight: 73 },
  { name: "david", gender: "male", weight: 120 }
]

// curried
const pick = (fields = []) => (from = []) =>
  from.map(item => Object.fromEntries(fields.map(f => [f, item[f]])))

const nameAndGender =
  pick(["name", "gender"]) // ✅ apply one argument

const nameAndWeight =
  pick(["name", "weight"]) // ✅ apply one argument

console.log(nameAndGender(people))
console.log(nameAndWeight(people))
.as-console-wrapper { min-height: 100%; top: 0; }

partial application

partial is perfectly adequate for advancing your understanding at this point. You don't need .bind as its first argument is concerned with dynamic context, a principle of object-oriented style.

Here's the same demo using an uncurried pick and applying partial application instead -

const people = [
  { name: "john", gender: "male", weight: 100 },
  { name: "amanda", gender: "female", weight: 88 },
  { name: "rachel", gender: "female", weight: 73 },
  { name: "david", gender: "male", weight: 120 }
]

// uncurried
const pick = (fields = [], from = []) =>
  from.map(item => Object.fromEntries(fields.map(f => [f, item[f]])))

const partial = (f, ...a) =>
  (...b) => f(...a, ...b)

const nameAndGender =
  partial(pick, ["name", "gender"]) // ✅ partial application

const nameAndWeight =
  partial(pick, ["name", "weight"]) // ✅ partial application

console.log(nameAndGender(people))
console.log(nameAndWeight(people))
.as-console-wrapper { min-height: 100%; top: 0; }

"is it mandatory to change the data structure?"

Certainly not, but you will quickly run into trouble. Let's carry your exercise through and see where problems arise. As you demonstrated, the curried program works fine -

const genderAndWeight = {
  john: {male: 100},
  amanda: {female: 88},
  rachel: {female: 73},
  david: {male: 120},
}

const getGenderOrWeightCurried = (fn) => (obj) =>
  Object.fromEntries(Object.entries(obj).map(([k, v]) => [k, fn(v)]));

const funcA = (x) => Number(Object.values(x));
const funcB = (x) => Object.keys(x).toString();

const getWeight = getGenderOrWeightCurried(funcA);
const getGender = getGenderOrWeightCurried(funcB);

console.log({
  weight: getWeight(genderAndWeight),
  gender: getGender(genderAndWeight),
});
.as-console-wrapper { min-height: 100%; top: 0; }

The partial application program in your question uses .bind incorrectly. The context (null) is passed as the second position, but .bind expects this argument in the first position -

const getWeight2 =
  getGenderOrWeightBothParams.bind(funcA, null); // ❌

const getWeight2 =
  getGenderOrWeightBothParams.bind(null, funcA); // ✅

You could do the same to fix getGender2, but let's use partial for this one instead. Dynamic context is an object-oriented mechanism and you do not need to be concerned with it when you are learning fundamentals of functional programming. partial allows you to bind a function's parameters without needing to supply a context -

const partial = (f, ...a) =>
  (...b) => f(...a, ...b)

const getGender2 =
  getGenderOrWeightBothParams.bind(funcB, null); // ❌

const gender2 =
  partial(getGenderOrWeightBothParams, funcB); // ✅

This gives you two working examples of partial application using the original proposed data structure -

const genderAndWeight = {
  john: {male: 100},
  amanda: {female: 88},
  rachel: {female: 73},
  david: {male: 120},
}

const partial = (f, ...a) =>
  (...b) => f(...a, ...b)

const getGenderOrWeightBothParams = (fn, obj) =>
  Object.fromEntries(Object.entries(obj).map(([k, v]) => [k, fn(v)]));

const funcA = (x) => Number(Object.values(x));
const funcB = (x) => Object.keys(x).toString();

const getWeight2 =
  getGenderOrWeightBothParams.bind(null, funcA); // ✅ .bind

const getGender2 =
  partial(getGenderOrWeightBothParams, funcB) // ✅ partial

console.log({
  weight: getWeight2(genderAndWeight),
  gender: getGender2(genderAndWeight),
});
.as-console-wrapper { min-height: 100%; top: 0; }

"so where's the problem?"

Right here -

const funcA = (x) => Number(Object.values(x)); // ⚠️
const funcB = (x) => Object.keys(x).toString(); // ⚠️ 

"but it works!"

Did you know that your funcA creates an array of a number, converts it to a string, then back to a number again? In fact the only reason it appears to work correctly is because each person is an object with a single key/value pair. As soon as you add more entries, the model breaks -

const o1 = { female: 73 }
const o2 = { female: 73, accounting: 46000 }
const o3 = { gender: "female", weight: 73, role: "accounting", salary: 46000 }

const funcA = x => Number(Object.values(x))

console.log(funcA(o1)) // 73
console.log(funcA(o2)) // NaN
console.log(funcA(o3)) // NaN

A similar issue is happening with funcB. Your function appears to work correctly because an array of a single string ["foo"] when converted to a string, will result in "foo". Try this on any larger array and you will get an unusable result -

const o1 = { female: 73 }
const o2 = { female: 73, accounting: 46000 }
const o3 = { gender: "female", weight: 73, role: "accounting", salary: 46000 }

const funcB = x => Object.keys(x).toString()

console.log(funcB(o1)) // "female"
console.log(funcB(o2)) // "female,accounting"
console.log(funcB(o3)) // "gender,weight,role,salary"

How are funcA and funcB going to work when more data is added to the tree?

to hell and back again

We know that funcA is called once per item in the original data. Choosing an person at random, let's see what happens when funcA reaches rachel's value. Just how bad is it, really?

Number(Object.values(x))  x := { female: 73 }
Number(value)  value := [73]

When Number is called with argument value, the following steps are taken:

  1. If value is present, then ✅
    1. Let prim be ? ToNumeric(value). ✅
    2. If Type(prim) is BigInt, let n be (ℝ(prim)). ❌
    3. Otherwise, let n be prim. ✅
  2. Else,
    1. Let n be +0.
  3. If NewTarget is undefined, return n. ✅
  4. Let O be ? OrdinaryCreateFromConstructor(NewTarget, "%Number.prototype%", « [[NumberData]] »).
  5. Set O.[[NumberData]] to n.
  6. Return O.
ToNumeric(value)  value := [73]

The abstract operation ToNumeric takes argument value and returns either a normal completion containing either a Number or a BigInt, or a throw completion. It returns value converted to a Number or a BigInt. It performs the following steps when called:

  1. Let primValue be ? ToPrimitive(value, number). ✅
  2. If Type(primValue) is BigInt, return primValue. ❌
  3. Return ? ToNumber(primValue). ✅
ToPrimitive(input[, preferredType])  input := [73], preferredType := number

The abstract operation ToPrimitive takes argument input (an ECMAScript language value) and optional argument preferredType (string or number) and returns either a normal completion containing an ECMAScript language value or a throw completion. It converts its input argument to a non-Object type. If an object is capable of converting to more than one primitive type, it may use the optional hint preferredType to favour that type. It performs the following steps when called:

  1. If Type(input) is Object, then ✅
    1. Let exoticToPrim be ? GetMethod(input, @@toPrimitive). ✅
    2. If exoticToPrim is not undefined, then ❌
      1. If preferredType is not present, let hint be "default".
      2. Else if preferredType is string, let hint be "string".
      3. Else,
        1. Assert: preferredType is number.
        2. Let hint be "number".
      4. Let result be ? Call(exoticToPrim, input, « hint »).
      5. If Type(result) is not Object, return result.
      6. Throw a TypeError exception.
    3. If preferredType is not present, let preferredType be number. ❌
    4. Return ? OrdinaryToPrimitive(input, preferredType). ✅
  2. Return input. ✅
OrdinaryToPrimitive(O, hint)  O := [73]  hint := number

The abstract operation OrdinaryToPrimitive takes arguments O (an Object) and hint (string or number) and returns either a normal completion containing an ECMAScript language value or a throw completion. It performs the following steps when called:

  1. If hint is string, then ❌
    1. Let methodNames be « "toString", "valueOf" ».
  2. Else, ✅
    1. Let methodNames be « "valueOf", "toString" ». ✅
  3. For each element name of methodNames, do ✅
    1. Let method be ? Get(O, name). ✅
    2. If IsCallable(method) is true, then ✅
      1. Let result be ? Call(method, O). ✅
      2. If Type(result) is not Object, return result. ⚠️
  4. Throw a TypeError exception.

We're getting deep here, but we've almost reached the botom. By the point marked ⚠️, [[3.2.2]], valueOf for an array will return the array itself, which still has an Object type. Therefore the loop [[3.]] continues with name := "toString"

O := [73]  name := "toString"
  1. Let method be ? Get(O, name). ✅
  2. If IsCallable(method) is true, then ✅
    1. Let result be ? Call(method, O). ✅
    2. If Type(result) is not Object, return result. ✅
OrdinaryToPrimitive(O, hint)  O := [73]  hint := number
Return => "73"
ToPrimitive(input[, preferredType])  input := [73], preferredType := number
Return => "73"
ToNumeric(value)  value := [73]
Return => ToNumber("73")
ToNumber(argument)  argument := "73"

The abstract operation ToNumber takes argument argument and returns either a normal completion containing a Number or a throw completion. It converts argument to a value of type Number according to Table 13 (below):

Argument Type Result
Undefined Return NaN.
Null Return +0.
Boolean If argument is true, return 1. If argument is false, return +0.
Number Return argument (no conversion).
String Return ! StringToNumber(argument). ✅
Symbol Throw a TypeError exception.
BigInt Throw a TypeError exception.
Object Apply the following steps:
... 1. Let primValue be ? ToPrimitive(argument, number).
... 2. Return ? ToNumber(primValue).

We reach StringToNumber("73") and now there's little point continuing down the rabbit hole. This entire can of worms was opened due to your self-inflicted choice of a bad data structure. Want to get the person's weight?

const person = { name: "rachel", weight: 73 }
console.log(person.weight) // 73

No unnecessary intermediate arrays, no array-to-string conversion, no string-to-number conversion, no possibility of NaN, no hell.

read more

Repeat the "hell" exercise for each of the other functions you are using. Determine for yourself if this is really the path you want to be on -

function composition

Curried functions are paired well with another technique called function composition. When a function takes just one argument and returns another, you can compose or sequence them, sometimes called "pipes" or "pipelines". This begins to demonstrate the effects of functional programming when applied to an entire system -

const gte = (x = 0) => (y = 0) =>
  y >= x

const filter = (f = Boolean) => (a = []) =>
  a.filter(f)
  
const prop = (k = "") => (o = {}) =>
  o[k]
  
const pipe = (...fs) =>
  x => fs.reduce((r, f) => f(r), x)
  
const heavyWeights =
  filter(pipe(prop("weight"), gte(100)))

const people = [
  { name: "john", gender: "male", weight: 100 },
  { name: "amanda", gender: "female", weight: 88 },
  { name: "rachel", gender: "female", weight: 73 },
  { name: "david", gender: "male", weight: 120 }
]

console.log(heavyWeights(people))
.as-console-wrapper { min-height: 100%; top: 0; }
[
  {
    "name": "john",
    "gender": "male",
    "weight": 100
  },
  {
    "name": "david",
    "gender": "male",
    "weight": 120
  }
]

If you found this section interesting, I invite you to read How do pipes and monads work together in JavaScript?

Mulan
  • 129,518
  • 31
  • 228
  • 259
  • Thanks, this is eye opening. I have several questions, though. Here's one: `people` object is a nice tabular presentation of the data (similar to a *data frame* in other languages like *R* and *Python*). However, is it mandatory to use such data structure when we want to utilize partial application? The way I understand it, partial application is all about fixing certain function arguments, regardless of the input data passed in other args, no? – Emman Apr 22 '22 at 12:32
  • @Emman there's no requirement to change the type, but you're in for a world of hurt if you continue down this path. It's important you understand the fundamentals of what your current program is doing. I made an extensive edit to my post in hopes to clear some fo this up. If you have any questions, please follow up :D – Mulan Apr 22 '22 at 16:12
  • thank you for elaborating, this is remarkable. When I wrote `funcA()` & `funcB()`, I addressed the *specific* data I had in hand. I didn't shoot for any extended version of data, so everything was tailored just for `genderAndWeight`. By contrast, your rundown aims to create generic functions that work beyond the scope of this very specific data. So this makes think of 2 questions. 1) if we just stick with this specific `genderAndWeight` data, is it still a bad idea to use my `funcA()`/`funcB()`?; and 2) does FP-style necessarily mean aiming to write generic functions rather than tailored ones? – Emman Apr 23 '22 at 08:50
  • 1
    1) Is it a "bad" idea? i would say yes due to abnormal use of objects. Object keys are meant to describe the values that follow them, but you are writing `{ : }` which has no relation. If you are looking for a compact representation, you could try `[, ]` which gives uniform access to the data members, `x[0]` and `x[1]`. All of that implicit type coercion from original `funcA` and `funcB` goes away. 2) Absolutely! a generic function is easy to reuse in many parts of your program, or imported into others. Tailored functions are specializations of generics. – Mulan Apr 23 '22 at 12:29
  • If you are interested, I can link some posts that assemble generic functions to solve complex/specialized problems. Have a nice day :D – Mulan Apr 23 '22 at 12:38
  • Thanks, I'm mostly intrigued by the structure you proposed in `people`. Indeed, `pick()` is an appealing generic function that can be partially applied nicely. By the same token, I wonder how a `filter()` function would look like, e.g. *return `people` whose `weight` is equal or larger than `100`*. If you happen to know posts that demonstrate such utility functions for `people`-like data, I'd be grateful! – Emman Apr 24 '22 at 12:41
  • Of note, I *am* familiar with JavaScript libraries that attempt to promote data analysis in JS. Examples are: [tidy.js](https://pbeshai.github.io/tidy/docs/getting_started), [Data-Forge](https://github.com/data-forge/data-forge-ts/blob/master/docs/guide.md#dataframe), [Arquero](https://github.com/uwdata/arquero), and [polars](https://github.com/pola-rs/polars). While impressive and appealing, I do wary of relying on such libraries because a) maintainers may stop maintaining, and b) my collaborators aren't trained on those APIs. Your `people` and `pick()` are vanilla JS and thus much better. – Emman Apr 24 '22 at 12:57
  • 1
    @Emman i added a section to the answer titled _function composition_ that details your `filter` example. i agree with your opinion about external libraries and avoid dependencies wherever possible in my programs. many module authors turn a blind eye to the weight an added dependency can bring. npm shows number of dependents but a tool like [bundlephobia](https://bundlephobia.com) can give you even better insight on how bloated a particular package is. – Mulan Apr 24 '22 at 20:08