每个命令式程序员都应使用的功能性编程原理

有时似乎函数式程序员是完全不同的品种。即使按照程序员的标准,他们似乎也比其他人更讨厌。他们使用奇怪的术语,例如“ monad”,“ for-comprehension”和“ lambda”。他们使用的语言不会以分号结尾。而且,无论Java程序员对C ++程序员有多么不安,这两个小组至少可以同意Haskell是很奇怪的。但是,如果有受其功能范式语言青睐的编程原则对我们其他人有用呢?事实是,即使对于习惯使用命令式语言的开发人员,函数式编程也可以提供很多东西。实际上,如今许多功能性编程原理在命令式语言中正变得越来越流行(我正在向您介绍Java 8)。
函数式编程原理如何使我受益?
每个开发人员都希望编写良好,干净,可维护,可理解的代码。例如,面向对象程序设计的流行部分是由于代码编写和维护带来的好处,而这种好处是范式鼓励开发人员组织其代码的方式。
函数式编程提供了自己的工具和实践,它们可以使代码以命令式编程无法实现的方式变得更加模块化。模块化代码导致易于理解,易于重用和易于测试的代码。
您可以将这些工具视为需要将程序的各个部分连接在一起时可以使用的一种粘合剂。命令式编程提供了某些类型的胶水。函数式编程提供了其他功能。我们可以使用更多类型的胶水来改善我们编写的代码的整体结构。
尤其是,比起可变性,他们更喜欢不变性,编写纯函数以及使用递归来分解问题,这些实践都可以作为一种新型胶水,各有千秋。最好的部分?这些是实践,而不是语言功能,并且无论您使用哪种语言编写,都可以使用。
不变性
许多函数式编程语言鼓励不变性,默认情况下通常使值不变。不变性是指防止状态被修改。变异可以发生在两个层次上:参考变异和价值变异。当您为现有变量分配新引用时,将发生引用突变:
var x = { foo:‘bar’ };
var y = x ;
x = { foo:‘baz’ };
控制台。log(x,y); //打印“ {foo:‘baz’},{foo:‘bar’}”

在此示例中,引用x被突变,但指向的对象没有突变,因此by指向的值y未更改。
修改现有对象时,会发生值突变:
var x = { foo:‘bar’ };
var y = x ;
X。foo = ‘baz’ ;
控制台。log(x,y); //打印“ {foo:‘baz’},{foo:‘baz’}”

