UP | HOME

effect-ts 써보기

Table of Contents

1.

  • JS/TS의 try-catch는 쓰기 힘들다. catch에서 타이핑 정보가 없기 때문에. Result 모나드를 쓰고 싶다.
  • fp-ts를 종종 썼는데, 메인테이너가 effect-ts에 합류하게 되어서 관심이 생겼다.
  • 이펙트 패턴을 배워보고 싶다.
  • effect-ts의 사상이 맘에 든다: 늘 반복되는 에러 핸들링, 디버깅, 트레이싱과 같은 것들은 라이브러리화하기.
  • Rust, Elixir는 std 라이브러리가 상당히 좋고 문서화도 잘 되어 있어서 좋았는데, TS는 그렇지 않아서 늘 아쉬웠다.
  • TypeScript에 잘 만든 표준 라이브러리가 필요하다고 생각했는데 effect가 그 기능을 해줄 수 있을지 궁금하다.

1.1. Why Effect?

https://effect.website/docs/getting-started/why-effect/#dont-re-invent-the-wheel

Application code in TypeScript often solves the same problems over and over again. Interacting with external services, filesystems, databases, etc. are common problems for all application developers. Effect provides a rich ecosystem of libraries that provide standardized solutions to many of these problems. You can use these libraries to build your application, or you can use them to build your own libraries.

Managing challenges like error handling, debugging, tracing, async/promises, retries, streaming, concurrency, caching, resource management, and a lot more are made manageable with Effect. You don’t have to re-invent the solutions to these problems, or install tons of dependencies. Effect, under one umbrella, solves many of the problems that you would usually install many different dependencies with different APIs to solve.

대충 똑같은 문제 또 풀지 말고 멋진 라이브러리 하나 만들자는 얘기.

2. Effect System

https://www.sandromaglione.com/articles/typescript-code-with-and-without-effect

effect-ts는 effect system에 대한 TypeScript 구현체이다. 이름부터 effect이니만큼.

Effect System에 대한 설명은 아래 글에 잘 되어 있다. 다른 언어와 다른 시스템이긴 하지만 이해해 어려움이 없다:

effect system은 어떤 프로그램이 실행되는 과정에서 발생할 수 있는 부수 효과들도 타입으로 표현하는 시스템이다. 어떤 컴퓨팅 환경이든 I/O, mutable, exception 등 순수 함수의 속성을 위배하는 부수 효과가 존재할 수 밖에 없다. 보통 이런 효과에 대해서 타이핑이 있는데 부족하거나, 있는데 제네릭하지 않거나, 아예 안 되어 있거나 하다.

예를 들어보자. 비동기 연산은 부수 효과를 동반한다. 평가 시점에 따라 다른 값이 존재할 수 있기 때문이다. Promise는 아직 완료되지 않을 수도, 성공하여 원하는 값을 갖고 있을 수도, 실패하여 그 원인을 나타낼 수도 있다. 그래서 JavaScript에서는 이를 구분하기 위해 Promise로 비동기 연산을 표현한다. 아래처럼:

declare function getUser(userId: number): Promise<User>

위의 Promise 타입은 getUser라는 함수를 충분히 잘 설명하는가? 완료되지 않음(pending)과 완료(resolve)는 적절히 표현하고 있으나, 실패(reject) 시 어떤 실패가 있을 수 있는지에 대해 누락되어 있다. 위 타이핑만으로는 getUser가 실패시 어떤 결과를 내포하는지 알 수가 없다.

위 글 중 Rust의 예시를 보자:

fn by_value(cat: Cat) { .. }
fn by_reference(cat: &Cat) { .. }
fn by_mutable_reference(cat: &mut Cat) { .. }

위와 같은 패턴은 Rust 표준 라이브러리에도 아주 많다. 세 함수는 하고자 하는 본질적인 일은 같다. 다만 인자로 취하는 cat이 메모리에서 어떻게 다뤄져야 하는지, 부수효과 때문에 서로 다른 타이핑의 세 함수가 필요한 것이다.

effect system은 이러한 부수효과들을 타입으로서 표현하여 서로 합성가능하게 하고자 하는 시스템이다.

3. Effect Type

effect-ts에서는 이러한 부수 효과를 포함한 연산을 Effect라는 타입으로 표현한다.

         ┌─── 성공시의 타입
         │        ┌─── 실패시의 타입
         │        │      ┌─── 의존성
         ▼        ▼      ▼
Effect<Success, Error, Requirements>

여담으로, 헷갈리게 ZIO랑은 정반대이다. ZIO는 ZIO[R, E, A]로 의존성, 실패, 성공 순서이다. 아마 Haskell 계열의 관례를 따르는 듯. Rust는 성공, 실패 순이다.

3.1. 예외 처리 표현하기

