考虑到长期影响(增加函数/参数的数量、其他开发人员接管等),每个选项的优缺点是什么?
选项1:不需要向每个方法传递foo
和bar
,但会创建难以遵循的嵌套函数。
const myFunction = ({foo, bar}) => {
const results = []
const function1 = () => {
return foo + bar;
}
const function2 = () => {
return foo * bar;
}
const res1 = function1();
const res2 = function2();
results.push(res1, res2);
return results;
}
选项2:将参数传递给每个函数,但删除嵌套,在我看来,这会使它更可读。
const function1 = ({foo, bar}) => {
return foo + bar;
}
const function2 = ({foo, bar}) => {
return foo * bar;
}
const myFunction = ({foo, bar}) => {
const results = []
const res1 = function1({foo, bar});
const res2 = function2({foo, bar});
results.push(res1, res2);
return results;
}
我更想知道如何改进我在这里的功能方法。非常感谢。
第二种方法更为惯用。事实上,第二种方法在函数式编程中有其名称。一个接受共享静态值作为输入的函数,也称为环境,被称为读取器。
// Reader e a = e -> a
// ask : Reader e e
const ask = x => x;
// pure : a -> Reader e a
const pure = x => _ => x;
// bind : Reader e a -> (a -> Reader e b) -> Reader e b
const bind = f => g => x => g(f(x))(x);
// reader : Generator (Reader e a) -> Reader e a
const reader = gen => (function next(data) {
const { value, done } = gen.next(data);
return done ? value : bind(value)(next);
}(undefined));
// Environment = { foo : Number, bar : Number }
// function1 : Reader Environment Number
const function1 = reader(function* () {
const { foo, bar } = yield ask;
return pure(foo + bar);
}());
// function2 : Reader Environment Number
const function2 = reader(function* () {
const { foo, bar } = yield ask;
return pure(foo * bar);
}());
// myFunction : Reader Environment (Array Number)
const myFunction = reader(function* () {
const res1 = yield function1;
const res2 = yield function2;
return pure([res1, res2]);
}());
// results : Array Number
const results = myFunction({ foo: 10, bar: 20 });
console.log(results);
在上面的示例中,我们使用一元表示法定义function1
、function2
和myFunction
。请注意,myFunction
并没有明确地将环境作为输入。它也没有明确地将环境传递给function1
和function2
。所有这些"管道"都由pure
和bind
函数处理。我们使用ask
单元操作访问单元上下文中的环境。
然而,当我们使用ReaderT
monad转换器将Reader
monad与其他monad组合时,真正的优势就来了。
编辑:如果您不想使用一元符号,则不必使用。您可以按如下方式定义function1
、function2
和myFunction
。
// Reader e a = e -> a
// Environment = { foo : Number, bar : Number }
// function1 : Reader Environment Number
const function1 = ({ foo, bar }) => foo + bar;
// function2 : Reader Environment Number
const function2 = ({ foo, bar }) => foo * bar;
// myFunction : Reader Environment (Array Number)
const myFunction = env => {
const res1 = function1(env);
const res2 = function2(env);
return [res1, res2];
};
// results : Array Number
const results = myFunction({ foo: 10, bar: 20 });
console.log(results);
缺点是,现在您明确地将环境作为输入,并将环境传递给子计算。然而,这可能是可以接受的。
编辑:这里还有另一种写这篇文章的方法,不使用一元符号,但仍然使用ask
、pure
和bind
。
// Reader e a = e -> a
// ask : Reader e e
const ask = x => x;
// pure : a -> Reader e a
const pure = x => _ => x;
// bind : Reader e a -> (a -> Reader e b) -> Reader e b
const bind = f => g => x => g(f(x))(x);
// Environment = { foo : Number, bar : Number }
// function1 : Reader Environment Number
const function1 = bind(ask)(({ foo, bar }) => pure(foo + bar));
// function2 : Reader Environment Number
const function2 = bind(ask)(({ foo, bar }) => pure(foo * bar));
// myFunction : Reader Environment (Array Number)
const myFunction =
bind(function1)(res1 =>
bind(function2)(res2 =>
pure([res1, res2])));
// results : Array Number
const results = myFunction({ foo: 10, bar: 20 });
console.log(results);
注意,使用生成器的一元表示法只是上面代码的语法糖。
这两种方法都是正确的。这里的问题是该代码与应用程序相关的上下文,这些函数是否与它们定义/使用的范围相关?
考虑这个例子。
const Calculator = class {
complexOperation({foo, bar}) {
const results = []
const res1 = this.sum({foo, bar});
const res2 = this.dot({foo, bar});
results.push(res1, res2);
return results;
}
sum({foo, bar}) {
return foo + bar;
}
dot({foo, bar}){
return foo * bar;
}
};
var calc = new Calculator();
calc.complexOperation({foo: 2, bar: 3})
在这个例子中,我们可以看到函数级抽象是如何与意图相关的。
永远记住"下台规则"。
现在让我们更改应用程序的意图。现在我们正在申请一家法律机构,我们必须进行一项复杂的操作来申请一些税款。
现在sum和dot不应该是类的一部分,因为它只用于复杂的操作,新的开发人员不在乎function1(我将其重命名为sum)做什么,他们不必阅读它,所以我们可以更改抽象级别。事实上,您将以一个包含一些步骤的方法结束。
在c#等其他语言中,您可以在函数使用后定义函数,但在javascript中不能,因此您不能在JS中的本地函数中应用Stepdown规则。有些人颠倒了Stepdown规则,定义了函数start中的所有局部函数,所以他们的眼睛只会跳到最后一个局部函数结束括号,开始阅读。
const BusinessTaxes = class {
complexOperation({foo, bar}) {
const addTax = () => {
return foo + bar;
}
const dotTax = () => {
return foo * bar;
}
// Jump your eyes here
const results = []
const res1 = addTax({foo, bar});
const res2 = dotTax({foo, bar});
results.push(res1, res2);
return results;
};
};
var businessTax= new BusinessTaxes();
businessTaxes.complexOperation({foo: 2, bar: 3})
总之,将您的代码组织到相同级别的抽象中,保持其结构化并与您的决策保持一致,您的代码将具有可读性和可维护性。
当开发人员专注于明确他们的意图时,可读性往往是一种副产品。
下一个开发人员(或未来的您)会理解您的意图吗
IMHO是你应该回答的唯一问题吗?因为它关注的是比更有形的东西;这个看起来好看吗">
从这个角度来看,两个版本都是这样做的。
除了两者:
- 可以用更好的名字
- 可以使用新的语法使其更容易被看到
const sumproduct_pair = ({a, b}) => {
const sum = () => a + b;
const product = () => a * b;
return [sum(), product()];
};
或
const sum = ({a, b}) => a + b;
const product = ({a, b}) => a * b;
const sumproduct_pair = ({a, b}) => [sum({a, b}), product({a, b})];
然而,的两个版本都可以改进,但YMMV:
在第一个版本中,sum
和product
都不需要存在。它们显然不是要重复使用的,而且非常简单,可以简化为最简单的表达:
const sumproduct_pair = ({a, b}) => [a+b, a*b];
在第二个版本中,如果您打算重用sum
和product
,那么考虑";针对接口而非实现的设计">。
函数sumproduct_pair
期望一个具有属性a
和b
的对象,但这并不意味着其他所有函数都需要具有相同的接口:
const sum = (a, b) => a + b;
const product = (a, b) => a * b;
const sumproduct_pair = ({a, b}) => [sum(a, b), product(a, b)];
虽然这似乎是一个微不足道的更改,但它删除了一些不必要的花括号(如果你想从少写开始提高可读性),最重要的是,它允许sum
和product
处理未知数量的数字:
const sum = (...xs) => xs.reduce((ret, x) => ret + x, 0);
const product = (...xs) => xs.reduce((ret, x) => ret * x, 1);
sum(1, 2, 3); //=> 6
sum(1, 2, 3, 4); //=> 10
product(1, 2, 3); //=> 6
product(1, 2, 3, 4); //=> 24