【问题标题】:What's the cleanest fp way to get a property pointed by another property获取另一个属性指向的属性的最干净的 fp 方法是什么
【发布时间】:2018-10-03 10:40:14
【问题描述】:

给定一个可能为空且可能具有以下属性的对象:

{
  templateId: "template1",
  templates: {
    template1: "hello"
  }
}

如何以故障安全的方式获取模板? (可能没有定义templateId,或者它引用的模板可能未定义)

我使用 ramda 并试图调整我的原始代码版本以使用可能的广告来避免显式的空/未定义检查。

我没有想出一个优雅而干净的解决方案。

朴素的 ramda 版本:

const getTemplate = obj => {
  const templateId = obj && prop("templateId", obj);
  const template = templateId != null && path(["template", templateId], obj);
  return template;
}

这确实有效,但我想避免空值检查,因为我的代码还有很多事情要做,而且变得更干净会非常好

编辑 我从几个答案中得到最好的答案是首先确保数据干净。 但这并不总是可能的。 我也想出了这个,我很喜欢。

const Empty=Symbol("Empty"); 
const p = R.propOr(Empty);
const getTemplate = R.converge(p,[p("templateId"), p("templates")]);

希望获得有关它的干净程度和可读性的反馈(以及是否存在会破坏它的边缘情况)

【问题讨论】:

  • “给定一个可能为空且可能具有以下属性的对象”排除了“干净”解决方案的可能性。 :P
  • 每个人都在指出您的输入数据如何成为问题。他们是对的。但是我经常要处理这样的数据,来自遗留系统,来自很久以前被大型企业选择的第三方供应商提供的不一致的 API,来自另一个团队中不了解如何正确使用的初级开发人员来自各种地方的错误响应或空值。你的目标是解决这个问题。如果你有这样的数据,也许你自己的第一步就是清理它。如果您以map(defaultTo({templates: []})) 开头,您将消除您的几个nil 检查。
  • 抱歉,map 是我自己解决问题的方法,不值得发帖。我使用了来自 user633183 的解决方案中的值列表。根据您的解决方案,您可能只使用其中的 defaultTo 部分。
  • 我明白了@davidchambers、@scott-sauyet、@user633183 感谢您的回答,感谢您抽出宝贵的时间!我喜欢这种解决方案:const Empty=Symbol("Empty"); const p = R.propOr(Empty);const getTemplate = R.converge(p,[p("templateId"), p("templates")]);,但我真的很想得到像你这样有经验和聪明的人的意见,因为我觉得它有点难以阅读。

标签: javascript functional-programming ramda.js sanctuary


【解决方案1】:

正如其他人告诉您的,丑陋的数据排除了精美的代码。清理您的空值或将它们表示为选项类型。

也就是说,ES6 确实允许您通过一些繁重的解构分配来处理这个问题

const EmptyTemplate =
  Symbol ()

const getTemplate = ({ templateId, templates: { [templateId]: x = EmptyTemplate } }) =>
  x
  
console.log
  ( getTemplate ({ templateId: "a", templates: { a: "hello" }}) // "hello"
  , getTemplate ({ templateId: "b", templates: { a: "hello" }}) // EmptyTemplate
  , getTemplate ({                  templates: { a: "hello" }}) // EmptyTemplate
  )

您可以继续让getTemplate 更具防御性。例如,下面我们接受使用空对象调用我们的函数,甚至根本没有输入

const EmptyTemplate =
  Symbol ()

const getTemplate =
  ( { templateId
    , templates: { [templateId]: x = EmptyTemplate } = {}
    }
  = {}
  ) =>
    x
 
console.log
  ( getTemplate ({ templateId: "a", templates: { a: "hello" }}) // "hello"
  , getTemplate ({ templateId: "b", templates: { a: "hello" }}) // EmptyTemplate
  , getTemplate ({                  templates: { a: "hello" }}) // EmptyTemplate
  , getTemplate ({})                                            // EmptyTemplate
  , getTemplate ()                                              // EmptyTemplate
  )

