如何读取功能性JavaScript代码?


9

我相信我已经学到了JavaScript中的一些/许多/大多数基本的函数式编程基础。但是,我在阅读功能代码(甚至是我编写的代码)时遇到了麻烦,并且想知道是否有人可以给我提供任何可以提供帮助的指针,技巧,最佳实践,术语等。

使用下面的代码。我写了这段代码。它旨在在say {a:1, b:2, c:3, d:3}和之间分配两个对象之间的相似度百分比{a:1, b:1, e:2, f:2, g:3, h:5}。我针对Stack Overflow上的这个问题生成了代码。因为我不确定海报要问的相似度百分比,所以我提供了四种不同的相似度:

  • 在第二个对象中可以找到的第一个对象中键的百分比,
  • 在第二个对象中可以找到的第一个对象中值的百分比,包括重复项,
  • 在第二个对象中可以找到的第一个对象中的值的百分比,不允许重复,并且
  • 在第一个对象中可以找到的第一个对象中{key:value}对的百分比。

我以合理的命令性代码开始,但是很快意识到这是一个非常适合函数式编程的问题。尤其是,我意识到,如果我可以针对上述四种策略中的每一种提取出一个或三个函数,这些策略定义了我要比较的特征类型(例如键或值等),那么我可能会能够将其余的代码减少(请原谅文字的打法)为可重复的单元。要知道,保持干燥。所以我改用函数式编程。我为结果感到非常自豪,我认为它相当优雅,并且我认为自己做得很好。

但是,即使我自己编写了代码并在构造过程中理解了代码的每个部分,当我现在回头看时,我仍然对如何读取任何特定的半行以及如何读取都感到困惑。 “抱怨”任何特定的半行代码实际上在做什么。我发现自己制作了一些易用的箭头,将不同的部分连接起来,这些部分很快就会变成一团意大利面。

因此,谁能告诉我如何以简洁明了的方式“阅读”一些更复杂的代码,这有助于我对正在阅读的内容的理解?我猜最吸引我的部分是连续有几个粗箭头的部分和/或连续有多个括号的部分。同样,在它们的核心处,我最终可以弄清楚逻辑,但是(我希望)有一种更好的方法可以快速,清晰,直接地“接受”一系列功能性JavaScript编程。

随意使用下面的任何代码行,甚至其他示例。但是,如果您需要我的一些初步建议,以下是一些建议。从一个相当简单的开始。在代码的末尾,有一个作为参数传递给函数:obj => key => obj[key]。一个人如何阅读和理解呢?较长的示例是从一开始就提供的一项完整功能:const getXs = (obj, getX) => Object.keys(obj).map(key => getX(obj)(key));。最后map一部分让我特别感兴趣。

请注意,目前我不是在寻找Haskell或符号抽象符号或currying的基础知识等的参考。我寻找的是英文句子,我可以在看着一行代码时默默地开口。如果您有专门解决此问题的参考书,那很好,但是我也没有在寻找答案,我应该去读一些基础教科书。我已经做到了,我得到了(至少是很大一部分)逻辑。还要注意,我不需要详尽的答案(尽管可以尝试这样的尝试):即使是简短的答案,也提供了一种优雅的方式来读取一行特定的否则麻烦的代码,将不胜感激。

我想这个问题的一部分:我甚至线性阅读的功能代码,你就知道,左到右,上到下的?还是被迫在绝对不是线性的代码页上创建意粉般布线的心理图景?而且,如果必须这样做,我们仍然必须阅读代码,那么如何获取线性文本并连接意大利面呢?

任何提示将不胜感激。

const obj1 = { a:1, b:2, c:3, d:3 };
const obj2 = { a:1, b:1, e:2, f:2, g:3, h:5 };

// x or X is key or value or key/value pair

const getXs = (obj, getX) =>
  Object.keys(obj).map(key => getX(obj)(key));

