I'm practicing partial application of a function, that is, fixing function arguments. I've learned two ways to achieve it:
.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.
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:
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
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:
- If
value
is present, then ✅
- Let
prim
be ? ToNumeric(value
). ✅- If Type(
prim
) is BigInt, letn
be 𝔽(ℝ(prim
)). ❌- Otherwise, let
n
beprim
. ✅- Else,
- Let
n
be +0𝔽.- If NewTarget is undefined, return
n
. ✅- Let
O
be ? OrdinaryCreateFromConstructor(NewTarget, "%Number.prototype%", « [[NumberData]] »).- Set
O.[[NumberData]]
ton
.- 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 returnsvalue
converted to a Number or a BigInt. It performs the following steps when called:
- Let
primValue
be ? ToPrimitive(value
, number). ✅- If Type(
primValue
) is BigInt, returnprimValue
. ❌- Return ? ToNumber(
primValue
). ✅
ToPrimitive(input[, preferredType]) input := [73], preferredType := number
The abstract operation ToPrimitive takes argument
input
(an ECMAScript language value) and optional argumentpreferredType
(string or number) and returns either a normal completion containing an ECMAScript language value or a throw completion. It converts itsinput
argument to a non-Object type. If an object is capable of converting to more than one primitive type, it may use the optional hintpreferredType
to favour that type. It performs the following steps when called:
- If Type(
input
) is Object, then ✅
- Let
exoticToPrim
be ? GetMethod(input
, @@toPrimitive). ✅- If
exoticToPrim
is not undefined, then ❌
- If
preferredType
is not present, let hint be "default".- Else if
preferredType
is string, let hint be "string".- Else,
- Assert:
preferredType
is number.- Let hint be "number".
- Let
result
be ? Call(exoticToPrim
,input
, « hint »).- If Type(
result
) is not Object, returnresult
.- Throw a TypeError exception.
- If
preferredType
is not present, letpreferredType
be number. ❌- Return ? OrdinaryToPrimitive(
input
,preferredType
). ✅- Return
input
. ✅
OrdinaryToPrimitive(O, hint) O := [73] hint := number
The abstract operation OrdinaryToPrimitive takes arguments
O
(an Object) andhint
(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:
- If
hint
is string, then ❌
- Let
methodNames
be « "toString", "valueOf" ».- Else, ✅
- Let
methodNames
be « "valueOf", "toString" ». ✅- For each element
name
ofmethodNames
, do ✅
- Let
method
be ? Get(O
,name
). ✅- If IsCallable(
method
) is true, then ✅
- Let result be ? Call(
method
,O
). ✅- If Type(result) is not Object, return result. ⚠️
- 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"
- Let
method
be ? Get(O
,name
). ✅- If IsCallable(
method
) is true, then ✅
- Let result be ? Call(
method
,O
). ✅- 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 convertsargument
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𝔽. Ifargument
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?