当前位置: 首页 > 科技观察

[译]FunctionalTypeScript

时间:2023-03-11 23:04:13 科技观察

说到函数式编程,我们往往会提到机制、方法,而不是核心原则。函数式编程与Monad、Monoid和Zipper等概念无关,尽管它们确实很有用。从根本上说,函数式编程是关于使用通用可重用函数进行组合式编程。这篇文章是我对使用函数式风格重构TypeScript代码的一些反思的结果。首先,我们需要使用以下技术:尽可能使用函数而不是简单的值管道化数据转换过程提取通用函数以开始使用!假设我们有两个类,Employee和Department。Employee有name和salary属性,而Department只是Employees的简单集合。classEmployee{constructor(publicname:string,publicsalary:number){}}classDepartment{constructor(publicemployees:Employee[]){}works(employee:Employee):boolean{returnthis.employees.indexOf(employee)>-1;}}我们要重构的是averageSalary函数。functionaverageSalary(employees:Employee[],minSalary:number,department?:Department):number{lettotal=0;letcount=0;employees.forEach((e)=>{if(minSalary<=e.salary&&(department===undefined||department.works(e))){total+=e.salary;count+=1;}});returntotal===0?0:total/count;}averageSalary函数接收employee数组,***salaryminSalary和可选的部门作为参数。如果传递部门参数,函数将计算该部门所有员工的平均工资;如果不通过,则计算所有员工的平均工资。函数使用如下:describe("averagesalary",()=>{constempls=[newEmployee("Jim",100),newEmployee("John",200),newEmployee("Liz",120),newEmployee(“Penny”,30)];constsales=newDepartment([empls[0],empls[1]]);it(“calculatestheaveragesalary”,()=>{expect(averageSalary(empls,50,sales)).toEqual(150);expect(averageSalary(empls,50)).toEqual(140);});});需求虽然简单粗暴,但不提代码就很难扩展,混乱是显而易见的。如果加入新的条件,函数签名和接口都得改,if语句会越来越臃肿可怕。让我们用一些函数式编程来重构这个函数。使用函数而不是简单的值使用函数而不是简单的值可能看起来不直观,但它是一种组织和概括代码的强大方式。在我们的例子中,这样做意味着用两个条件函数函数替换minSalary和department参数。typePredicate=(e:Employee)=>boolean;functionaverageSalary(employees:Employee[],salaryCondition:Predicate,departmentCondition?:Predicate):number{lettotal=0;letcount=0;employees.forEach((e)=>{if(salaryCondition(e)&&(departmentCondition===undefined||departmentCondition(e))){total+=e.salary;count+=1;}});returntotal===0?0:total/count;}//...期望(averageSalary(empls,(e)=>e.salary>50,(e)=>sales.works(e)))。toEqual(150);我们做的是将salary和department的条件接口统一起来。虽然以前这两个条件是硬编码的,但现在它们被明确定义并遵循一致的接口。这种集成使我们能够将所有条件作为数组传递。functionaverageSalary(employees:Employee[],conditions:Predicate[]):number{lettotal=0;letcount=0;employees.forEach((e)=>{if(conditions.every(c=>c(e))){total+=e.salary;count+=1;}});return(count===0)?0:total/count;}//...expect(averageSalary(empls,[(e)=>e.salary>50,(e)=>sales.works(e)])).toEqual(150);条件数组无非就是组合条件,可以用简单的组合器放在一起,这样看起来更清晰。functionand(谓词:Predicate[]):Predicate{return(e)=>predicates.every(p=>p(e));}functionaverageSalary(employees:Employee[],conditions:Predicate[]):number{lettotal=0;letcount=0;employees.forEach((e)=>{if(and(conditions)(e)){total+=e.salary;count+=1;}});return(count==0)?0:total/count;}值得注意的是,“and”组合器是通用的、可重用的并且可能扩展为库。显示结果averageSalary函数现在更加稳健。我们可以在不破坏函数接口或更改函数实现的情况下添加新条件。流水线数据转换函数式编程的另一个有用实践是将所有数据转换转换为流水线。在这种情况下,过滤器过程被提取到循环之外。functionaverageSalary(employees:Employee[],conditions:Predicate[]):number{constfiltered=employees.filter(and(conditions));lettotal=0letcount=0filtered.forEach((e)=>{total+=e.salary;count+=1;});return(count==0)?0:total/count;}这样count计数就没用了。functionaverageSalary(employees:Employee[],conditions:Predicate[]):number{constfiltered=employees.filter(and(conditions));lettotal=0filtered.forEach((e)=>{total+=e.salary;});return(filtered.length==0)?0:total/filtered.length;}接下来,如果在叠加之前提取工资,求和过程就变成了简单的reduce。functionaverageSalary(employees:Employee[],conditions:Predicate[]):number{constfiltered=employees.filter(and(conditions));constsalaries=filtered.map(e=>e.salary);consttotal=salaries.reduce((a,b)=>a+b,0);return(salaries.length==0)?0:total/salaries.length;}提取通用函数然后我们发现最后两行代码没有任何关系与当前域关系。它不包含任何与员工和部门相关的信息。只是一个计算平均值的函数。所以也提取它。functionaverage(nums:number[]):number{consttotal=nums.reduce((a,b)=>a+b,0);return(nums.length==0)?0:total/nums.length;}functionaverageSalary(employees:Employee[],conditions:Predicate[]):number{constfiltered=employees.filter(and(conditions));constsalaries=filtered.map(e=>e.salary);returnaverage(salaries);}和曾经,提取的函数是完全通用的。***,调出所有工资部分后,得到***解。functionemployeeSalaries(employees:Employee[],conditions:Predicate[]):number[]{constfiltered=employees.filter(and(conditions));returnfiltered.map(e=>e.salary);}functionaverageSalary(employees:Employee[],conditions:Predicate[]):number{returnaverage(employeeSalaries(employees,conditions));}对比原计划和***计划,我敢说后者无疑更好。首先,它更通用(我们可以在不破坏函数接口的情况下添加新的条件类型)。其次,我们摆脱了可变状态和if语句,这使得代码更易于阅读和理解。何时停止在函数式编程中,我们编写了许多接受一个集合并返回一个新集合的小函数。这些功能可以以不同的方式组合和重用——太棒了。然而,这种风格的一个缺点是代码可能变得过于抽象并且难以阅读。这些功能一起做什么?我喜欢用乐高积木做类比:乐高积木可以以不同的形式组合在一起——它们是可组合的。但是请注意,并非所有块都是小块。因此,在使用本文中描述的技术重构代码时,不要试图将所有内容都变成一个接受数组并返回数组的函数。诚然,这样的功能组合极其容易使用,但它们也会大大降低我们对程序的理解能力。总结本文展示了如何使用函数式思维重构TypeScript代码。我遵循以下规则:使用函数而不是简单的值尽可能进行数据转换通用函数的流水线提取要了解更多信息,我强烈推荐以下两本书:ReginaldBraithwaite的“JavaScriptAllonge”MichaelFogus的“FunctionalJavaScript”关注@victorsavkin获取有关Angular和TypeScript的更多知识。