Search code examples
javascriptecmascript-6functional-programmingcurryingpartial-application

Why does partial application work when currying but not with .bind()?


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

Solution

  • 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?