为什么部分应用程序在 currying 时有效,但在 .bind() 中无效?

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

我正在练习函数的部分应用,即固定函数参数。我学会了两种实现它的方法:

  1. 首先柯里化原始函数。
  2. 通过使用.bind()方法。

在下面的示例中,我将展示只有第一种策略,即首先 currying 有效。我的问题是为什么使用 .bind() 不起作用。

例子

考虑以下数据:

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

我想创建两个将此数据重新格式化为新对象的实用函数:

因为预计这两个函数非常相似,所以我想创建一个主函数,然后从中派生出两个版本,从而遵守 DRY 原则。

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

此解决方案的核心是我要提供给 fn 参数的内容。所以要么

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

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

现在正在做部分应用:

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

效果很好:

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' } }

到目前为止一切顺利。下面的方法用了.bind(),不行


// 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


还值得注意的是,在不同的情况下,.bind() 确实允许部分应用。例如:

const mySum = (x, y) => x + y;
const succ = mySum.bind(null, 1);
console.log(succ(3)); // => 4

来自哪里

柯里化和部分应用是函数式继承,因此在此上下文之外使用它们将阻止您获得它们的全部好处,并且可能成为 self-inflicted 混乱的根源。

提议的数据结构充满了问题,最大的问题是数据在数据对象的两个值 键之间混合。姓名、性别和体重都是 namegenderweight 是键。这会将您的数据更改为这种合理的形状,它也采用合理的名称,people

柯里化

pick 很容易实现它的目标,因为 namegenderweight 在语义上都是相邻的,即它们都是从对象中挑选的键。当数据跨值和键混合时,它会更难导航结构并给您的程序带来不必要的复杂性。

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 完全足以增进您对这一点的理解。您不需要 .bind,因为它的第一个参数与动态上下文有关,这是 object-oriented 风格的原则。

这是使用未柯里化 pick 并应用 partial 应用程序的相同演示 -

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; }

"是否强制更改数据结构?"

当然不会,但你很快就会运行惹上麻烦。让我们把你的练习进行到底,看看问题出在哪里。正如您所演示的,柯里化程序运行良好 -

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; }

您问题中的部分应用程序使用.bind不正确。上下文 (null) 作为第二个位置传递,但 .bind 期望此参数位于 第一个 位置 -

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

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

您可以用同样的方法修复 getGender2,但让我们用 partial 来代替这个。动态上下文是一种 object-oriented 机制,在学习函数式编程基础知识时无需关注它。 partial 允许您绑定函数的参数而无需提供上下文 -

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

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

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

这为您提供了两个使用原始建议数据结构的部分应用的工作示例 -

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; }

“那么问题出在哪里?”

就在这里-

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

“但它有效!”

您知道您的 funcA 创建一个数字数组,将其转换为字符串,然后再转换回数字吗?事实上,它似乎可以正常工作的唯一原因是因为每个人都是一个具有 单个 key/value 对的对象。一旦添加更多条目,模型就会中断 -

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

funcB 也发生了类似的问题。您的函数似乎可以正常工作,因为单个字符串 ["foo"] 的数组在转换为字符串时将导致 "foo"。在任何更大的阵列上尝试此操作,您将得到无法使用的结果 -

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"

当向树中添加更多数据时,funcAfuncB 将如何工作?

地狱再回来

我们知道 funcA 在原始数据中每个项目被调用一次。随机选择一个人,让我们看看当funcA达到rachel的值时会发生什么。到底有多糟糕,真的吗?

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.

我们正在深入这里,但我们几乎已经到达底部。到标记 ⚠️ 的点,数组的 [[3.2.2]]、valueOf 将 return 数组本身,它仍然具有 Object 类型。因此循环 [[3.]] 继续 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).

我们到达了 StringToNumber("73"),现在没有继续向下 the rabbit hole 的意义了。由于您 self-inflicted 选择了错误的数据结构,这整罐蠕虫都被打开了。想知道这个人的体重吗?

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

没有不必要的中间数组,没有array-to-string转换,没有string-to-number转换,没有NaN的可能性,没有地狱。

阅读更多

对您正在使用的每个其他功能重复“地狱”练习。自己确定这是否真的是您想要走的路 -

功能组合

柯里化函数与另一种称为 function composition 的技术搭配得很好。当一个函数只接受一个参数而 return 是另一个参数时,您可以 composesequence 它们,有时称为“管道&uot;或“管道”。这开始证明函数式编程应用于整个系统时的效果 -

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
  }
]

如果您觉得本节有趣,我邀请您阅读