请选择 进入手机版 | 继续访问电脑版

技术控

    今日:200| 主题:53462
收藏本版 (1)
最新软件应用技术尽在掌握

[其他] 函数式TypeScript

[复制链接]
温存Hostisblue 发表于 2016-10-11 13:16:34
238 4

立即注册CoLaBug.com会员,免费获得投稿人的专业资料,享用更多功能,玩转个人品牌!

您需要 登录 才可以下载或查看,没有帐号?立即注册

x

函数式TypeScript

函数式TypeScript

  谈到函数式编程时,我们常提到机制、方法,而不是核心原则。函数式编程不是关于 Monad、Monoid 和 Zipper 这些概念的,虽然它们确实很有用。从根本上来说,函数式编程就是关于如使用通用的可复用函数进行组合编程。本文是我在重构 TypeScript 代码时使用函数式的一些思考的结果。
  首先,我们需要用到以下几项技术:
  
       
  • 尽可能使用函数代替简单值   
  • 数据转换过程管道化   
  • 提取通用函数  
  来,开始吧!
  假设我们有两个类,Employee 和 Department。Employee 有 name 和 salary 属性,Department 只是 Employee 的简单集合。
  1. class Employee {
  2.   constructor(public name: string, public salary: number) {}
  3. }
  4. class Department {
  5.   constructor(public employees: Employee[]) {}
  6.   works(employee: Employee): boolean {
  7.     return this.employees.indexOf(employee) > -1;
  8.   }
  9. }
