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, let n
be (ℝ(prim
)). ❌
- Otherwise, let
n
be prim
. ✅
- Else,
- Let
n
be +0.
- If NewTarget is undefined, return
n
. ✅
- Let
O
be ? OrdinaryCreateFromConstructor(NewTarget, "%Number.prototype%", « [[NumberData]] »).
- Set
O.[[NumberData]]
to n
.
- 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:
- Let
primValue
be ? ToPrimitive(value
, number). ✅
- If Type(
primValue
) is BigInt, return primValue
. ❌
- 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:
- 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, return result
.
- Throw a TypeError exception.
- If
preferredType
is not present, let preferredType
be number. ❌
- Return ? OrdinaryToPrimitive(
input
, preferredType
). ✅
- 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:
- If
hint
is string, then ❌
- Let
methodNames
be « "toString", "valueOf" ».
- Else, ✅
- Let
methodNames
be « "valueOf", "toString" ». ✅
- For each element
name
of methodNames
, 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 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?