프로그래밍 공부/javascript & typescript

[Functional Programming] 1장: 기본 배열 함수 구현하기

99mini 2025. 4. 19. 08:00
반응형
SMALL

0. 프로젝트 구조 및 라이브러리

├── jest.config.ts
├── package.json
├── tsconfig.json
├── /src
│   ├── index.ts
│   └── index.spec.ts

사용한 라이브러리 (devDependency)

"@types/jest": "^29.5.14",
"jest": "^29.7.0",
"ts-jest": "^29.3.2",
"ts-node": "^10.9.2",
"typescript": "^5.8.3"

1. Fx 클래스 도입

export class Fx<T> implements Iterable<T> {
  constructor(private iterable: Iterable<T>) {}
  [Symbol.iterator]() {
    return this.iterable[Symbol.iterator]();
  }

  static of<T>(iter: Iterable<T>): Fx<T> {
    return new Fx(iter);
  }
  // ...
}
  • static of(...): Fx 객체를 생성하는 팩토리 메서드입니다.
  • Symbol.iterator(): Fx 자체도 이터러블로 사용할 수 있습니다.

체이닝 가능한 API 설계

Fx의 모든 method는 Fx인스턴스를 반환하며 불편성을 지키며 메서드 체이닝이 가능하게 설계합니다.

Fx.of(data)
  .filter(...)
  .map(...)
  .take(...)
  .toArray()
.map(fn) → Fx<U>  
.filter(fn) → Fx<T>  
.take(n) → Fx<T>

2. map

map<U>(fn: (item: T) => U): Fx<U> {
  const self = this;
  function* generator() {
    for (const item of self.iterable) {
      yield fn(item);
    }
  }
  return new Fx(generator());
}
  • map은 각 항목에 fn 함수를 적용하여 새 값을 만들어냅니다.
  • function*으로 구현했기 때문에 지연 평가(lazy evaluation)가 작동합니다. 실제로 .toArray() 등으로 소비되기 전까지 연산이 수행되지 않습니다.
  • 원본 데이터는 그대로 유지되고 변환된 새로운 Fx가 생성됩니다.

3. filter

filter(fn: (item: T) => boolean): Fx<T> {
  const self = this;
  function* generator() {
    for (const item of self.iterable) {
      if (fn(item)) yield item;
    }
  }
  return new Fx(generator());
}
  • filter는 각 항목에 조건을 적용해 true인 경우만 yield합니다.
  • Fx는 배열이 아니기 때문에 메모리 전체에 값을 저장하지 않으며 필요한 값만 즉시 계산하여 반환합니다.

4. take

take(n: number): Fx<T> {
  const self = this;
  function* generator() {
    let i = 0;
    for (const item of self.iterable) {
      if (i++ >= n) break;
      yield item;
    }
  }
  return new Fx(generator());
}
  • take(n)은 최대 n개까지만 값을 생성합니다.
  • 배열의 slice(0, n)과는 달리 앞에서부터 순회하며 조건 만족 시 즉시 종료합니다.
  • 무한한 이터러블에도 사용할 수 있는 점에서 유용하게 사용될 수 있습니다.

예시

Fx.of(range()) // 무한 range generator
  .map(x => x * x)
  .filter(x => x % 2 === 0)
  .take(10)
  .toArray();

5. toArray

toArray(): T[] {
  return [...this.iterable];
}
  • toArray()는 지연된 이터러블을 한 번에 평가하여 실제 배열로 변환합니다.
  • 이 함수가 호출되기 전까지는 map, filter, take 등은 평가되지 않고 순수하게 설정된 연산만 보관됩니다.

6. 전체코드

// index.ts
export class Fx<T> implements Iterable<T> {
  private iterable: Iterable<T>;

  constructor(iterable: Iterable<T>) {
    this.iterable = iterable;
  }

  static of<T>(iterable: Iterable<T>): Fx<T> {
    return new Fx(iterable);
  }

  map<U>(fn: (item: T) => U): Fx<U> {
    const self = this;
    function* generator() {
      for (const item of self.iterable) {
        yield fn(item);
      }
    }
    return new Fx(generator());
  }

  filter(fn: (item: T) => boolean): Fx<T> {
    const self = this;
    function* generator() {
      for (const item of self.iterable) {
        if (fn(item)) yield item;
      }
    }
    return new Fx(generator());
  }

  take(n: number): Fx<T> {
    const self = this;
    function* generator() {
      let i = 0;
      for (const item of self.iterable) {
        if (i++ >= n) break;
        yield item;
      }
    }
    return new Fx(generator());
  }

  toArray(): T[] {
    return [...this.iterable];
  }

  [Symbol.iterator](): Iterator<T> {
    return this.iterable[Symbol.iterator]();
  }
}
반응형
LIST