fp-ts 入门:Either 与 Validation 问题 Either Validation 附录

2025-06-05

fp-ts 入门:Either 与 Validation

问题

任何一个

验证

附录

问题

假设你需要实现一个 Web 表单来注册一个账户。该表单包含两个字段:usernamepassword,并且必须满足以下验证规则:

  • username不能为空
  • username不能有破折号
  • password至少需要 6 个字符
  • password至少需要有一个大写字母
  • password至少需要有一个数字

任何一个

Either<E, A>类型表示计算可能因类型错误而失败E或因类型值而成功A,因此是实现我们的验证规则的良好候选。

例如,让我们对每个password规则进行编码

import { Either, left, right } from 'fp-ts/Either'

const minLength = (s: string): Either<string, string> =>
  s.length >= 6 ? right(s) : left('at least 6 characters')

const oneCapital = (s: string): Either<string, string> =>
  /[A-Z]/g.test(s) ? right(s) : left('at least one capital letter')

const oneNumber = (s: string): Either<string, string> =>
  /[0-9]/g.test(s) ? right(s) : left('at least one number')
Enter fullscreen mode Exit fullscreen mode

我们可以使用以下方式链接所有规则...chain

import { chain } from 'fp-ts/Either'
import { pipe } from 'fp-ts/function'

const validatePassword = (s: string): Either<string, string> =>
  pipe(
    minLength(s),
    chain(oneCapital),
    chain(oneNumber)
  )
Enter fullscreen mode Exit fullscreen mode

因为我们使用的Either检查是快速失败的。也就是说,任何失败的检查都会短路后续的检查,所以我们只会收到一个错误。

console.log(validatePassword('ab'))
// => left("at least 6 characters")

console.log(validatePassword('abcdef'))
// => left("at least one capital letter")

console.log(validatePassword('Abcdef'))
// => left("at least one number")
Enter fullscreen mode Exit fullscreen mode

然而,这可能会导致糟糕的用户体验,如果能同时报告所有这些错误就更好了。

抽象Validation在这里可能会有帮助。

验证

验证非常相似Either<E, A>,它们表示可能因类型错误而失败E或因类型值而成功计算A,但与通常的计算相反Either,它们能够收集多个失败

为了做到这一点,我们必须告诉验证如何组合两个类型的值E

这就是半群的意义所在:组合两个相同类型的值。

例如,我们可以将错误打包到非空数组中。

'fp-ts/Either'模块提供了一个getValidation函数,给定一个半群,返回一个替代的Applicative实例Either

import { getSemigroup } from 'fp-ts/NonEmptyArray'
import { getValidation } from 'fp-ts/Either'

const applicativeValidation = getValidation(getSemigroup<string>())
Enter fullscreen mode Exit fullscreen mode

然而为了使用applicativeValidation我们必须首先重新定义所有规则以便它们返回类型的值Either<NonEmptyArray<string>, string>

我们不必重写所有以前的函数,因为这样很麻烦,而是定义一个组合器,将输出的支票转换Either<E, A>为输出的支票Either<NonEmptyArray<E>, A>

import { NonEmptyArray } from 'fp-ts/NonEmptyArray'
import { mapLeft } from 'fp-ts/Either'

function lift<E, A>(check: (a: A) => Either<E, A>): (a: A) => Either<NonEmptyArray<E>, A> {
  return a =>
    pipe(
      check(a),
      mapLeft(a => [a])
    )
}

const minLengthV = lift(minLength)
const oneCapitalV = lift(oneCapital)
const oneNumberV = lift(oneNumber)
Enter fullscreen mode Exit fullscreen mode

让我们把所有内容放在一起,我将使用sequenceT助手来执行n操作并从左到右执行,返回结果元组

import { sequenceT } from 'fp-ts/Apply'
import { map } from 'fp-ts/Either'

function validatePassword(s: string): Either<NonEmptyArray<string>, string> {
  return pipe(
    sequenceT(getValidation(getSemigroup<string>()))(
      minLengthV(s),
      oneCapitalV(s),
      oneNumberV(s)
    ),
    map(() => s)
  )
}
console.log(validatePassword('ab'))
// => left(["at least 6 characters", "at least one capital letter", "at least one number"])
Enter fullscreen mode Exit fullscreen mode

附录

请注意,sequenceT助手能够处理不同类型的操作:

interface Person {
  name: string
  age: number
}

// Person constructor
const toPerson = ([name, age]: [string, number]): Person => ({
  name,
  age
})

const validateName = (s: string): Either<NonEmptyArray<string>, string> =>
  s.length === 0 ? left(['Invalid name']) : right(s)

const validateAge = (s: string): Either<NonEmptyArray<string>, number> =>
  isNaN(+s) ? left(['Invalid age']) : right(+s)

function validatePerson(name: string, age: string): Either<NonEmptyArray<string>, Person> {
  return pipe(
    sequenceT(applicativeValidation)(validateName(name), validateAge(age)),
    map(toPerson)
  )
}
Enter fullscreen mode Exit fullscreen mode
文章来源:https://dev.to/gcanti/getting-started-with-fp-ts-either-vs-validation-5eja
PREV
开始使用 fp-ts:函子函数作为程序其中约束 B = F<C> 导致函子函子 fp-ts 中的函子一般问题解决了吗?
NEXT
功能设计:智能构造函数