简要介绍fp-ts(三)

IO

函数式编程最不能容忍的就是副作用,但是副作用又是一段有用的程序所必不可少的。所以,在函数式编程中,会把产生副作用的代码减少到最小并聚集在一起。IO就是用来表示一个会产生副作用的操作。查看 fp-ts 中IO的定义:

interface IO<A> {
  (): A
}
复制代码

就会发现,这不就是一个普通的函数吗。其实,这就是能够在 pure 的世界藏匿令人害怕的副作用的方法。一段有副作用的代码一旦被放到一个函数中,只要我们不执行这个函数,那罪恶的副作用就被禁锢在潘多拉魔盒之中,世界就仍然是 pure 且美好的。当一切准备就绪,我们就可以在一个与其他 pure 代码隔离开的角落里执行这个函数,释放出副作用?。更妙的是IO也有FunctorMonad等 typeclass 的 instance。

const getRoot: IO<HTMLElement> = () => document.documentElement;

const calcWidth = (elm: HTMLElement) => elm.clientWidth;

const log =
  <A>(a: A): IO<void> =>
  () =>
    console.log(a);

const main: IO<void> = pipe(
  Do,
  chain(() => getRoot),
  map(calcWidth),
  chain(log)
);

// impure
main();
复制代码

Task

IO 赋予了我们以 pure 的方式使用同步产生的副作用的能力,而 Task 则是让我们能够使用异步的副作用。Task的定义仍然很简单:

interface Task<A> {
  (): Promise<A>
}
复制代码

原来就是一个返回Promise的函数。同样的,Task也有FunctorMonad等 typeclass 的 instance,因此我们很熟悉的mapchain等函数又再次可以派上用场。至此,我们在讨论的过程中都回避了一个事实:无论是IO还是Task都是可能失败的。比如,我们在发起资源请求的时候,难免出现网络不可达或者资源不存在的情况。正因为如此,fp-ts 提供了另外两个 module,分别是IOEitherTaskEither。但是它们其实是我们早已熟知的概念:

interface IOEither<E, A> extends IO<Either<E, A>> {}

interface TaskEither<E, A> extends Task<Either<E, A>> {}
复制代码

原来TaskEither e a就是Task (Either e a)。相比起Promise<A>,使用 TaskEither e a的好处是使用者很难忽略这个操作可能失败这个事实。TaskEither提供了一个很方便的函数,帮助我们从一个对可能产生的错误遮遮掩掩的Promise<A>得到一个TaskEither e a

const tryCatch = <E, A>(f: Lazy<Promise<A>>, onRejected: (reason: unknown) => E): TaskEither<E, A> => () =>
  f().then(E.right, (reason) => E.left(onRejected(reason)))
复制代码

对于异步操作很普遍的前端开发来说,TaskEither的使用将会非常频繁。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享