上面,我们开始感到有点痛苦。这个信号很重要,不要忽视,因为它警告我们做错了什么。如果您必须支持这么多空检查,则表明您需要收紧程序其他区域的代码。逐字复制/粘贴其中任何一个答案并错过每个人都试图教给您的课程是不明智的。

【讨论】:

  • 好一个。虽然它不适用于空 obj。如果碰巧得到这样的对象,也会给出一个奇怪的输出:{templates:{undefined:1}}(我赞成很好地使用解构)
  • @Tiago,我更新了我的答案。您是否看到每个空检查如何使问题复杂化?这里的每个人都想给你上一堂宝贵的课——问题从你的输入开始。
  • @TiagoCoelho ie,如果你放弃这种想法“很好地使用解构”,你确实错过了这一课。
  • 数据可能不干净......如果我理解正确,你告诉我我需要过滤数据。我可以理解这一点,我同意。我认为你不应该假设这表明我的程序的其他部分有任何错误的代码。我知道每个空检查都会加剧问题......这就是我在这里要求输入的一个原因。我希望看到一些可以很好地满足这些要求的 Maybe 数据类型(或类似数据类型)的使用。
【解决方案2】:

这里是 vanilla Javascript 中的 ADT 方法:

// type constructor

const Type = name => {
  const Type = tag => Dcons => {
    const t = new Tcons();
    t[`run${name}`] = Dcons;
    t.tag = tag;
    return t;
  };

  const Tcons = Function(`return function ${name}() {}`) ();
  return Type;  
};

const Maybe = Type("Maybe");

// data constructor

const Just = x =>
  Maybe("Just") (cases => cases.Just(x));

const Nothing =
  Maybe("Nothing") (cases => cases.Nothing);

// typeclass functions

Maybe.fromNullable = x =>
  x === null
    ? Nothing
    : Just(x);

Maybe.map = f => tx =>
  tx.runMaybe({Just: x => Just(f(x)), Nothing});

Maybe.chain = ft => tx =>
  tx.runMaybe({Just: x => ft(x), Nothing});

Maybe.compk = ft => gt => x => 
  gt(x).runMaybe({Just: y => ft(y), Nothing});

// property access

const prop =
  k => o => o[k];

const propSafe = k => o =>
  k in o
    ? Just(o[k])
    : Nothing;

// auxiliary function

const id = x => x;

// test data

// case 1

const o = {
  templateId: "template1",
  templates: {
    template1: "hello"
  }
};

// case 2

const p = {
  templateId: null
};

// case 3

const q = {};

// case 4

const r = null; // ignored

// define the action (a function with a side effect)

const getTemplate = o => {
  const tx = Maybe.compk(Maybe.fromNullable)
    (propSafe("templateId"))
      (o);

  return Maybe.map(x => prop(x) (o.templates)) (tx);
};

/* run the effect,
   that is what it means to compose functions that may not produce a value */


console.log("case 1:",
  getTemplate(o).runMaybe({Just: id, Nothing: "N/A"})
);

console.log("case 2:",
  getTemplate(p).runMaybe({Just: id, Nothing: "N/A"})
);

console.log("case 3:",
  getTemplate(q).runMaybe({Just: id, Nothing: "N/A"})
);

如您所见,我使用函数来编码 ADT,因为 Javascript 在语言级别上不支持它们。这种编码称为 Church/Scott 编码。 Scott 编码在设计上是不可变的,一旦您熟悉了它,它的处理就是小菜一碟。

Just 值和Nothing 都是Maybe 类型,并包含一个tag 属性,您可以在该属性上进行模式匹配。

[编辑]

自从 Scott(不是刚才的编码人员)和 OP 要求提供更详细的答复后,我扩展了我的代码。我仍然忽略对象本身是null 的情况。您必须在前面的步骤中处理此问题。

您可能会认为这是过度设计的 - 对于这个人为的示例肯定是这样。但是当复杂性增加时,这些功能样式可以减轻痛苦。另请注意,我们可以使用这种方法处理各种效果,而不仅仅是null 检查。

