如果你想做一个函数编程程序员(第四部分)
原文链接:https://medium.com/@cscalfani/so-you-want-to-be-a-functional-programmer-part-4-18fbe3ea9e49
这是一篇自译的文章(来源于机译和自己的理解),希望对大家有所启发。(如有侵权,请与我联系,侵权必删!!!)
迈出第一步来理解函数式编程概念是最重要的,有时也是最困难的一步。关于这一点,仁者见仁智者见智。
Currying
如果您从第3部分中还记得,我们在组合mult5和add(in)时遇到问题的原因是因为mult5使用1个参数,而add使用2个。
我们可以通过仅将所有函数限制为仅接受1个参数来轻松解决此问题。
相信我。它并不像听起来那样糟糕。
我们只需编写一个使用2个参数,但一次只包含1个参数的add函数。Curried函数使我们能够做到这一点。
Curried函数是一次仅包含一个参数的函数。
这将使我们在使用mult5进行组合之前给add第一个参数。然后,当调用mult5AfterAdd10时,add将获得其第二个参数。
在Javascript中,我们可以通过重写add来实现:
var add = x => y => x + y
这个版本的add是一个函数,该函数现在使用一个参数,然后再使用另一个参数。
详细地说,add函数采用单个参数x,并返回采用单个参数y的函数,该函数最终将返回x和y相加的结果。
现在,我们可以使用此版本的add来构建mult5AfterAdd10的工作版本:
var compose = (f, g) => x => f(g(x));
var mult5AfterAdd10 = compose(mult5, add(10));
compose函数采用2个参数,f和g。然后,它返回一个带有1个参数x的函数,该函数在调用时将应用g在f之后去处理x。
那我们到底做了什么?好吧,我们将普通的旧添加函数转换为curried版本。由于第一个参数10可以预先传递给它,而调用mult5AfterAdd10时将传递最后一个参数,这使添加操作更加灵活。
此时,您可能想知道如何在Elm中重写add函数。事实证明,您不必这样做。在Elm和其他功能语言中,所有功能都会自动进行管理。
所以add函数看起来是一样的:
add x y =
x + y
这就是应该在第3部分中写回mult5AfterAdd10的方式:
mult5AfterAdd10 =
(mult5 << add 10)
从语法上讲,Elm击败了像Java语言这样的命令性语言,因为它已针对诸如curring和composition之类的功能性事物进行了优化。
Currying and Refactoring
另一个令人神往的时间是在重构期间,当您创建具有大量参数的函数的通用版本,然后使用它来创建具有较少参数的专用版本时。
例如,当我们有以下函数将字符串放在方括号和双括号中时:
bracket str =
"{" ++ str ++ "}"
doubleBracket str =
"{{" ++ str ++ "}}"
这是我们的使用方式:
bracketedJoe =
bracket "Joe"
doubleBracketedJoe =
doubleBracket "Joe"
我们可以概括一下bracket和doubleBracket:
generalBracket prefix str suffix =
prefix ++ str ++ suffix
但是现在每次我们使用generalBracket时,我们都必须使用括号:
bracketedJoe =
generalBracket "{" "Joe" "}"
doubleBracketedJoe =
generalBracket "{{" "Joe" "}}"
我们真正想要的是两全其美。
如果我们重新排序generalBracket的参数,我们可以创建 bracket 和doubleBracket通过利用curried:
generalBracket prefix suffix str =
prefix ++ str ++ suffix
bracket =
generalBracket "{" "}"
doubleBracket =
generalBracket "{{" "}}"
注意,通过将最有可能是静态的参数(即前缀和后缀)放在首位,然后将最有可能更改的参数(即str)放在末位,我们可以轻松地创建generalBracket的专用版本。
参数顺序对于充分利用currying至关重要。
另外,请注意,bracket和doubleBracket是用point-free notation表示的,即隐含了str参数。bracket和doubleBracket都是等待其最终参数的函数。
现在我们可以像以前一样使用它:
bracketedJoe =
bracket “Joe”
doubleBracketedJoe =
doubleBracket “Joe”
但是这次我们使用的是广义的curried函数generalBracket。
常用功能
让我们看一下功能语言中使用的3个常见功能。
但首先,让我们看一下下面的Javascript代码:
for (var i = 0; i < something.length; ++i) {
// do stuff
}
这段代码有一个主要的wrong。这不是bug。问题在于该代码是样板代码,即一遍又一遍地编写的代码。
如果您使用命令式语言(例如Java,C#,Javascript,PHP,Python等)进行编码,那么您会发现自己编写此样板代码比其他任何代码都要多。
那是怎么回事
因此,让我们杀死它。让我们将其放在一个函数(或几个函数)中,再也不要编写for循环了。好吧,几乎永远不会;至少直到我们转向功能语言为止。
让我们先从修改array为things:
var things = [1, 2, 3, 4];
for (var i = 0; i < things.length; ++i) {
things[i] = things[i] * 10; // MUTATION ALERT !!!!
}
console.log(things); // [10, 20, 30, 40]
啊!!可变性!
让我们再试一次。这次我们不会改变任何东西:
var things = [1, 2, 3, 4];
var newThings = [];
for (var i = 0; i < things.length; ++i) {
newThings[i] = things[i] * 10;
}
console.log(newThings); // [10, 20, 30, 40]
好的,所以我们没有对things进行更改,但从技术上讲,我们对newThings进行了更改。现在,我们将忽略这一点。毕竟,我们使用Java语言。转到功能语言后,我们将无法进行更改。
这里的重点是要了解这些功能的工作原理,并帮助我们减少代码中的噪音。
让我们将此代码放入函数中。我们将调用我们的第一个公共函数map,因为它会将旧数组中的每个值映射到新数组中的新值:
var map = (f, array) => {
var newArray = [];
for (var i = 0; i < array.length; ++i) {
newArray[i] = f(array[i]);
}
return newArray;
};
请注意,传入了函数f,以便我们的map函数可以对数组的每个对象执行我们想做的任何事情。
现在我们可以调用重写以前的代码来使用map:
var things = [1, 2, 3, 4];
var newThings = map(v => v * 10, things);
你看没有for循环。而且更容易阅读,也更容易推理。
好吧,从技术上讲,map函数中有for循环。但是至少我们不必再编写该样板代码了。
现在,让我们编写另一个通用函数来过滤数组中的内容:
var filter = (pred, array) => {
var newArray = [];
for (var i = 0; i < array.length; ++i) {
if (pred(array[i]))
newArray[newArray.length] = array[i];
}
return newArray;
};
请注意,如果我们保留该项,则谓词函数pred如何返回TRUE;如果抛弃它,则如何返回FALSE。
以下是使用filter过滤奇数的方法:
var isOdd = x => x % 2 !== 0;
var numbers = [1, 2, 3, 4, 5];
var oddNumbers = filter(isOdd, numbers);
console.log(oddNumbers); // [1, 3, 5]
与使用for循环手动编码相比,使用我们的新filter功能要简单得多。
最后的通用函数称为reduce。通常,它用于获取列表并将其减少为单个值,但实际上可以做得更多。
此功能通常在功能语言中称为fold。
var reduce = (f, start, array) => {
var acc = start;
for (var i = 0; i < array.length; ++i)
acc = f(array[i], acc); // f() takes 2 parameters
return acc;
});
该reduce函数采用的reduction函数f,初始值和一个数组。
请注意,reduction函数f接受2个参数,即数组的当前项和累加器acc。每次迭代它将使用这些参数来产生一个新的累加器。返回最后一次迭代的累加器。
一个示例将帮助我们了解其工作原理:
var add = (x, y) => x + y;
var values = [1, 2, 3, 4, 5];
var sumOfValues = reduce(add, 0, values);
console.log(sumOfValues); // 15
请注意,add函数接受2个参数并将其相加。我们的reduce函数需要一个接受2个参数的函数,因此它们可以很好地协同工作。
我们从起始值0开始,然后传入要求和的数组values。在reduce函数内部,总和随着对value的迭代而累积。最终的累积值将作为sumOfValues返回。
这些函数map,filter和reduce均使我们能够对数组执行常见的操作操作,而无需编写样板式的for循环。
但是在函数式语言中,它们更有用,因为没有递归的循环构造。迭代功能不仅非常有用。他们是必要的。
我的脑子!!!!
现在足够了。
在本文的后续部分中,我将讨论引用完整性,执行顺序,类型等。
接下来:第5部分