函数式编程:对if-else的另一种解释

Posted treeShaking

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了函数式编程:对if-else的另一种解释相关的知识,希望对你有一定的参考价值。

本来想给本文起个名字叫‘避免代码中的if-else’,或‘重构if-else’,但实际上都不达意。代码中并不能彻底避免if-else,比如,if-else的分支只代表业务逻辑的不同分支,那么写成如下是合理的:

 
   
   
 
  1. if (condition) {

  2.    do task1

  3. } else {

  4.    do task2

  5. }

  6. // 或

  7. condition do task1 : do task2

但是如果业务逻辑分支变得多而校验也随之变得复杂时,你可能会看到下面可读性很差的代码:

 
   
   
 
  1. const validateBranching = ({ name, type, email, phone }) => {

  2.  if (!isValidName(name)) {

  3.    return Required('name');

  4.  } else if (type === 'email') {

  5.    } else if (phone && !isValidPhone(phone)) {

  6.      return Optional(InvalidPhone(phone));

  7.    } else {

  8.      return { type, name, email, phone };

  9.    }

  10.  } else if (type === 'phone') {

  11.    if (!isValidPhone(phone)) {

  12.      return InvalidPhone(phone);

  13.    } else {

  14.      return { type, name, email, phone };

  15.    }

  16.  } else {

  17.    return InvalidType(type);

  18.  }

  19. };

以上是对输入的名字,邮箱,手机号进行的校验。泄这种代码实属正常,这是最直接的逻辑,运行效率是最高的。只不过她不优雅,甚至会让你陷入麻烦。从直观上看,一下子找不出业务的正向逻辑,无法分清,哪些是业务分支,哪些是出错处理,哪些是某个业务的嵌套分支,在调试时尤其让你烦恼的一遍遍的梳理这一团乱麻的if-else,有人可能会对这样的代码做些改变:

 
   
   
 
  1.  }

  2. };


  3. const assertPhone = (phone, wrapper=id) => {

  4.  if (!isValidPhone(phone)) {

  5.  }

  6. };


  7. const validateThrow = ({ name, type, email, phone }) => {

  8.  if (!isValidName(name)) {

  9.    throw Required('name');

  10.  }

  11.  switch (type) {

  12.    case 'email':

  13.      if (phone)  assertPhone(phone, Optional);

  14.      return { type, name, email, phone };


  15.    case 'phone':

  16.      assertPhone(phone);

  17.      return { type, name, email, phone };


  18.    default:

  19.      throw InvalidType(type);

  20.  }

  21. };

以上代码最突出的优化,应该是对部分逻辑进行函数的封装,并给函数起了个通俗的名字,如 assertPhone,一看就知道是在做出错处理。这使得,你的出错处理开始与业务逻辑在阅读体验上分离开来了。但是这么做,必须在出错处理函数里抛出异常。据说抛出异常并不很好:

  • 如果你在浏览器里运行,就不应该在业务逻辑里捕获异常,因为抛出异常,就需要捕获异常,如果只单处理了业务逻辑错误,而忽略了浏览器环境可能出现的错误,你的程序就不好调试了。

  • 你后续得写那些try-catch,多余的东西总会让我难受。

那么有没有比上面更好的办法。当然,只要使用monad思想写一套出错处理的函数式编程库就行。而且现在已经有了。本文使用了folktale的Data.Either,folktale是个大型的函数式编程库,我们现在只用Data.Either,顺便提一句,官方出了个换了名字的另一个库result,区别是result在函数命名上更倾向于做出错处理,因为result的正向逻辑是Ok,反向是Error,而Data.Either的正向是Right,反向是Left,我觉得都没问题,但是我们希望不只是做出错处理,Left也有可能是逻辑的一部分。所以以Data.Either为例。先解释一下这个库的用法。

安装:

 
   
   
 
  1. $ npm install data.either

通常用于取值的函数有:

 
   
   
 
  1. import { Right, Left, of, Either } from 'data.either'

  2. Right(1).get() // 1, Right构造一个对象向,约定存储正向逻辑的值

  3. of(1).get() // 1, of等价于Right

  4. Left(1).get() // throw new TypeError("Can't extract the value of a Left(a).")

  5. Left(1).getOrElse(2) // 2, get用于获取存于Right对象内的值,如果用于Left会报错,但Left可以使用getOrElse获取一个默认值

  6. Right(1).getOrElse(2) // 1, 由于Right可以通过get合法的取到值,所以不会反悔默认值

  7. Left(1).merge() // 1, 取出对象中的值,无论存于Left和Right对象中

  8. Right(1).merge() // 1, 取出对象中的值,无论存于Left和Right对象中

