fp-ts 入门:Either 与 Validation
问题
任何一个
验证
附录
问题
假设你需要实现一个 Web 表单来注册一个账户。该表单包含两个字段:username
和password
,并且必须满足以下验证规则:
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')
我们可以使用以下方式链接所有规则...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)
)
因为我们使用的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")
然而,这可能会导致糟糕的用户体验,如果能同时报告所有这些错误就更好了。
抽象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>())
然而为了使用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)
让我们把所有内容放在一起,我将使用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"])
附录
请注意,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)
)
}