vlambda博客
学习文章列表

一本正经的扯淡:不要再尝试函数式编程了

作者 | Ilya Suzdalnitski

译者 | 刘雅梦

编辑 | 陈思

也许你曾听说过所谓的“函数式”编程,也许你甚至在想接下来是否要尝试一下。但是,函数式编程有很多缺陷,并不适用于现实项目的开发,并且会造成工作效率的下降。欲知详情,且听本文娓娓道来。【译者注:本篇采用了讽刺的写法,若急于知道真相,请拉至文末。】

注:这篇文章写得太有意思了,以至于我不得不转载给 MacTalk 读者看看。
也许你曾听说过所谓的“函数式”编程。也许你甚至在想接下来是否要尝试一下。
答案是别 !它简直是地狱!
函数式编程有很多缺陷,并不适用于现实项目的开发,并且会造成工作效率的下降。为什么呢?且听本文娓娓道来!
一、函数式编程无法满足复杂的企业需求  

现实世界中的企业级软件需要满足一系列复杂的、严格的、强制性的需求,这些需求与大量内嵌于软件解决方案中的抽象预期相关。换句话说,面向对象编程有助于程序员使用多种抽象机制,这些抽象机制完全能够满足企业的复杂需求。

这读起来有点拗口,但请先忍一下!接下来将做清晰地解释。
所谓的“函数式”编程,由于是基于数学的,所以没有合适的抽象机制(显然,这么做不太好,除了在学术界,数学在现实世界中没有任何应用)。与 OOP 不同,函数式编程并没有试图去满足企业所要求的众多严格而复杂的需求。
这段代码演示了函数式编程中普遍存在的问题:
  
    
    
  
import { filter, first, get } from 'lodash/fp';
const filterByType = type => filter( x => x.type === type );
const fruits = [ { type: 'apple', price: 1.99 }, { type: 'orange', price: 2.99 }, { type: 'grape', price: 44.95 } ];
const getFruitPrice = type => fruits => fruits |> filterByType(type) |> first |> get('price');
const getApplePrice = getFruitPrice('apple');
console.log('apple price', getApplePrice(fruits));
fp-sucks-apples-fp.js
看着它生不生气?没事,不光是你!
函数式编程并没有像所有严肃的企业通常所要求的那样,尝试对功能进行适当的抽象和封装。
任何一个有自尊心的软件工程师都不会写这样的代码!如果他们这样做了,那么他们可能会被那些严肃的大型企业立即解雇,以防止进一步的损失。在下一节中,我们将研究一个适当抽象的 OOP 程序。
二、函数式软件解决方案并不是面向未来的  
大家都清楚,一个专业且有自尊心的软件工程师的首要职责是,编写能够满足复杂业务需求且面向未来的代码。
与上面那段灾难性的函数式代码相比,让我们快速地看一个适当抽象的 OOP 程序。它做的事情完全相同,但采取的却是一种抽象且面向未来的方式:
class Fruit { constructor(type, price) { this.type = type; this.price = price; } }
class AbstractFruitFactory { make(type, price) { return new Fruit(type, price); } }
class AppleFactory extends AbstractFruitFactory { make(price) { return super.make("apple", price); } }
class OrangeFactory extends AbstractFruitFactory { make(price) { return super.make("orange", price); } }
class GrapeFactory extends AbstractFruitFactory { make(price) { return super.make("grape", price); } }
class FruitRepository { constructor() { this.fruitList = []; }
locate(strategy) { return strategy.locate(this.fruitList); }
put(fruit) { this.fruitList.push(fruit); } }
class FruitLocationStrategy { constructor(fruitType) { this.fruitType = fruitType; }
locate(list) { return list.find(x => x.type === this.fruitType); } }
class FruitPriceLocator { constructor(fruitRepository, locationStrategy) { this.fruitRepository = fruitRepository; this.locationStrategy = locationStrategy; }
locatePrice() { return this.fruitRepository.locate(this.locationStrategy).price; } }
const appleFactory = new AppleFactory(); const orangeFactory = new OrangeFactory(); const grapeFactory = new GrapeFactory();
const fruitRepository = new FruitRepository(); fruitRepository.put(appleFactory.make(1.99)); fruitRepository.put(orangeFactory.make(2.99)); fruitRepository.put(grapeFactory.make(44.95));
const appleLocationStrategy = new FruitLocationStrategy("apple");
const applePriceLocator = new FruitPriceLocator( fruitRepository, appleLocationStrategy );
const applePrice = applePriceLocator.locatePrice();
console.log("apple", applePrice);
file-fp-sucks-apples-oop-js
正如我们所看到的那样,它 SOLID 对所有的核心功能都进行了适当的抽象。这段代码是 SOLID 的。
不要让简单的东西愚弄了你!它完全满足通常任何大型企业所要求的所有的复杂业务需求。
这个健壮的解决方案完全是面向未来的,并且适当地利用了企业级依赖注入。
三、严肃的管理需要严肃的功能  
希望到目前为止,开发团队已经按照企业的规定,完成了与代码抽象相关的复杂业务需求。开发人员现在应该把资源重点投入到实现项目经理定义的功能上。
现实中的任何企业产品经理都知道,只有交付的新功能才是真正具有业务价值的。开发人员不应该将资源浪费在诸如单元测试、重构等耗时的事情上。
很显然,所谓的“函数式”编程是有缺陷的,它没必要使像重构、单元测试等多余的工作变得那么简单。这反过来又会分散开发团队的注意力,开发人员可能会不小心地将时间浪费在那些无用的活动上,而不是提供新的功能。
下面的例子非常清楚地展示了函数式编程的劣势,它使重构变得过于简单了:
  
    
    
  
// 重构之前:
// calculator.js: const isValidInput = text => true;
const btnAddClick = (aText, bText) => { if (!isValidInput(aText) || !isValidInput(bText)) { return; } }
// 重构之后:
// inputValidator.js: export const isValidInput = text => true;
// calculator.js: import { isValidInput } from './inputValidator';
const btnAddClick = (aText, bText, _isValidInput = isValidInput) => { if (!_isValidInput(aText) || !_isValidInput(bText)) { return; } }
file-fp_refactoring-js
如果这样的重构让你对它的简单性感到不安,你并不是唯一的一个!重构前有六行代码,重构后只有七行代码?你一定是在开玩笑吧!
让我们将其与面向对象代码的适当重构进行对比:
  
    
    
  
// 重构之前: public class CalculatorForm { private string aText, bText;
private bool IsValidInput(string text) => true;
private void btnAddClick(object sender, EventArgs e) { if ( !IsValidInput(bText) || !IsValidInput(aText) ) { return; } } }
// 重构之后: public class CalculatorForm { private string aText, bText;
private readonly IInputValidator _inputValidator;
public CalculatorForm(IInputValidator inputValidator) { _inputValidator = inputValidator; }
private void btnAddClick(object sender, EventArgs e) { if ( !_inputValidator.IsValidInput(bText) || !_inputValidator.IsValidInput(aText) ) { return; } } }
public interface IInputValidator { bool IsValidInput(string text); }
public class InputValidator : IInputValidator { public bool IsValidInput(string text) => true; }
public class InputValidatorFactory { public IInputValidator CreateInputValidator() => new InputValidator(); }
file-oop_refactoring-cs
这才是正确编程的样子!重构前 9 行代码,重构后 22 行代码。重构需要付出更多的努力,这将促使企业开发人员在进行诸如重构之类的浪费资源的活动之前能三思而后行。
四、声明式代码的谬论  
所谓的“函数式”程序员错误地以编写声明式代码为荣。这没什么值得骄傲的,这种代码只是制造了一种生产力的假象。
任何开发人员的核心职责都应该包括进行适当且严格的面向对象抽象(这也是任何大型企业所要求的)。
让我们来看一段适当抽象的 OOP 代码:
  
    
    
  
class CountryUserSelectionStrategy { constructor(country) { this.country = country; }
isMatch(user) { return user.country === this.country; } }
class UserSelector { constructor(repository, userSelectionStrategy) { this.repository = repository; this.userSelectionStrategy = userSelectionStrategy; }
selectUser() { let user = null;
for (const u in users) { if ( this.userSelectionStrategy.isMatch(u) ) { user = u; break; } }
return user; } }
const userRepository = new UserRepository(); const userInitializer = new UserInitializer(); userInitializer.initialize(userRepository);
const americanSelectionStrategy = new CountryUserSelectionStrategy('USA'); const americanUserSelector = new UserSelector(userRepository, americanSelectionStrategy);
const american = americanUserSelector.selectUser();
console.log('American', american);
file-fp-sucks-imperative-js
请关注第 20 行的循环命令。忽略次要的样板 OOP 代码,它与当前任务无关。为了使代码示例符合严肃企业提出的严格抽象要求,必须包含它。
另一方面,声明式代码过于简洁,并且错误地引导开发人员将注意力集中在不太重要的事情上,比如业务逻辑。将上述健壮的企业解决方案与下面这段较差的“声明式”代码进行对比:
  
    
    
  
SELECT * FROM Users WHERE Country=’USA’;
SQL 每次都让我感到害怕,因为它是声明式的。为什么选择 SQL 呢?为什么它们不能让开发人员使用适当的企业级抽象并编写正常的面向对象的代码呢?特别是当我们已经拥有了这些工具时。这真让人吃惊。
五、现实世界建模  
面向对象编程简直是天才。与“函数式”编程不同,它使用继承、多态和封装等高级技术能完美地为现实世界建模。
任何有自尊心的软件开发人员都应该每天使用继承来实现代码的可重用性。正如我之前所说,继承完美地模拟了现实世界。例如,猫总是从一个抽象的现实世界中的动物身上继承它们的特性和行为。生命起源于几十亿年前的海洋。因此,所有哺乳动物(包括猫)都继承了原始鱼类的特性(如 garfield.fishHead )以及方法(如 garfield.swim 和 garfield.layCaviar )。难怪猫这么喜欢 洗澡 和游泳!人类其实是一样的,如果我们愿意,我们也可以很容易地开始产卵!
对于代码组织,我们的程序应该始终遵循类似的分层方法。函数式编程错误地将开发人员从受现实世界启发所得到的如此惊人的代码共享结构中解脱出来。这会产生深远的影响,特别是在非常复杂的企业软件中。
六、函数应始终与对象绑定  
这只是常识,也是对现实世界的完美建模。你在 Chapters 购买的笔记本带有内置的“写方法”。每当你打算写东西的时候,都要调用这个方法。你可能没有意识到这一点,但你还有其他一些方法,比如.eat(veggies))、doHomeWork。这只是常识,不然你妈妈怎么能让你吃蔬菜,让你完成家庭作业呢?当然,她过去常常直接调用这些方法!
在现实世界中,如果没有一个专门负责协调任务的 Manager,工作是不可能完成的。年轻人可能需要一个管理者来满足他们基本需求,比如“netflix-n-chill”。到底由谁来协调整个过程?如果他们聪明的话,就会像 OOP 推荐的那样,雇佣多个管理者。
在现实世界中,创造任何新的、酷的东西也都需要有一个专门的 Factory。Leonardo 拥有一个 MonaLisaFactory,Trump 建造了一个秘密的 WallFactory。俄罗斯过去有一个 CommunismFactory,现在主要维护它隐藏在克里姆林宫地下深处的 CorruptionFactory。
我们可以清楚地看到,这只是“函数式”棺材中的另一颗钉子,因为它没有试图模拟现实世界。允许函数独立于对象存在,这显然是错误的。显然,函数编程不适用于任何现实的编码。
七、函数式编程没有提供成长的机会  
首先最重要的是,软件工程师应该专注于持续的提升和成长。为了真正掌握面向对象的编程,软件工程师必须掌握大量的知识。
首先,他们必须学习高级 OOP 技术,如继承、抽象、封装和多态。然后,他们应该熟悉各种设计模式(比如单例模式)并在代码中使用。大约有 30 种基本的设计模式需要学习。此外,理想情况下,开发人员也应该在代码中使用各种企业级抽象技术。
其次,是熟悉领域驱动设计之类的技术,并学习如何分解单体应用。还建议他们学习下合适的重构工具,比如 Resharper,因为 OOP 代码重构起来并不容易。
至少需要 20-30 年的时间才能熟练掌握 OOP。即使如此,大多数有 30 年 OOP 经验的人也还没有真正掌握它。学习之路坎坷不平,充满了不确定性。OOP 开发者需要终生学习,这是多么令人兴奋啊!
那么可怜的函数式程序员呢?很不幸的,没什么可学的。我曾亲自教过一些初级开发人员用 JavaScript 进行函数式编程,他们只用大约半年的时间就变得非常擅长了。他们只需要理解一些基本概念,然后很快就能学会怎么应用它们了。终生学习的乐趣在哪里?我不会羡慕他们的。
八、成功是一段旅程,而不是终点  
我们承认,我们的程序员是靠时间获取报酬的。就像过去两年在我家附近挖洞的建筑工人一样(顺便说一句,他们正在修建一堵墙,啊不,一条路)。
我们来定义下程序员的生产力。每个在大型企业工作过的人都知道取得成功的简单公式:
  
    
    
  
生产力 = 代码行数 x 修复 bug 数
修复 bug
人脑在处理状态方面真的很差,在给定的时间内,我们只能在大脑中记住大约五项工作。编程过程中的状态可以是内存中的任何数据,例如 OOP 中的字段 / 变量。使用可变状态就像是在玩杂耍。我认识的人里,没有几个能同时玩三个球的,更不用说五个了。
OOP 很好地利用了这个弱点。在 OOP 中,几乎所有的东西都是可变的。感谢上帝,OOP 非常重视开发人员的生产力问题!在 OOP 中,所有的可变状态也都能通过引用来共享!这意味着,我们不仅要考虑当前正在处理的对象的可变状态,还要考虑与之交互的其他 10-50 个对象的可变状态!这就类似于同时玩 50 个球,而且它还有一个额外的好处,那就是它能很好地锻炼大脑肌肉。
Bug?是的,最终我们会丢掉一些我们一直在玩的球。在这 50 个对象之间的交互中, 我们可能会漏掉一些小细节。但谁在乎呢,真的吗?在生产过程中,客户应该上报 Bug,任何大型企业都是这么做的。然后把这些 bug 录入到 JIRA 里,嗯,相当严肃的一款企业级软件。几年后,这些 bug 将被修复。问题解决了!
天哪,我喜欢使用我的手机银行应用程序。它非常先进,银行也很重视我的业务,他们很认真地对待我的隐私。但我被告知,这些 bug 只是一些特性!
所谓的“函数式”编程错误地隔离了状态,并使状态不可变。这有一个不幸的结果,那就是降低了复杂性,从而减少了 bug 的数量。代码库中的 bug 越少意味着我们需要修复的 bug 也就越少。承包商将无法继续向他们的客户收取修复这些 bug 的费用。任何大型企业中工作的开发人员在他们的经理眼中将会变得很糟糕,并且可能会严重影响他们在组织中晋升的机会。
代码行数
我们也应该向管理层不断展示我们取得的进步。最有效的方法是什么呢?当然是代码行数!如果我们都转向函数式编程,我们会让管理层非常不安和疑惑。“声明式”代码将使我们的代码更加简洁,代码行数也将大幅减少。实现完全相同的目标,最多可以减少 3-5 倍的代码,这是不可接受的!
换言之,面对严肃的企业管理,我们的生产力将大幅下降,我们的工作也将再次面临危险。远离“函数式”编程符合我们的最大利益。
同样的建议也适用于向客户收取工作时间费用的承包商。以下是取得成功的简单公式:
  
    
    
  
代码行数 = 编码实践 = $$$ 纯利润 $$$
当然,这个取得成功的公式也直接适用于那些按照代码行收费的软件承包商:
if (1 == '1') { doStuff();} else { // pure profit}
“意大利面”是我们的谋生之道
与函数式编程不同,OOP 为我们提供了一种一致的方式来编写意大利面代码,这对开发人员的工作效率是一个真正的福音。意大利面代码意味中更多的计费时间,它能转化成 OOP 工程师的纯利润。意大利面不仅味道鲜美,而且是 OOP 程序员的谋生之道!
面向对象对于承包商和严肃企业的员工来说都是一个真正的福音。
九、Bug 预防部门  
我们不应该害怕使用 OOP。再说一遍,那些讨厌的 bug 没什么好担心的!任何一个严肃的企业都有一个完整的 bug 预防部门(又称客户支持部门),其主要工作是保护他们开发人员的资源免受愤怒客户的影响。毕竟,不能正确使用应用程序是客户自己的错。
开发人员不应该为诸如 bug 报告之类无关紧要的事情而烦恼。这样可以确保没有浪费任何企业资源,并且允许开发人员在使用适当的企业级面向对象抽象和复杂设计模式的同时,集中精力实现新功能。
Bug 报告过程
为了保护企业资源,通常会精心设计一个详细而严格的流程。一旦客户遇到 bug,他们通常必须在线查找客户支持的电话号码。客户将看到一个包含各种选项的高级交互式电话菜单。通常需要两到五分钟来听菜单并选择正确的选项。缺乏耐心的客户通常在这一步就放弃了。
然后,客户通常会被告知,公司正在处理“意外的大量呼叫”或“平均等待时间是 56 分钟”。他们通常会为由此带来的不便而道歉,并表达他们对客户业务的重视程度。在这个步骤中,大多数客户通常会放弃报告 bug。为了取悦顾客,通常会播放鼓舞人心的音乐。他们还会让客户去关注这款很棒的新应用程序。这款应用程序就是客户最初遇到问题的那个。
在等了 56 分钟之后,呼叫被路由到了位于北美洲某处的呼叫中心。当地的美国雇员通常都是经过严格培训的,他们能够用浓重的印度或保加利亚口音说话。代理会说,应用程序的这个问题不是他的责任,但是很高兴帮客户转交到另一个部门。
又等了 42 分钟之后,一个代理很高兴地告诉客户这个 bug 实际上是一个功能,并建议用户浏览应用程序的帮助部分。如果客户仍然坚持,代理可能会创建一个支持通知单,甚至可能会给客户回电!这个 bug 不能被复现。
我希望你现在已经确信,担心 bug 不是开发人员的工作。企业通常会采取严格的措施来保护开发人员资源。
十、避免新手面试的错误  
如果你正在积极地寻找工作,那么花点精力把你的简历中那些“函数式”的废话都删掉吧,否则没有人会认真对待你。在现实的企业世界中,没有人受过“函数组合”、“纯函数”、“单子”或“不变”等幼稚事物的训练。你不想看起来像个局外人。谈论这些事情会让你的面试官哑口无言,而且会彻底毁掉你成功的机会。
企业的技术招聘人员也是要经过严格培训的,这使他们能够正确地区分 Java 和 JavaScript 等技术。
一定要在你的简历中穿插一些词来证明你了解各种严格的企业级抽象技术,比如类、继承、设计模式、依赖注入、实体、抽象工厂和单例等。
当被要求在白板上实现经典的冒泡(FizzBuzz)求职面试问题时,请确保你做好了充分的准备。这是你展示自己严谨的企业级系统设计能力的机会。第一步是充分设计解决方案,同时使用适当的 OOP 设计模式和严格的企业级抽象技术。FizzBuzz 企业版 是一个很好的起点。很多人犯了一个新手易犯的错误,那就是依赖诸如函数之类的劣质设计技术。难怪他们从来没有收到潜在雇主的回复。
十一、函数式编程不能用于构建严肃的软件解决方案  
在考虑了上面所有严肃且严谨的论点之后,我们现在可以清楚地看到,这种所谓的“函数式”编程并没有带来任何好处。很明显,我们应该不惜一切代价避免它。
所谓的“函数式”编程是近几年来的一种流行趋势。很高兴,它正在消亡!像 Facebook 和 Microsoft 这样的大公司早就意识到了函数式编程的局限性,以及面向对象方法在代码组织中的明显优势。他们正在将资源转移到新一代面向对象语言上,即 ReasonOL 和 BosqueOOP。这些语言将状态的可变性带到了一个全新的高度,幸运的是,它们不支持诸如不可变数据结构之类的无用的函数式的东西。
十二、上帝的恩惠  
因此,你可能会问,除了所谓的“函数式”编程,还有什么其他替代方案吗?面向对象编程,傻瓜!它是由一位真正的编程之神赐予我们的。OOP 是一种不可忽视的力量。它是开发人员生产力的终极工具,能让你和你的团队成员一直忙着工作(就业)。
愿(面向对象)的力量与你同在。还有你的代码。我是这种原力的一员。祝安好。
要获得更深入的解释,请参阅我的另一篇面向对象编程文章《上帝的恩惠》。
PS:八成大多数人已经猜到了,这是一篇讽刺性文章。所有的新开发人员,别那么严肃嘛,函数式编程非常棒!花点时间去学习它吧,你会走在大多数同行的前列。
原文链接:
https://medium.com/better-programming/fp-toy-7f52ea0a947e