然后来对Left和Right的其他常用成员函数做一个分类

Left和Right具有相同的成员函数,但是执行不同,区别在于,部分函数在Left对象上传播,部分函数在Right对象上传播,部分在两者上都起作用。所谓传播propagation,可以理解为忽略,由于函数式编程通常使用 .进行连续执行逻辑,像一个链条,链条上每个环节就是逻辑处理,处理结果就在这个链条上流传,当然可以选择性忽略某个环节,忽略在形式上就像传播过去了一样,并且传播时携带了原有的上个环节的信息,常用的成员函数,在不同的对象上起作用,可以分类为如下

  • Right: map,chain

  • Left: leftMap,orElse

  • Right,Left: bimap,fold,merge,getOrElse

其中最长用的应该是上面的前两者,而且 mapchainleftMaporElse是对应的。这些函数的用法和名字都和函数式编程前面几节里的一样,属于约定俗成的。所以具体功能不再赘述,主要看如何使用。我们对本文开头出的业务重构一下:

 
   
   
 
  1. import { Right, Left, of, Either } from 'data.either'

  2. interface Props {

  3.    name: string,

  4.    type: string,

  5.    email: string,

  6.    phone: number

  7. }


  8. type CheckInfo = Either<string|Props, string|Props>

  9. type FuncType = (v: Props) => CheckInfo

  10. const testName: FuncType = v => {

  11.    return v.name.trim() === '' ? Left('no name value') : Right(v)

  12. }


  13. const testType: FuncType = v => {

  14.    switch (v.type) {

  15.        default: return Left('error type value')

  16.    }

  17. }


  18.    return v.email.indexOf('@') === -1 ? Left('error email value') : Right(v)

  19. }


  20. const testPhone: FuncType = v => {

  21.    return isNaN(v.phone) ? Left('error phone value') : Right(v)

  22. }


  23. const maybeTestPhone: FuncType = v => {

  24.    return v.phone ? testPhone(v).leftMap(_ => 'email ok, but phone error') : Right(v)

  25. }


  26. }

  27. const doIt = ({name, type, email, phone}: Props) => {

  28.    return of({name, type, email, phone})

  29.        .chain(testName)

  30.        .chain(testType)

  31. }

首先对各个小模块进行封装,其实就算不使用either库,你也应该这么做,可以形成良好的可读性。从例子可知,你需要对最基本的校验模块进行封装,需要用的时候,就用chain进行组合。

如果你想看到更具普遍性的写法,稍加改造即可。

 
   
   
 
  1.    return v.type && v.type === 'email' ? Right(v) : Left(v)

  2. }


  3. const typeIsPhone: FuncType = v => {

  4.    return v.type && v.type === 'phone' ? Right(v) : Left(v)

  5. }


  6. const doIt2 = ({name, type, email, phone}: Props) => {

  7.    return of({name, type, email, phone})

  8.            .chain(testName)

  9.            .chain(maybeTestPhone)

  10.        .orElse(typeIsPhone)

  11.            .chain(testPhone)

  12.        .orElse(v => typeof v === "string" ? Left(v) : Left('error type value'))

以上写法可以看出模块组合和错误传播的能力,在直观上,你仅仅在写正向逻辑,每一个模块都有可能产生错误,但无需在中间捕获错误,因为错误会传播到最后。你只需关心结果是正确还是出错。这样的代码首先是清晰的,命令式的,然后还具有可重用,可重构的,同时还是 pointFree的,也就是中间无实参传递。

运行一下

 
   
   
 
  1. const result = doIt({

  2.    name: 'yangdechao',

  3.    type: 'email',

  4.    email: 'skji@163.com',

  5.    phone: 123123123,

  6. }).merge()

  7. /*

  8. { name: 'yangdechao',

  9.  type: 'email',

  10.  email: 'skji@163.com',

  11.  phone: 123123123 }

  12. */

 
   
   
 
  1. const result2 = doIt2({

  2.    name: 'yangdechao',

  3.    type: 'phone',

  4.    email: 'skji163.com',

  5.    phone: 123123123,

  6. }).merge()  // phone ok, but email error


以上是关于函数式编程:对if-else的另一种解释的主要内容,如果未能解决你的问题,请参考以下文章

反应式编程与事件驱动编程有何不同?

Jmeter分离登录事务的另一种方式

PowerShell-自定义函数 Function的另一种写法

20个简洁的 JS 代码片段

解析:JavaScript中的函数式编程

Python函数式编程