const getPctSameXs = (getX, filter = vals => vals) =>
  (objA, objB) =>
    filter(getXs(objB, getX))
      .reduce(
        (numSame, x) =>
          getXs(objA, getX).indexOf(x) > -1 ? numSame + 1 : numSame,
        0
      ) / Object.keys(objA).length * 100;

const pctSameKeys       = getPctSameXs(obj => key => key);
const pctSameValsDups   = getPctSameXs(obj => key => obj[key]);
const pctSameValsNoDups = getPctSameXs(obj => key => obj[key], vals => [...new Set(vals)]);
const pctSameProps      = getPctSameXs(obj => key => JSON.stringify( {[key]: obj[key]} ));

console.log('obj1:', JSON.stringify(obj1));
console.log('obj2:', JSON.stringify(obj2));
console.log('% same keys:                   ', pctSameKeys      (obj1, obj2));
console.log('% same values, incl duplicates:', pctSameValsDups  (obj1, obj2));
console.log('% same values, no duplicates:  ', pctSameValsNoDups(obj1, obj2));
console.log('% same properties (k/v pairs): ', pctSameProps     (obj1, obj2));

// output:
// obj1: {"a":1,"b":2,"c":3,"d":3}
// obj2: {"a":1,"b":1,"e":2,"f":2,"g":3,"h":5}
// % same keys:                    50
// % same values, incl duplicates: 125
// % same values, no duplicates:   75
// % same properties (k/v pairs):  25

Answers:


18

您几乎很难阅读它,因为这个特定示例不太可读。无意冒犯,您在互联网上发现的令人沮丧的很大一部分样本也不是。很多人只在周末玩功能编程,而从不真正需要长期维护生产功能代码。我会这样写:

function mapObj(obj, f) {
  return Object.keys(obj).map(key => f(obj, key));
}

function getPctSameXs(obj1, obj2, f) {
  const mapped1 = mapObj(obj1, f);
  const mapped2 = mapObj(obj2, f);
  const same = mapped1.filter(x => mapped2.indexOf(x) != -1);
  const percent = same.length / mapped1.length * 100;
  return percent;
}

const getValues = (obj, key) => obj[key];
const valuesWithDupsPercent = getPctSameXs(obj1, obj2, getValues);

由于某些原因,很多人脑海中浮现出这样的想法,即功能代码应具有一定的美学“外观”,如大型嵌套表达式。请注意,尽管我的版本在所有分号上都与命令式代码有些相似,但所有内容都是不可变的,因此您可以替换所有变量,并根据需要获得一个大表达式。它的确与意大利面条版本一样“具有功能”,但可读性更高。

在这里,表达式被分成非常小的部分,并给出了对域有意义的名称。通过将通用功能(例如mapObj命名功能)提取来避免嵌套。Lambda保留用于非常短的功能,并且在上下文中有明确的用途。

如果遇到难以阅读的代码,请对其进行重构,直到更容易为止。这需要一些练习,但这是值得的。功能代码可以像命令一样可读。实际上,通常如此,因为它通常更简洁。


绝对不冒犯!虽然我仍然会保持在这个我知道一些关于函数式编程的东西,也许我的说法的问题大概有多少,我知道都有些夸大了。我真的是一个相对的初学者。因此,看到如何以一种简洁,清晰但仍能起作用的方式重写我的这一特殊尝试,似乎就像金子一样……谢谢。我会仔细研究您的改写。
安德鲁·威廉姆斯

1
我听说它说具有长链和/或方法嵌套可以消除不必要的中间变量。相反,您的答案使用命名良好的中间变量将我的链条/嵌套打断为独立的中间语句。在这种情况下,我发现您的代码更具可读性,但是我想知道您尝试的通用性。您是在说长方法链和/或深层嵌套经常或什至始终是要避免的反模式,或者有时它们会带来明显的好处?对于功能性编码还是命令式编码,该问题的答案是否有所不同?
Andrew Willems