在这里,即使y未直接修改,它仍引用与相同的对象x,并且其foo属性的值已更改。
参考突变和值突变之间的区别是微妙的,但是仍然需要理解。在许多具有编译时不变性的语言中,引用不变性很容易添加到您的代码中,但是值不变性则更加困难。例如,在Java中,您可以将引用声明为final,但这不会阻止您更改final被引用对象上非值的值,除非这些值也表示为 final。
不变性是确保代码解耦的一种低成本方法。作为开发人员,它使您可以控制如何更改系统中的对象。例如,在多线程程序中,这可能非常有用。由于变异,导致了许多(尽管不是全部)导致代码不具有线程安全性的错误和模糊的边缘情况。
如果对象和引用被锁定,那么您不必担心争用情况,即两个线程试图同时覆盖一个值,或者在两次读取之间该值意外更改的情况。它还使代码更易于直观地调试。读取代码的人不必担心当前正在读取的代码外部的源可能会更改特定的值,因为该值根本无法更改。这些只是不变性可以使您的代码更安全,更容易推理的几种方式。
但是,不变性确实要付出代价。根据对象的实现方式和所使用的语言,为了修改不可变的对象,您可能需要使用实例化对象时要声明的更改来克隆整个对象。这将导致创建许多对象然后将其丢弃,这可能会更频繁地触发垃圾回收。
因此,某些用例(例如游戏或GUI开发)不适合不可变性。但是,即使在这些特殊环境中,也可以在适当的地方使用不变性,以便从其提供的安全保证中受益。尽管有这样的谨慎,但如果您正确地构造对象并故意对对象的哪些部分进行更改,则仍然可以在不降低性能的情况下利用更改的性能。例如,树或链接列表以不可变的方式比哈希表或数组列表更容易使用。
不变性改变了我们处理代码问题的方式。它改变了我们对代码部分的思考方式,并鼓励我们以更清洁,更线程安全的方式将它们组合在一起。但是,仅不变性有时似乎比提供帮助更多的是障碍。幸运的是,与其他功能编程原理结合使用时,操作起来更容易。这些原理中的许多原理,例如纯函数,都是通过编写不可变的代码来启用的。
纯函数
知道函数式编程将重点放在函数上肯定不足为奇。但是,它们并不是指命令式程序员指“方法”或“过程”的“功能”。相反,“函数”在这种情况下可以追溯到我们在数学课上学到的函数。像是好东西f(x) = x + 1。这些功能很简单。他们取一个值并返回结果。它们是可预测的和可靠的。最重要的是,它们仅计算结果。函数式编程鼓励按照数学中函数的方式编写程序。
这些称为纯函数。
纯函数的最显着特征是它们不修改任何状态。这包括提供给函数的参数上的状态,全局状态,甚至是程序本身外部的状态。函数式程序员喜欢说非纯函数确实可以执行他们想要的任何事情,并且无法在调用站点知道调用站点不会产生副作用。一个有趣的例子是调用非纯函数可能会在某处发射导弹。当然不太可能,但是如何保证在不亲自调查代码的情况下调用某些任意过程实际上不会执行此操作?如果该功能是纯粹的,那么根据定义它就不能发射任何导弹。
当然,功能纯度可能太高。如果未修改任何状态,则程序可能根本不会运行。因此,纯函数应与不变性一样谨慎使用。
纯函数有很多好处。一个重要的特性是称为参照透明性。从理论上讲,参照透明函数可以将调用站点替换为调用函数的实际结果,而根本不改变程序的行为。
换句话说,参照透明函数可确保给定输入集的给定结果。无论何时调用2,f(x) = x + 1它将始终返回3 x。这意味着该函数不仅不能在调用时改变任何状态,而且也不能依赖任何可能会改变的外部状态。引用透明的函数可以轻松地缓存其结果。例如,使用纯函数时,便可以进行记忆和动态编程。
纯函数自然也是线程安全的。因为没有状态发生突变,所以可以由任意数量的并行线程调用一个纯函数。实际上,纯函数使并行化和并发编程变得轻而易举。给定两个不依赖于彼此结果的纯函数,您可以按任何顺序调用这些函数而不会引起竞争条件。
将函数转换为纯函数的最简单方法是将纯函数需要的所有状态作为函数的参数注入。如果您的函数过于复杂,则可能会有一些缺点,因为最终可能会导致参数列表过长。
这也凸显了与OOP范例一起谨慎使用纯函数的重要性。对象上的方法具有许多可用状态,不需要将其作为参数提供。如果使对象上的成员变量不可变或使用将对象作为其参数之一的静态函数,则可以解决此问题(请考虑使用Python)。
递归
递归-及其精细的子类型-尾递归-是几乎每个程序员都应该熟悉的概念。递归在函数式编程中是必不可少的,在函数式编程中,对不变性和纯函数的强调使常规for循环充其量只能在一般意义上使用,并且最好不要这样做。递归是一种循环机制,其中函数在循环的每一遍都重复调用自身,而不是依赖于计数器变量。
递归的核心概念之一(也是我将其作为当务之急的程序员使用的提示的原因)是将较大的问题分解为较小的,自相似的部分。较小的问题更容易理解,也更直观地解决。这自然可以提高代码的理解力和可维护性。
每当遇到需要循环的代码时,请问问自己递归是否是执行循环的正确方法。遍历数组以对其包含的每个值调用函数更适合常规循环,而使用quicksort策略对数组进行排序将是递归的最佳选择。
如果问题允许,请务必记住使用尾部递归(假设您的语言支持)。尾递归是指递归调用是函数结束之前发生的最后一件事,换句话说,它位于尾部位置。此递归函数是尾递归:

function factorial(x, acc) {
acc = acc || 1; // acc can be omitted when initially invoking factorial()
if (x > 1) {
return factorial(x - 1, acc * x);
} else {
return acc;
}
}
尾递归是有益的,因为它避免了递归最大的弱点之一:堆栈溢出。编译器可以通过以下方式优化尾递归调用:它们不会导致堆栈函数指针在每次调用时都越来越深。如果您的递归函数可能被调用了数百次,请考虑以尾递归的方式编写函数或使用更常规的循环机制来重写它。
结论
函数式程序员和命令式程序员之间的鸿沟并不像您想象的那样大。最终,双方都可以为编程世界做出很多贡献。函数式编程世界熟悉的工具可以用在命令式编程语言中,以使我们的代码更简洁,模块化,更易于维护。
最后,开发这么多年我也总结了一套学习Java的资料与面试题,如果你在技术上面想提升自己的话,可以关注我,私信发送领取资料或者在评论区留下自己的联系方式,有时间记得帮我点下转发让跟多的人看到哦。在这里插入图片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章