fp-ts 入门:IO 错误处理提升

2025-06-08

fp-ts 入门:IO

错误处理

起重

fp-ts同步中,有效计算由类型表示IO,该类型基本上是一个thunk,即具有以下签名的函数:() => A

interface IO<A> {
  (): A
}
Enter fullscreen mode Exit fullscreen mode

请注意,表示永不失败的IO计算

此类计算的示例如下:

  • 读/写localStorage
  • 获取当前时间
  • 写信给console
  • 获取随机数

示例(读取/写入localStorage

import { fromNullable, Option } from 'fp-ts/Option'

const getItem = (key: string): IO<Option<string>> => () =>
  fromNullable(localStorage.getItem(key))

const setItem = (key: string, value: string): IO<void> => () =>
  localStorage.setItem(key, value)
Enter fullscreen mode Exit fullscreen mode

示例(获取当前时间)

const now: IO<number> = () => new Date().getTime()
Enter fullscreen mode Exit fullscreen mode

示例(写入console

const log = (s: unknown): IO<void> => () => console.log(s)
Enter fullscreen mode Exit fullscreen mode

示例(获取随机数)

const random: IO<number> = () => Math.random()
Enter fullscreen mode Exit fullscreen mode

IO类型允许一个Monad实例,因此您可以map......

import { io } from 'fp-ts/IO'

/** get a random boolean */
const randomBool: IO<boolean> = io.map(random, n => n < 0.5)
Enter fullscreen mode Exit fullscreen mode

...或chain计算

/** write to the `console` a random boolean */
const program: IO<void> = io.chain(randomBool, log)

program()
Enter fullscreen mode Exit fullscreen mode

请注意,在您调用之前什么也不会发生program()

这是因为它program是一个代表有效计算的,因此为了执行任何副作用,您必须“运行该操作”。IO

由于IO动作只是值,因此您可以使用Monoid等有用的抽象来处理它们......

示例(龙与地下城)

import { log } from 'fp-ts/Console'
import { getMonoid, IO, io } from 'fp-ts/IO'
import { fold, Monoid, monoidSum } from 'fp-ts/Monoid'
import { randomInt } from 'fp-ts/Random'

type Die = IO<number>

const monoidDie: Monoid<Die> = getMonoid(monoidSum)

/** returns the sum of the roll of the dice */
const roll: (dice: Array<Die>) => IO<number> = fold(monoidDie)

const D4: Die = randomInt(1, 4)
const D10: Die = randomInt(1, 10)
const D20: Die = randomInt(1, 20)

const dice = [D4, D10, D20]

io.chain(roll(dice), result => log(`Result is: ${result}`))()
/*
Result is: 11
*/
Enter fullscreen mode Exit fullscreen mode

..或者定义有用的组合器

/** Log any value to the console for debugging purposes */
const withLogging = <A>(action: IO<A>): IO<A> =>
  io.chain(action, a => io.map(log(`Value is: ${a}`), () => a))

io.chain(roll(dice.map(withLogging)), result => log(`Result is: ${result}`))()
/*
Value is: 4
Value is: 2
Value is: 13
Result is: 19
*/
Enter fullscreen mode Exit fullscreen mode

错误处理

如果我们想要表示一个可能失败的同步有效计算怎么办?

我们需要两个效果:

类型构造函数 效果(解释)
IO<A> 同步有效计算
Either<E, A> 计算可能失败

解决方案是放入Either里面IO,这导致了IOEither类型

interface IOEither<E, A> extends IO<Either<E, A>> {}
Enter fullscreen mode Exit fullscreen mode

当我们“运行”一个类型的值时IOEither<E, A>,如果得到一个Left,则表示计算失败,错误类型为E,否则我们得到一个Right,表示计算成功,并且值为类型A

示例(读取文件)

由于fs.readFileSync可能会抛出,我将使用tryCatch助手

tryCatch: <E, A>(f: () => A) => IOEither<E, A>
Enter fullscreen mode Exit fullscreen mode

其中f是一个 thunk,它要么抛出错误(由 自动捕获tryCatch),要么返回 类型的值A

import { toError } from 'fp-ts/Either'
import { IOEither, tryCatch } from 'fp-ts/IOEither'
import * as fs from 'fs'

const readFileSync = (path: string): IOEither<Error, string> =>
  tryCatch(() => fs.readFileSync(path, 'utf8'), toError)

readFileSync('foo')() // => left(Error: ENOENT: no such file or directory, open 'foo')
readFileSync(__filename)() // => right(...)
Enter fullscreen mode Exit fullscreen mode

起重

fp-ts/IOEither模块提供了其他允许创建类型值的帮助程序IOEither,它们统称为提升函数

以下是摘要

起始值 升降功能
IO<E> leftIO: <E, A>(ml: IO<E>) => IOEither<E, A>
E left: <E, A>(e: E) => IOEither<E, A>
Either<E, A> fromEither: <E, A>(ma: Either<E, A>) => IOEither<E, A>
A right: <E, A>(a: A) => IOEither<E, A>
IO<A> rightIO: <E, A>(ma: IO<A>) => IOEither<E, A>

示例(加载随机文件)

假设我们要随机加载三个文件(1.txt、、)中的一个文件的内容2.txt3.txt

randomInt: (low: number, high: number) => IO<number>函数返回一个在闭区间内均匀分布的随机整数[low, high]

import { randomInt } from 'fp-ts/Random'
Enter fullscreen mode Exit fullscreen mode

我们可以randomIntreadFileSync上面定义的函数进行链接

import { ioEither } from 'fp-ts/IOEither'

const randomFile = ioEither.chain(
  randomInt(1, 3), // static error
  n => readFileSync(`${__dirname}/${n}.txt`)
)
Enter fullscreen mode Exit fullscreen mode

但这不进行类型检查!

类型不一致:在上下文randomInt 中运行,IO而在上下文readFileSync 中运行IOEither

然而,我们可以通过使用(参见上面的摘要)randomInt来提升上下文IOEitherrightIO

import { ioEither, rightIO } from 'fp-ts/IOEither'

const randomFile = ioEither.chain(rightIO(randomInt(1, 3)), n =>
  readFileSync(`${__dirname}/${n}.txt`)
)
Enter fullscreen mode Exit fullscreen mode
鏂囩珷鏉ユ簮锛�https://dev.to/gcanti/getting-started-with-fp-ts-io-36p6
PREV
fp-ts入门:Monad 问题:嵌套上下文 定义 好的,但是……为什么?Kleisli类别,我们一步步构建组合 定律 fp-ts中的Monad 结论
NEXT
开始使用 fp-ts:应用应用柯里化应用应用提升一般问题解决了吗?