在函数式编程中何时选择无点样式与以数据为中心的样式比较合适?

时间:2018-12-07 13:33:26

标签: javascript functional-programming ramda.js pointfree

在这种情况下,这与JavaScript中的函数式编程有关,在我的示例中,我将使用Ramda。

虽然每个人都完全接受函数式编程,但是围绕如何“正确”进行编程也有很多讨论。

这两个函数将执行完全相同的操作:获取一个列表并返回一个新列表,其中所有字符串均已被修剪。

// data-centric style
const trimList = list => R.map(R.trim, list);
// point-free style
const trimList = R.map(R.trim);

到目前为止,一切都很好。但是,在一个更复杂的示例中,两种样式之间的区别是惊人的:获取一个列表并返回一个新列表,其中所有字符串都等于对象中找到的属性。

var opts = {a: 'foo', b: 'bar', c: 'baz'}; 
var list = ['foo', 'foo', 'bar', 'foo', 'baz', 'bar'];

myFilter(opts, 'a', list); //=> ["foo", "foo", "foo"]
myFilter(opts, 'b', list); //=> ["bar", "bar"]
// data-centric style
const myFilter = (opts, key, list) => {
  var predicate = R.equals(opts[key]);
  return R.filter(predicate, list);
};
// point-free style
const myFilter = R.converge(
  R.filter, [
    R.converge(
      R.compose(R.equals, R.prop), [
        R.nthArg(1),
        R.nthArg(0)]),
    R.nthArg(2)]);

除了可读性和个人品味之外,是否有可靠的证据表明一种风格在某些情况下比另一种更合适?

3 个答案:

答案 0 :(得分:5)

学术术语是 eta转化。当您具有带有冗余lambda抽象的函数时,例如

const trim = s => s.trim();
const map = f => xs => xs.map(x => f(x));

const trimList = xs => map(trim) (xs); // lambda redundancy

您只需通过eta简化即可去除最后的lamdba抽象:

const trimList = map(trim);

广泛使用 eta缩减时,最终会出现无点样式。但是,这两种版本在功能范式上都很好。只是样式问题。

实际上,至少有两个原因在Javascript中使用 eta抽象(与 eta还原相反):

  • 修复Java的多参数函数,就像我对map = f => xs => xs.map(x => f(x))所做的那样
  • 防止像recur = f => x => f(recur(f)) (x)中那样立即评估表达式/语句(懒惰的评估效果)

答案 1 :(得分:2)

我不知道有证据显示一种风格比另一种风格更具优势。但是,在编程史上,向更高的抽象趋向于明显……而对这种趋势的抵制也同样清晰。从Assembly迁移到Fortran或LISP就是在抽象堆栈上移动。使用SQL而不是定制B树检索是另一回事。在我看来,无论是在Javascript之类的语言中还是在不断变化的编程语言环境中,向FP的迁移都是类似的举动。

但是,其中大部分与比该句法决策更基本的元素有关:方程式推理意味着我们可以在更坚实的基础上构建自己的抽象。因此,纯度和不变性至关重要。没有积分只是很高兴。

也就是说,它通常更简单。这很重要。更简单的代码更易于阅读,更易于修改。请注意,我区分了简单简单-经典talk by Rich Hickey明确表达了这一区别。那些新风格的人通常会觉得更混乱;厌恶下一代语言及其所有同类语言的汇编程序员也是如此。

通过不定义中间变量,甚至不指定可以推断的参数,我们可以显着提高简单性。

很难争辩:

const foo = (arg) => {
  const qux = baz(arg)
  return bar(qux)
}

甚至是这个:

const foo = (arg) => bar(baz(arg))

比这简单:

const foo = compose(bar, baz)

那是因为尽管所有三个都涉及这些概念:

  • 函数声明
  • 功能参考

第二个也添加:

  • 参数定义
  • 功能主体
  • 功能应用程序
  • 嵌套函数调用

第一个版本具有:

  • 参数定义
  • 功能主体
  • 功能应用程序
  • 局部变量定义
  • 局部变量赋值
  • return语句

而第三个仅添加

  • 功能组成

如果简单意味着缠结的概念更少,那么即使某些人不太熟悉,无点版本也会更简单。


最后,大部分归结为可读性。与编写代码相比,您花费更多的时间阅读自己的代码。其他人会花很多时间 来阅读它。如果您编写的代码简单易读,那么每个人的体验都会更好。因此,在更容易理解无点代码的地方,请使用它。

但是,不必为了消除一个点而删除每个点。很容易陷入试图使所有内容变得毫无意义的陷阱,因为您可以做到。我们已经知道这是可能的。我们不需要查看血腥细节。

答案 2 :(得分:1)

有一些很好的答案,我认为,将两种样式混合在一起是可行的方法。

最后一个无点样式示例有点混乱,您可以减少混乱:

const myFilter = converge(
  filter,
  [compose(equals , flip(prop)) , nthArg(2)]
 )