复制代码
我们要重构的是 averageSalary 函数。
  1. function averageSalary(employees: Employee[], minSalary: number, department?: Department): number {
  2.    let total = 0;
  3.    let count = 0;
  4.    employees.forEach((e) => {
  5.      if(minSalary <= e.salary && (department === undefined || department.works(e))){
  6.        total += e.salary;
  7.        count += 1;
  8.      }
  9.    });
  10.   return total === 0 ? 0 : total / count;
复制代码
averageSalary 函数接收 employee 数组、最低薪资 minSalary 以及可选的 department 作为参数。如果传了 department 参数,函数会计算该部门中所有员工的平均薪资;若不传,则对全部员工进行计算。
  该函数的使用方式如下:
  1. describe("average salary", () => {
  2.   const empls = [
  3.     new Employee("Jim", 100),
  4.     new Employee("John", 200),
  5.     new Employee("Liz", 120),
  6.     new Employee("Penny", 30)
  7.   ];
  8.   const sales = new Department([empls[0], empls[1]]);
  9.    
  10.   it("calculates the average salary", () => {
  11.     expect(averageSalary(empls, 50, sales)).toEqual(150);
  12.     expect(averageSalary(empls, 50)).toEqual(140);
  13.   });
复制代码
需求虽简单粗暴,可就算不提代码难以拓展,其混乱是显而易见的。若新增条件,函数签名及接口就不得不发生变动,if 语句也会也越来越臃肿可怕。
  我们一起用一些函数式编程的办法重构这个函数吧。
  使用函数代替简单值

  使用函数代替简单值看起来似乎不太直观,但这却是整理归纳代码的强大办法。在我们的例子中,这样做,意味着要将 minSalary 和 department 参数替换成两个条件检验的函数。
  1. type Predicate = (e: Employee) => boolean;
  2. function averageSalary(employees: Employee[], salaryCondition: Predicate,  
  3.   departmentCondition?: Predicate): number {
  4.   let total = 0;
  5.   let count = 0;
  6.   employees.forEach((e) => {
  7.     if(salaryCondition(e) && (departmentCondition === undefined || departmentCondition(e))){
  8.       total += e.salary;
  9.       count += 1;
  10.     }
  11.   });
  12.   return total === 0 ? 0 : total / count;
  13. }
  14. // ...
  15. expect(averageSalary(empls, (e) => e.salary > 50, (e) => sales.works(e))).toEqual(150);
复制代码
我们所做的就是将 salary、department 两个条件接口统一起来。而此前这两个条件是写死的,现在它们被明确定义了,并且遵循一致的接口。这次整合允许我们将所有条件作为数组传递。
  1. function averageSalary(employees: Employee[], conditions: Predicate[]): number {
  2.   let total = 0;
  3.   let count = 0;
  4.   employees.forEach((e) => {
  5.     if(conditions.every(c => c(e))){
  6.       total += e.salary;
  7.       count += 1;
  8.     }
  9.   });
  10.   return (count === 0) ? 0 : total / count;
  11. }
  12. //...
  13. expect(averageSalary(empls, [(e) => e.salary > 50, (e) => sales.works(e)])).toEqual(150);
复制代码
条件数组只不过是组合的条件,可以用一个简单的组合器将它们放到一起,这样看起来更加明晰。
  1. function and(predicates: Predicate[]): Predicate{
  2.   return (e) => predicates.every(p => p(e));
  3. }
  4. function averageSalary(employees: Employee[], conditions: Predicate[]): number {
  5.   let total = 0;
  6.   let count = 0;
  7.   employees.forEach((e) => {
  8.     if(and(conditions)(e)){
  9.       total += e.salary;
  10.       count += 1;
  11.     }
  12.   });
  13.   return (count == 0) ? 0 : total / count;
  14. }
复制代码
值得注意的是,“and” 组合器是通用的,可以复用并且还可能拓展为库。
  提起结果
  现在,averageSalary 函数已健壮得多了。我们可以加入新条件,无需破坏函数接口或改变函数实现。
  数据转换过程管道化

  函数式编程的另外一个很有用的实践是将所有数据转换过程变成管道。在本例中,就是将 filter 过程提取到循环外面。
  1. function averageSalary(employees: Employee[], conditions: Predicate[]): number {
  2.   const filtered = employees.filter(and(conditions));
  3.   let total = 0
  4.   let count = 0
  5.   filtered.forEach((e) => {
  6.     total += e.salary;
  7.     count += 1;
  8.   });
  9.   return (count == 0) ? 0 : total / count;
  10. }
复制代码
这样一来计数的 count 就没什么用了。
  1. function averageSalary(employees: Employee[], conditions: Predicate[]): number{
  2.   const filtered = employees.filter(and(conditions));
  3.   let total = 0
  4.   filtered.forEach((e) => {
  5.     total += e.salary;
  6.   });
  7.   return (filtered.length == 0) ? 0 : total / filtered.length;
  8. }
复制代码
接下来,如在叠加之前将 salary 摘取出来,求和过程就变成简单的 reduce 了。
  1. function averageSalary(employees: Employee[], conditions: Predicate[]): number {
  2.   const filtered = employees.filter(and(conditions));
  3.   const salaries = filtered.map(e => e.salary);
  4.   const total = salaries.reduce((a,b) => a + b, 0);
  5.   return (salaries.length == 0) ? 0 : total / salaries.length;
  6. }
复制代码
提取通用函数

  接着我们发现,最后两行代码和当前域完全没什么关系。其中不包含任何与员工、部门相关的信息。仅仅只是一个计算平均数的函数。所以也将其提取出来。
  1. function average(nums: number[]): number {
  2.   const total = nums.reduce((a,b) => a + b, 0);
  3.   return (nums.length == 0) ? 0 : total / nums.length;
  4. }
  5. function averageSalary(employees: Employee[], conditions: Predicate[]): number {
  6.   const filtered = employees.filter(and(conditions));
  7.   const salaries = filtered.map(e => e.salary);
  8.   return average(salaries);
  9. }
复制代码
又一次,提取出的函数是完全通用的。
  最后,将所有 salary 部分提出来之后,我们得到终极方案。
  1. function averageSalary(employees: Employee[], minSalary: number, department?: Department): number {
  2.    let total = 0;
  3.    let count = 0;
  4.    employees.forEach((e) => {
  5.      if(minSalary <= e.salary && (department === undefined || department.works(e))){
  6.        total += e.salary;
  7.        count += 1;
  8.      }
  9.    });
  10.   return total === 0 ? 0 : total / count;
  11. 0
复制代码
对比原始方案和终极方案,我敢说,毫无疑问,后者更棒。首先,它更通用(我们可以不破坏函数接口的情况下添加新类型的判断条件)。其次,我们从可变状态(mutable state)和 if 语句中解脱出来,这使代码更容易阅读、理解。
  何时收手

  函数式风格的编程中,我们会编写许多小型函数,它们接收一个集合,返回新的集合。这些函数能够以不同方式组合、复用 —— 棒极了。不过,这种风格的一个缺点是代码可能会变得过度抽象,导致难以读懂,那些函数组合在一起到底要干嘛?
  我喜欢使用乐高来类比:乐高积木能够以不同形式放在一起 —— 它们是可组合的。但注意,并不是所有积木都是一小块。所以,在使用本文所述技巧进行代码重构时,千万别妄图将一切都变成接收数组、返回数组的函数。诚然,这样一些函数组合使用极度容易,可它们也会显著降低我们对程序的理解能力。
  小结

  本文展示了如何使用函数式思维重构 TypeScript 代码。我所遵循的是以下几点规则:
  
       
  • 尽可能使用函数代替简单值   
  • 数据转换过程管道化   
  • 提取通用函数  
  了解更多

  强烈推荐以下两本书:



上一篇:react-adsence:简单的 Google 广告 React 组件
下一篇:25 Questions and Answers About Hortonworks DataFlow
杜飞 发表于 2016-10-12 09:11:52
吾生也有涯,而吃也无涯.
回复 支持 反对

使用道具 举报

amw5520 发表于 2016-10-21 21:29:47
房价越来越高,所以,好男人越来越少……
回复 支持 反对

使用道具 举报

duniang333 发表于 2016-10-31 09:03:37
白领算个屁,养猪才洋气.
回复 支持 反对

使用道具 举报

董宇 发表于 2016-11-15 10:39:08
有些人,去死都和我没关系。有些人,弄死我我都放不下。
回复 支持 反对

使用道具 举报

*滑动验证:
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

我要投稿

推荐阅读

扫码访问 @iTTTTT瑞翔 的微博
回页顶回复上一篇下一篇回列表
手机版/CoLaBug.com ( 粤ICP备05003221号 | 文网文[2010]257号 )

© 2001-2017 Comsenz Inc. Design: Dean. DiscuzFans.

返回顶部 返回列表