June 12, 2021
본 포스트는 fp-ts 공식 문서의 Learning Resources에 있는 Getting Started에서 소개하는 문서들을 번역하며 학습한 문서입니다. 원본 문서는 링크에서 확인할 수 있으며 작성한 코드들은 여기에서 확인할 수 있습니다.
계정을 등록하기 위한 웹 양식을 구현해야 합니다. 양식에는 username
과 password
두 가지 필드가 있으며 아래의 유효성 검사 규칙을 따라야 합니다.
username
은 비워둘 수 없습니다.username
에 –
를 포함할 수 없습니다.password
는 6자 이상이어야 합니다.password
는 적어도 하나의 대문자를 가져야 합니다.password
는 최소 하나의 숫자를 가져야 합니다.Either<E, A>
타입은 E
타입의 오류로 실패하거나 A
타입의 값으로 성공할 수 있는 계산을 나타내므로 유효성 검사 규칙을 구현하기에 좋은 후보입니다.
예를 들어 각 비밀번호 규칙을 인코딩해 보겠습니다.
import type { Either } from 'fp-ts/lib/Either';
import { left, right } from 'fp-ts/lib/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
을 이용해 묶을 수 있습니다.
원문에서는
pipeable/pipe
를 사용하라고 작성되어 있지만, 최신 버전의 fp-ts에서는 deprecated 되어 있으며function/pipe
를 사용하면 됩니다.
import type { Either } from 'fp-ts/lib/Either';
import { chain } from 'fp-ts/lib/Either';
import { pipe } from 'fp-ts/lib/function';
export const chainValidatePassword = (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")
그러나 이것은 나쁜 UX로 이어질 수 있으며 이런 모든 오류를 동시에 보고하는 것이 좋습니다.
여기에서 Validation
추상화가 도움이 될 수 있습니다.
Validation
은 Either<E, A>
와 매우 유사하며 E
타입의 오류로 실패하거나 A
타입의 값으로 성공할 수 있는 계산을 나타내지만 Either
의 일반적인 계산과는 달리 여러 실패를 수집할 수 있습니다.
이를 위해서는 E
타입의 두 값을 결합하는 방법을 Validation
에게 알려야 합니다.
동일한 타입의 두 값을 결합하는 것이 Semigroup의 모든 것입니다.
예를 들어 오류를 비어있지 않은 배열에 압축 할 수 있습니다.
'fp-ts/lib/Either'
모듈은 Semigroup
이 주어지면 Either
에 대한 대체 Applicative 인스턴스를 반환하는 합니다.getValidation
함수를 제공
원문에서는
getValidation
을 사용하라고 작성되어 있지만, 최신 버전의 fp-ts에서는 deprecated 되어 있으며getApplicativeValidation
를 사용하면 됩니다.
import { getSemigroup } from 'fp-ts/lib/NonEmptyArray';
import { getApplicativeValidation } from 'fp-ts/lib/Either';
const applicativeValidation = getApplicativeValidation(getSemigroup<string>());
그러나 applicativeValidation
을 사용하려면 먼저 Either<NonEmptyArray<string>, string>
타입의 값을 반환하도록 모든 규칙을 재정의 해야 합니다.
번거롭게 이전 함수를 모두 다시 작성하는 대신 Either<E, A>
를 반환하는 것을 Either<NonEmptyArray<E>, A>
를 반환하도록 콤비네이터를 정의할 수 있습니다.
import type { NonEmptyArray } from 'fp-ts/lib/NonEmptyArray';
import { mapLeft } from 'fp-ts/lib/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);
모두 합쳐서 n
개의 작업을 왼쪽에서 오른쪽으로 수행하여 결과 튜플을 반환하는 sequenceT
헬퍼 함수를 사용할 것입니다.
import { sequenceT } from 'fp-ts/lib/Apply';
import { map } from 'fp-ts/lib/Either';
function validatePassword(s: string): Either<NonEmptyArray<string>, string> {
return pipe(
sequenceT(getApplicativeValidation(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 생성자
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)
);
}