【问题标题】:How to curry a function that takes an options object as argument rather than several distinct arguments?如何curry一个将选项对象作为参数而不是几个不同参数的函数?
【发布时间】:2017-10-21 08:20:10
【问题描述】:

过去几天我一直在研究部分应用程序和currying。

我想知道如何将这些概念与只接受一个 options 对象作为参数的函数一起使用。

const myFunc = options => {
  const options.option1 = options.option1 || 'default value';
  const options.option2 = options.option2 || 'another default value';
  // ... etc, it takes about 5 more options, all of which have a
  // default fall-back value if not supplied

  return doSometing(options);
}

在这种情况下,我不喜欢更改 myFunc 签名并将每个选项作为单独的参数传递,因为记住必须提供选项的顺序很痛苦。

我想坚持函数式风格,避免使用new ... 实例化对象只是为了保持状态;我有一种预感,这可以通过部分应用来实现。当需要测试或实例化时,它可以让事情变得更简单。

但是,我不知道如何在没有单独参数的情况下正确执行部分应用程序。

你会如何处理这个重构?

【问题讨论】:

标签: javascript functional-programming ecmascript-6 currying ramda.js


【解决方案1】:

我认为您的问题与部分应用无关。 myFunc到底做了什么?

  • 它设置了几个可选的默认值
  • 它调用另一个函数

这并不多。然而出现了两个问题:

  • 函数组合被硬编码并隐藏在myFunc的正文中
  • 从函数签名中看不出哪些默认值被覆盖

简单地说,一个“适当的”函数通过它的签名来揭示它的功能。所以让我们摆脱 myFunc 包装器:

const options = {
  foo: 1,
  bar: true,
  bat: "",
  baz: []
};

// function composition

const comp = (...fs) => x => fs.reduceRight((acc, f) => f(acc), x);

// applicator for partial application with right section

const _$ = (f, y) => x => f(x) (y); // right section

// `Object` assignment

const assign = o => p => Object.assign({}, o, p);

// specific function of your domain

const doSomething = o => (console.log(o), o);

// and run (from right-to-left)

comp(doSomething, _$(assign, {baz: [1, 2, 3], bat: "abc"})) (options);

现在您无需查看函数体即可准确了解发生了什么。选项Object 的属性顺序也不重要。

_$ 的评论。它有这个奇怪的名称,因为在这种特殊情况下,我更喜欢视觉名称而不是文本名称。给定函数sub = x => y => x - y_$(sub, 2) 仅表示x => x - 2。这称为部分应用的sub 函数的右侧部分,因为仍然缺少“left”参数。

【讨论】:

  • 老兄,这个答案是一锤子圣牛。我已经阅读了很多关于 fp 的文章(最近才尝试严格实施),但我大多只是遇到了理论,并没有太多的实施。仅从您的回答中,我了解到:并非所有东西都需要咖喱;有时部分应用更有意义,应用程序是一个函数,它接受一个函数并将其应用于一个值,以及如何使用默认值。令人难以置信的答案!我不敢相信它没有更多的赞成票!!!
【解决方案2】:

根据 Dan D 的回答和 cmets,此技术可让您重复部分应用此类函数,直到提供所有必填字段:

const vals = (obj) => Object.keys(obj).map(key => obj[key]);

const addDefaults = (() => {
  const req = Symbol();
  const addDefaults = (defaults, fn) => (options) => {
    const params = Object.assign({}, defaults, options);
    return (vals(params).includes(req)) 
      ? addDefaults(params, fn)
      : fn(params);
  };
  addDefaults.REQUIRED = req;
  return addDefaults;
})();


const order = addDefaults({
  color: addDefaults.REQUIRED,
  size: addDefaults.REQUIRED,
  giftWrap: false,
  priority: false
}, (opts) => 
   `${opts.size}, ${opts.color}, ${opts.giftWrap ? '' : 'no'} wrap, priority: ${opts.priority}`
);

order({color: 'Red', size: 'Medium'}); // "Medium, Red, no wrap, priority: false"

const orderLarge = order({size: 'Large'}); // Options -> String
orderLarge({color: 'Blue'}); // "Large, Blue, no wrap, priority: false"

【讨论】:

    【解决方案3】:

    我建议将带有选项对象的函数柯里化与处理默认值的方法相同。将此视为部分应用程序:

    myFuncWithMyOptions(myFunc, myOptions) {
      return function(options) {
        return myFunc(Object.assign({}, myOptions, options));
      }
    }
    

    如果您希望myOptions 中的选项不被options 中的选项覆盖,只需将两个参数交换为Object.assign

    【讨论】:

    • 如果有一些选项属性没有默认值因此是必需的,您可以通过提供所需属性的名称列表或将它们包含在myOptions 中来扩展它一个值,表明没有它们的调用是不完整的(undefined 是可能的,如果它永远不可能是值,或者一些 Symbol 附加到 myFuncWithMyOptions。)如果发送了一个不完整的对象,你可以返回一个新的原始函数的包装版本,具有一组扩展的默认值和一组减少的必填字段。
    • @ScottSauyet 是的,这可能会使调试更容易,因为调用会更早失败。但它不会改变它的效果。
    • 嗯,我在想这会像其他形式的部分应用程序一样工作。如果您不提供所有必需的参数,您只需返回一个新函数,它将其选项与以前的选项(和默认值)合并并使用它们调用原始函数(假设您现在已经提供了所有必需的.)
    • @DanD。我认为斯科特在这里指的是柯里化。要 curry 一个常用函数,您需要知道它的数量,要“curry”一个选项对象函数,您需要知道它需要哪些属性。
    • 如果您被允许为同一选项提供不止一次的值或一次提供多个选项,那么即使有固定数量的属性,也没有固定的数量。那么 curry 是不合理的,而是必须明确地部分应用。
    猜你喜欢
    • 2022-01-27
    • 2020-05-16
    • 2013-03-20
    • 1970-01-01
    • 2011-06-10
    • 2016-11-19
    • 2021-05-19
    • 2020-12-23
    • 1970-01-01
    相关资源
    最近更新 更多