例如,我目前正在构建一个 FRP 解决方案,它基本上基于相同的构建块。这种模式的重复是我不想再没有的功能范式的特征之一。

【讨论】:

  • 你能像我的原始代码一样扩展它来实际获取模板吗? (我给了你一票教我一些新东西)
  • 这种方式只适用于templateId属性,可能是null。如果对象本身可能是null,则生成该对象的计算也必须返回Maybe
  • 是的,关键是你怎么写这个...我在我的原始代码中所做的就是获取templateId,然后获取对应的模板。您的答案与仅使用 ramda 的 propOr 大致相同。 propOr("N/A","templateId", o)
  • 您可以简单地定义mapapchain 实例来组合纯函数或链式动作。对于Maybe ADT,它们很简单。在这方面,我的方法比 Ramda 更具表现力,因为它依赖于类型,因此可以依赖于重载函数。
  • @ftor:我也很想看到这个完成以解决最初的问题。这是非常好的。
【解决方案3】:

您可以使用R.pathOr。只要路径的任何部分不可用,就会返回默认值。例如:

const EmptyTemplate = Symbol();

const getTemplateOrDefault = obj => R.pathOr(
  EmptyTemplate,
  [ "templates", obj.templateId ],
  obj
);

可以在this snippet 中找到一系列测试。该示例显示pathOr 可以很好地处理所有(?)“错误”情况:

const tests = [
  { templateId: "a",  templates: { "a": 1 } }, // 1
  {                   templates: { "a": 1 } }, // "empty"
  { templateId: "b",  templates: { "a": 1 } }, // "empty"
  { templateId: null, templates: { "a": 1 } }, // "empty"
  { templateId: "a",  templates: {        } }, // "empty"
  { templateId: "a"                         }  // "empty"
];

编辑:要支持nullundefined 输入,您可以使用快速defaultTo 组合方法:

const templateGetter = compose(
  obj => pathOr("empty", [ "templates", obj.templateId ], obj),
  defaultTo({})
);

【讨论】:

  • 如果 obj 本身为 null 或未定义怎么办?
  • 啊,我忘记了这个要求......我编辑了我的答案,但我不得不承认它确实使它更难阅读......
【解决方案4】:

试试这个,

const input = {
  templateId: "template1",
  templates: {
    template1: "hello"
  }
};

const getTemplate = (obj) => {
    const template = obj.templates[obj.templateId] || "any default value / simply remove this or part";
    //use below one if you think templates might be undefined too,
    //const template = obj.templates && obj.templates[obj.templateId] || "default value"
    return template;
}
console.log(getTemplate(input));

您可以使用 && 和 || 的组合使表达式短路。

此外,如果键存储在变量中,则对对象使用 [](而不是 .)来获取值。

完成检查

const getTemplate = (obj) => {
    const template = obj && obj.templateId && obj.templates && obj.templates[obj.templateId] || "default value"
    return template;
}

【讨论】:

  • 这不适用于 null obj,也不适用于像这样的对象:{templates:{undefined:1},a:2}
  • @TiagoCoelho。就像我说的,我们可以使用 && 和 || 的组合当变量未定义/为空时检查条件。所以对于null obj,我们可以这样做,obj && obj.templates[obj.templateId] || “默认值”。对于您给出的示例,预期的输出应该是什么?由于 templateId 未定义,上述 sn-p 将输出为 1。如果 1 不是正确答案,那么您可以这样做,例如 obj && obj.templates && obj.templateId && obj.templates[obj.templateId] || “默认值”。我会更新我的答案以反映这一点。
  • 感谢您的回答。我的意思是,我在询问一个没有显式检查的优雅而干净的解决方案,添加这些检查会使代码意图淡化。当“templateId”未定义时,我的版本将返回未定义,这也是我所期望的。 (或一些默认值/“N/A”)
猜你喜欢
  • 2011-05-09
  • 1970-01-01
  • 2020-11-08
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-11-06
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多