在函数式编程中,何时选择点免风格和数据中心风格是合适的?

6

如果有关注,这篇文章是关于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)]);

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

2
这是一个基于观点的问题,根据规则不允许。话虽如此,这并没有争议。强制使用无点风格是没有意义的。通常人们会进行 eta-reduce,即将 λx. f x 转换为 f,如果没有问题的话(当 f 未定义时,在 call-by-value 中语义不同)。但除此之外就没有什么了…… - Jorge Adriano Branco Aires
1
@customcommander,我认为你的问题很有趣也很重要...但不适合在这里讨论。 - Jared Smith
@JaredSmith,我已经改掉了我的问题。 - customcommander
1
我同意关闭这个问题,尽管我也喜欢它,并且想给出我的答案,但是它太长了,不适合作为评论,而且大部分是对@JorgeAdriano评论的扩展。如果您再次尝试并将重点放在“何时选择无点风格与数据中心风格更合适?”可能不会被关闭。虽然这仍然是基于观点的,但我相信它被“建设性”例外所覆盖。最终,重点应该放在代码的可读性上。 - Scott Sauyet
感谢@ScottSauyet的建议,我已经按照您的建议重新措辞了我的问题,并删除了一个可能被视为主观的段落。现在只需要更多的投票来重新打开这个问题。 - customcommander
显示剩余7条评论
3个回答

8
我不知道有任何证据表明其中一种风格优于另一种。但是编程历史上的趋势非常明显,朝着更高的抽象化方向发展……同样也有一个明显的反对这种趋势的历史。从汇编语言到Fortran或LISP的转变是向抽象化堆栈上升的一步。使用SQL而不是自定义B-树检索是另一种。在编程语言中使用FP,例如Javascript,以及编程语言的不断变化,对我来说是类似的转变。

但其中很多因素都比这个语法决策更基本:等式推理意味着我们可以在更坚实的基础之上构建自己的抽象。因此纯洁性和不可变性至关重要;点无关仅仅是美好的愿望。

尽管如此,它通常更简单。这很重要。简单的代码更容易阅读,更容易修改。请注意,我区分了“简单”和“易”的区别——Rich Hickey的经典演讲中明确表达了这一区别。那些刚接触这种风格的人通常会发现它更加混乱;与此类似的是汇编程序员,他们憎恶下一代语言及其同类。

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

很难争辩这一点:

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

甚至可以是这样的:
const foo = (arg) => bar(baz(arg))

比这个简单:

const foo = compose(bar, baz)

这是因为虽然所有三个都涉及以下概念:
函数声明
函数引用
第二种方式还增加了以下内容:
参数定义
函数体
函数应用
函数调用的嵌套
第一个版本有:
参数定义
函数体
函数应用
本地变量定义
本地变量赋值
返回语句
而第三个版本只增加了
函数组合
如果更简单意味着少一些交织的概念,那么无参版本会更简单,即使对一些人来说不太熟悉。
最终,这在很大程度上取决于可读性。你花费的时间阅读自己的代码比编写代码的时间更长。其他人花费的时间更多。如果你编写的代码简单易读,那么对每个人的体验都会更好。因此,在点无关代码更易读的情况下,请使用它。
但是不要觉得有必要删除每个点以证明一点。很容易陷入试图使所有内容都成为无关点的陷阱,只是因为你可以。我们已经知道这是可能的;我们不需要看到血腥的细节。

“保持简单”的难点在于它的相对性。对于经验丰富的开发人员和新手来说,它的含义完全不同。” - user10675354
2
@reify:我想表达的观点,以及Rich Hickey的演讲重点是,易用性 / 熟悉度 实际上是相对度量;而简单性实际上更加客观。 - Scott Sauyet

6
学术术语是 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 缩减方法轻松地去除最后的 lambda 抽象层:
const trimList = map(trim);

当您广泛使用Eta缩减时,您最终会得到点无样式。然而,在函数编程范式中,这两个版本都是完全可以的。这只是一个风格问题。
实际上,至少有两个原因在Javascript中使用Eta抽象(与Eta缩减相反):
- 修复Javascript的多参数函数,例如我使用map = f => xs => xs.map(x => f(x)) - 防止表达式/语句的立即评估(惰性评估效果),如recur = f => x => f(recur(f)) (x)

当你说“至少有两个原因使用 eta 抽象”时,你是指“eta 转换”吗? - customcommander
1
@customcommander 在编程中,eta抽象(添加函数)和eta规约(删除函数)都被归为一般术语eta转换 - user10675354

1

我认为混合两种风格是可行的,这里有一些好的答案。

最后一个无点风格示例有点令人困惑,你可以让它更清晰明了:

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接