July 20, 2021
본 포스트는 fp-ts 공식 문서의 Learning Resources에 있는 Functional design series에서 소개하는 문서들을 번역하며 학습한 문서입니다. 원본 문서는 링크에서 확인할 수 있으며 작성한 코드들은 여기에서 확인할 수 있습니다.
지난 포스트에서 유사한 Unix 명령을 모방하는 IO<A>
작업이 주어지면 실행 시간을 콘솔에 출력하는 작업 IO<A>
를 파생할 수 있는 time
combinator를 작성했습니다.
import type { IO } from 'fp-ts/lib/IO';
import { Monad } from 'fp-ts/lib/IO';
import { now } from 'fp-ts/lib/Date';
import { log } from 'fp-ts/lib/Console';
export function time<A>(ma: IO<A>): IO<A> {
return Monad.chain(now, (start) =>
Monad.chain(ma, (a) =>
Monad.chain(now, (end) =>
Monad.map(log(`Elapsed: ${end - start}`), () => a),
),
),
);
}
하지만 이 combinator에는 두 가지 문제가 있습니다.
IO
타입에 한해서 동작합니다.이 포스트에서는 유연하지 않은 첫 번째 문제를 다룰 것입니다.
항상 값을 출력하는 대신에 계산된 값과 함께 실행 시간을 반환할 수 있습니다.
import type { IO } from 'fp-ts/lib/IO';
import { now } from 'fp-ts/lib/Date';
import { Monad } from 'fp-ts/lib/IO';
export function time<A>(ma: IO<A>): IO<[A, number]> {
return Monad.chain(now, (start) =>
Monad.chain(ma, (a) => Monad.map(now, (end) => [a, end - start])),
);
}
이제는 다른 combinator를 정의해 실행 시간으로 무엇을 할 것인지 선택할 수 있습니다.
또한 여전히 콘솔에 출력할 수 있습니다.
export function withLogging<A>(ma: IO<A>): IO<A> {
return Monad.chain(time(ma), ([a, millis]) =>
Monad.map(log(`Result: ${a}, Elapsed: ${millis}`), () => a),
);
}
작성한 withLogging
combinator를 사용하는 방법
import { randomInt } from 'fp-ts/lib/Random';
function fib(n: number): number {
return n <= 1 ? 1 : fib(n - 1) + fib(n - 2);
}
const program = withLogging(map(fib)(randomInt(30, 35)));
program()
/*
Result: 14930352, Elapsed: 127
*/
또는 실행 시간을 무시할 수도 있습니다.
export function ignoreSnd<A>(ma: IO<[A, unknown]>): IO<A> {
return Monad.map(ma, ([a]) => a);
}
또는 예를 들어 비어 있지 않은 작업 목록 중 가장 빠른 것만 유지할 수도 있습니다.
import type { IO } from 'fp-ts/lib/IO';
import { Apply } from 'fp-ts/lib/IO';
import { Ord } from 'fp-ts/lib/number';
import { contramap } from 'fp-ts/lib/Ord';
import { getApplySemigroup } from 'fp-ts/lib/Apply';
import { concatAll, min } from 'fp-ts/lib/Semigroup';
export function fastest<A>(head: IO<A>, tail: Array<IO<A>>): IO<A> {
const ordTuple = contramap(([_, elapsed]: [A, number]) => elapsed)(Ord);
const semigroupTuple = min(ordTuple);
const semigroupIO = getApplySemigroup(Apply)(semigroupTuple);
const fastest = concatAll(semigroupIO)(time(head))(tail.map(time));
return ignoreSnd(fastest);
}
작성한 fastest
combinator를 사용하는 방법
Monad.chain(
fastest(program, [program, program]),
a => log(`Fastest result is: ${a}`)
)()
/*
Result: 5702887, Elapsed: 49
Result: 2178309, Elapsed: 20
Result: 5702887, Elapsed: 57
Fastest result is: 2178309
*/
다음 포스트에서는 강력한 프로그래밍 스타일인 tagless final을 도입해 두 번째 문제를 다루겠습니다.
fastest
combinator의 구현은 매우 조밀합니다. 상세한 내용을 살펴보겠습니다.
// 적어도 하나의 작업 --v v--- 가능한 다른 작업
function fastest<A>(head: IO<A>, tail: Array<IO<A>>): IO<A>
contramap
은 Ord
combinator입니다. T
에 대한 Ord
인스턴스와 U
에서 T
로의 함수가 주어지면 U
에 대한 Ord
인스턴스를 파생할 수 있습니다.// `Ord<number>`에서 `Ord<[A, number]>`로 파생
const ordTuple = contramap(([_, elapsed]: [A, number]) => elapsed)(Ord);
min
은 Ord<T>
인스턴스를 Semigroup<T>
인스턴스로 변환합니다. 이 인스턴스는 두 값을 결합할 때 더 작은 값을 반환합니다.// `Ord<[A, number]>`에서 `Semigroup<[A, number]>`로 변환
const semigroupTuple = min(ordTuple);
getSemigroup
은 Semigroup
combinator입니다. T
에 대한 Semigroup
인스턴스가 주어지면 IO<T>
에 대한 Semigroup
인스턴스를 파생할 수 있습니다.// `Semigroup<[A, number]>`에서 `Semigroup<IO<[A, number]>>`로 파생
const semigroupIO = getApplySemigroup(Apply)(semigroupTuple);
concatAll
는 제공된 Semigroup
을 사용하여 비어있지 않은 작업 목록을 줄입니다.// 비어있지 않은 배열 `IO<[A, number]>`에서`IO<[A, number]>`로 변환
const fastest = concatAll(semigroupIO)(time(head))(tail.map(time));
// `IO<[A, number]>`에서 `IO<A>`로 변환
return ignoreSnd(fastest);