Effect의 Success, Error를 활용하여 연산의 성공/실패를 표현할 수 있다. 성공시에는 Effect.succeed를, 실패시에는 Effect.fail을 이용하여 값을 반환한다.

import { Effect } from 'effect'

const getUser = (userId: number): Effect.Effect<User, Error> => {const user = userDatabase[userId]return user
│   │ ? Effect.succeed(user)
│   │ : Effect.fail(new Error('User not found'))
}

3.2. 동기 연산 표현하기

Effect.sync
항상 성공
Effect.try
try-catch
Effect.suspend
실패 가능
//     ┌─── Effect.Effect<User, Error, never>
//    ▼     실패할 수 있고, 의존성이 있음
const getUser = (userId: number) => Effect.suspend(() => {const user = userDatabase[userId]return user
│   │ ? Effect.succeed(user)
│   │ : Effect.fail(new Error('User not found'))
})

위의 getUser는 제일 처음의 getUser와 다르게 Effect.suspend로 감쌌다. 위의 getUser()는 실행 완료한 상태를 표현하는 Effect를 반환하고, suspend로 감싼 getUser는 실행 계획 자체를 반환한다. 즉 lazy하다.

3.2.1. fibonacci

import { Effect } from "effect";

const blowsUp = (n: number): Effect.Effect<number> => {console.log(`Creating blowsUp(${n})`);
│ return n < 2
│   ? Effect.sync(() => {
│   │   console.log(`blowsUp(${n})`);
│   │   return 1;
│   │ })
│   : Effect.zipWith(blowsUp(n - 1), blowsUp(n - 2), (a, b) => a + b);
};

const allGood = (n: number): Effect.Effect<number> => {console.log(`Creating blowsUp(${n})`);
│ return n < 2
│   ? Effect.sync(() => {
│   │   console.log(`allGood(${n})`);
│   │   return 1;
│   │ })
│   : Effect.zipWith(
│   │   Effect.suspend(() => allGood(n - 1)),
│   │   Effect.suspend(() => allGood(n - 2)),
│   │   (a, b) => a + b,
│   │ );
};

console.log(Effect.runSync(blowsUp(5)));
console.log(Effect.runSync(allGood(5)));

실행 결과를 보면 eaglr 한 blowsUp은 시작하자마자 Effect를 쫙 만드는 것을 볼 수 있다. $O(n2)$의 공간복잡도를 사용한다. lazy한 allGood은 객체 하나씩 생성하므로 $O(n)$의 공간복잡도를 쓴다. 둘 다 시간복잡도는 $O(n2)$으로 비효율적이지만 메모리 사용에서 차이가 난다:

> @template/basic@0.0.0 dev /Users/nyeong/Playground/effect-test
> tsx src/Program.ts

Creating blowsUp(5)
Creating blowsUp(4)
Creating blowsUp(3)
Creating blowsUp(2)
Creating blowsUp(1)
Creating blowsUp(0)
Creating blowsUp(1)
Creating blowsUp(2)
Creating blowsUp(1)
Creating blowsUp(0)
Creating blowsUp(3)
Creating blowsUp(2)
Creating blowsUp(1)
Creating blowsUp(0)
Creating blowsUp(1)
blowsUp(1)
blowsUp(0)
blowsUp(1)
blowsUp(1)
blowsUp(0)
blowsUp(1)
blowsUp(0)
blowsUp(1)
8
Creating blowsUp(5)
Creating blowsUp(4)
Creating blowsUp(3)
Creating blowsUp(2)
Creating blowsUp(1)
allGood(1)
Creating blowsUp(0)
allGood(0)
Creating blowsUp(1)
allGood(1)
Creating blowsUp(2)
Creating blowsUp(1)
allGood(1)
Creating blowsUp(0)
allGood(0)
Creating blowsUp(3)
Creating blowsUp(2)
Creating blowsUp(1)
allGood(1)
Creating blowsUp(0)
allGood(0)
Creating blowsUp(1)
allGood(1)
8

3.3. 비동기 연산 표현하기

일반적으로는 비동기 연산을 표현하기 위해 Promise<T>를 쓴다. Promise는 성공시의 타입 T만 표현할 수 있다는 한계가 있다. Effect에서는 Effect.promiseEffect.tryPromise를 써서 이를 극복한다.

Effect.promise
항상 성공
Effect.tryPromise
try-catch
Effect.async
실패 가능
//     ┌─── Effect.Effect<string, never, never>
//    ▼     항상 성공하고 의존성이 없음을 타입으로 나타냄
const getUser = (userId: number) => Effect.tryPromise({try: () => fetchUserFromDb(userId)
│   │ .then(user => {
│   │   │ if (!user) throw new Error('User not found')
│   │   │ return user
│   │ }),
│   catch: (error) => error
})

3.4. Effect 실행하기

  • Effect.runPromise
  • Effect.runSync

4. 참고

Author: 안녕

Created: 2024-12-10 Tue 22:08