在函数式编程中什么时候选择无点风格与以数据为中心的风格比较合适?

When is it appropriate to choose point-free style vs a data-centric style in functional programming?

如果重要的话,这是关于 JavaScript 中的函数式编程,在我的示例中,我将使用 Ramda。

虽然工作中的每个人都完全接受了函数式编程,但也有很多关于如何“正确”地做到这一点的讨论。

这两个函数将做完全相同的事情:获取一个列表并return一个新列表,其中所有字符串都已被修剪。

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

到目前为止一切顺利。然而,对于一个更复杂的示例,两种样式之间的差异是惊人的: 获取列表和 return 一个新列表,其中所有字符串都等于在对象中找到的 属性 .

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)]);

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

学术术语是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缩减相反)至少有两个原因:

  • 修复 Javascript 的多参数函数,就像我对 map = f => xs => xs.map(x => f(x))
  • 所做的一样
  • 防止expressions/statements立即求值(惰性求值效果),如recur = f => x => f(recur(f)) (x)

据我所知,没有证据表明一种风格优于另一种风格。但是在编程的历史上有一个明显的趋势是更高级的抽象……以及同样明显的抵制这种趋势的历史。从 Assembly 迁移到 Fortran 或 LISP 是在抽象堆栈中向上移动。使用 SQL 而不是定制的 B 树检索是另一回事。在我看来,无论是在 Javascript 这样的语言中还是在不断变化的编程语言环境中,转向 FP 都是类似的举措。

但其中大部分与比句法决定更基本的元素有关:等式推理意味着我们可以在更坚实的基础上构建自己的抽象。所以纯度和不变性是必不可少的;无积分只是很好。

也就是说,它通常更简单。这很重要。更简单的代码更容易阅读,更容易修改。请注意,我区分了 simpleeasy —— 经典 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 声明

而第三个只添加

  • 功能组合

如果更简单意味着更少的概念纠缠在一起,那么无点版本更简单,即使某些人不太熟悉它。


最后,这很大程度上归结为可读性。与编写代码相比,您花在阅读自己的代码上的时间更多。其他人会花 很多 的时间阅读它。如果您编写的代码简单易读,那么每个人的体验都会好很多。所以在无点代码更易读的地方,使用它。

但是,为了证明一个观点,咳咳,没有必要删除每一个观点。很容易掉进陷阱,试图让一切都变得毫无意义,只是因为你可以。我们已经知道这是可能的;我们不需要看到血淋淋的细节。

有一些很好的答案,在我看来,混合两种风格是可行的方法。

最后一个无点风格的例子有点混乱,你可以让它不那么混乱:

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