如何在不失去可读性的情况下在JavaScript中使用无点风格



当我尝试用无点风格编写JavaScript时,我发现如果强制使用这种风格的每个函数,有时会失去其可读性。例如:

import R from 'ramda'
const ceil = Math.ceil
const pagination = {
total: 101,
itemsPerPage: 10,
currentPage: 1
}
// ================= Pointful style ==================
const pageCount = (pagination) => {
const pages = ceil(pagination.total / pagination.itemsPerPage)
const remainPages = pagination.total % pagination.itemsPerPage === 0 ? 0 : 1
return pages + remainPages
} 
pageCount(pagination) // => 11
// ================ Pointfree style ==================
const getPages = R.pipe(
R.converge(R.divide, [R.prop('total'), R.prop('itemsPerPage')]),
ceil
)
const getRemainPages = R.ifElse(
R.pipe(
R.converge(R.modulo, [R.prop('total'), R.prop('itemsPerPage')]),
R.equals(0)
),
R.always(0),
R.always(1)
)
const pageCount2 = R.converge(R.add, [
getPages,
getRemainPages
])
pageCount2(pagination) // => 11

我写了一个简单的分页模块来计算pageCount,以有点风格和无点风格给出总项目数和每页项目数。显然,有点样式比无点样式版本可读性更强。后者有点晦涩难懂。

我做得对吗?有没有什么方法可以让无点风格的代码更可读?

手动合成

让我们从手动组成函数开始:

const calcPages = (totalItems, itemsPerPage) =>
ceil(div(totalItems, itemsPerPage));
const div = (x, y) => x / y;
const ceil = Math.ceil;
const pagination = {
total: 101,
itemsPerPage: 10,
currentPage: 1
}
console.log(
calcPages(pagination.total, pagination.itemsPerPage)
);

程序合成

下一步,我们将提取参数:

const comp2 = (f, g) => (x, y) => f(g(x, y));
const div = (x, y) => x / y;
const ceil = Math.ceil;
const calcPages = comp2(ceil, div);
const pagination = {
total: 101,
itemsPerPage: 10,
currentPage: 1
}
console.log(
calcPages(pagination.total, pagination.itemsPerPage)
);

函数定义现在是无点的。但调用代码不是。如果您知道高阶函数comp2是如何工作的,那么表达式comp2(ceil, div)对您来说是非常声明性的。

现在很明显,calcPages的名称是错误的,因为函数的组成要通用得多。让我们称之为。。。intDiv(好吧,可能还有一个更好的名字,但我数学不好)。

破坏修改器

在下一步中,我们修改intDiv,使其能够处理对象:

const destruct2 = (x, y) => f => ({[x]:a, [y]:b}) => f(a, b);
const comp2 = (f, g) => (x, y) => f(g(x, y));
const div = (x, y) => x / y;
const ceil = Math.ceil;
const intDiv = comp2(ceil, div);
const calcPages = destruct2("total", "itemsPerPage") (intDiv);
const pagination = {
total: 101,
itemsPerPage: 10,
currentPage: 1
}
console.log(
calcPages(pagination)
);

我再次调用了修改后的函数calcPages,因为它现在需要一个特定的pagination对象,因此不那么通用。

如果您知道所涉及的高阶函数是如何工作的,那么所有内容都是声明性的,可读性很好,即使它是用无点风格编写的。

结论

无点风格是函数组合、currying和高阶函数的结果。它本身不是一件事。如果你为了避免无点风格而停止使用这些工具,那么你就会失去函数式编程所提供的很多表现力和优雅性。

让我们从一个简单的例子开始:

//    inc :: Number -> Number
const inc = R.add(1);

我发现上面的内容比它的"有意义的"等价物更清楚:

//    inc :: Number -> Number
const inc = n => R.add(1)(n);

一旦对部分应用Ramda函数感到满意,上面一行中的lambda就只是噪声。

让我们走到另一个极端:

//    y :: Number
const y = R.ifElse(R.lt(R.__, 0), R.always(0), Math.sqrt)(x);

这将是更清楚地用"pointful"风格写的

//    y :: Number
const y = x < 0 ? 0 : Math.sqrt(x);

我的建议是在简单的情况下使用无点表达式,并在表达式变得复杂时恢复到"有点"风格。我经常做得太过分,然后撤销最后几次更改,以获得更清晰的表达。

在我看来,这里的问题是"可读性",更具体地说,你不能把代码作为书中的文本从左到右连续阅读。

ramda库中的一些函数可以从右到左读取,如compose():

import { compose, add, multiply } from 'ramda'
// the flow of the function is from right to left
const fn = compose(add(10), multiply) // (a, b) => (a * b) + 10

然后你就到了一个点,某些函数要求你按照一定的顺序给出它们的参数(非交换函数),通常是以从左到右的方式,比如lt()

import { __, lt } from 'ramda'
// you need to read the parameters from left to right to understand it's meaning
const isNegative = lt(__, 0) // n => n < 0

当这些方向发生变化时,阅读代码会更加困难,因为要找到代码流动的路径需要付出更多的努力。

import { compose, length, gte, __ } from 'ramda'
// 1) you start reading the code from left to right
// 2) you see compose: switch modes to right to left, jump to the end of compose
// 3) you get to gte, where you need to switch back to left to right mode
// 4) glue all previous parts together in your mind, hope you'll still remember where you started
const hasAtLeast3Items = compose(gte(__, 3), length) // arr => arr.length >= 3

在使用ramda时,有一些函数非常有用,但会立即破坏可读性。这些要求你多次切换阅读方向,要求你跟踪代码中太多的子步骤。对我来说,这个列表中的第一个函数是converge

注意:上面的问题似乎只发生在参数超过1的函数中,但add()没有这个问题,add(__, 3)add(3)是相同的。

最新更新