3
在某些情况下,消除中间变量可以增加清晰度。例如,在FP中,您几乎从不需要索引到数组。有时中间结果也没有很好的名字。但是,以我的经验来看,大多数人都倾向于错误地选择其他方式。
Karl Bielefeldt

6

我并没有在Javascript中做过很多高功能的工作(我想这是-大多数谈论功能Java的人可能正在使用map,filters和reduces,但是您的代码定义了自己的高级函数,这是稍微有些高级),但是我在Haskell做到了,而且我认为至少有一些经验可以转化。我将给您一些指向我所学到的知识的指针:

指定功能的类型非常重要。Haskell不需要您指定函数的类型是什么,但是在定义中包含类型可以使它更易于阅读。尽管Javascript不以相同的方式支持显式键入,但是没有理由不在注释中包含类型定义,例如:

// getXs :: forall O, F . O -> (O -> String -> F) -> [F]
const getXs = (obj, getX) =>
    Object.keys(obj).map(key => getX(obj)(key));

通过使用一些类似的类型定义实践,它们使函数的含义更加清晰。

命名很重要,甚至比过程编程更重要。许多函数式程序都是用非常简洁的样式编写的,这种样式在约定上很繁琐(例如,“ xs”是一个列表/数组,而“ x”是其中的一个项目的约定非常普遍),但是除非您了解那种样式轻松地,我建议使用更详细的命名。查看您使用过的特定名称,“ getXs”有点不透明,因此“ getXs”也没有太大帮助。我将“ getXs”称为“ applyToProperties”,而“ getX”可能是“ propertyMapper”。这样,“ getPctSameXs”将是“ percentPropertiesSameWith”(“ with”)。

另一个重要的事情是编写惯用的代码。我注意到您正在使用一种语法a => b => some-expression-involving-a-and-b来生成咖喱函数。这很有趣,在某些情况下可能很有用,但是您在这里没有做任何可利用咖喱函数受益的事情,而改用传统的多参数函数会更惯用Javascript。这样做可以让您一目了然地看到正在发生的事情。您还const name = lambda-expression用于定义函数,而在其中更惯用function name (args) { ... }。我知道它们在语义上稍有不同,但是除非您依靠这些差异,否则我建议尽可能使用更常见的变体。


5
+1类型!仅仅因为语言没有它们,并不意味着您不必考虑它们。ECMAScript的一些文档系统具有用于记录功能类型的类型语言。几种ECMAScript IDE也具有类型语言(通常,它们也了解主要文档系统的类型语言),甚至可以使用这些类型注释执行基本的类型检查和启发式提示
约尔格W¯¯米塔格

您给了我很多建议:类型定义,有意义的名称,使用成语...谢谢!在众多可能的评论中,只有少数几个评论:我不一定打算将某些部分编写为咖喱函数;当我在编写代码时重构代码时,它们就以这种方式演变。我现在可以看到不需要它,甚至只是将这两个函数的参数合并为一个函数的两个参数,不仅更有意义,而且可以立即使这短一点的可读性更好。
安德鲁·威廉姆斯

@JörgWMittag,感谢您对类型重要性的评论以及您所写其他答案的链接。我使用WebStorm时并没有意识到,根据我对您的其他答案的了解,WebStorm知道如何解释类​​似jsdoc的注释。我从您的评论中假设jsdoc和WebStorm可以一起用于注释功能性代码,而不仅仅是命令性代码,但是我必须深入研究才能真正了解这一点。我以前玩过jsdoc,现在知道WebStorm并且可以在那儿合作了,我希望我会更多地使用该功能/方法。
安德鲁·威廉姆斯

@Jules,只是为了阐明我在上面的评论中指的是哪个咖喱函数:正如您所暗示的,的每个实例obj => key => ...都可以简化为,(obj, key) => ...因为以后getX(obj)(key)也可以简化为get(obj, key)。相反,(getX, filter = vals => vals) => (objA, objB) => ...至少在所编写的其余代码的上下文中,不能轻易简化另一个咖喱函数。
安德鲁·威廉姆斯
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.