Skip to content

rae-han(2주차): 2. 함수형 프로그래밍과 타입 시스템 그리고 LISP #8

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 18, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,285 @@
# 2.1 타입 추론과 함수 타입 그리고 제네릭

## 2.1.1 타입 추론

타입스크립트의 타입 추론은 타입을 명시적으로 선언하지 않아도 타입을 추론해주는 기능이다.

변수, 함수의 반환 값, 객체 그리고 고차 함수의 인자 타입 또한 추론해 준다. 또한 제네릭 함수를 사용해 다형성을 높여 다양한 타입을 지원하는 범용 함수를 만들 수 있게 해준다.

타입 추론은 코드의 가독성과 안정성을 높이는데 중요한 역할을 한다.

## 2.1.2 함수 타입과 제네릭

제네릭을 활용해 폭넓은 타입을 지원하는 범용 함수를 만들 수 있다. 고차 함수에서는 인자로 전달받은 함수의 매개변수 타입을 추론하고 함께 전달된 인자들의 타입도 유연하게 추론하도록 돕는다.

함수 오버로드와 타입 가드를 통해 다양한 입력 타입을 안전하게 처리 가능하다.

또한 타입 별칭(type)을 통해 재사용 가능하다.

인자로 받은 값을 항상 그대로 돌려주는 항등 함수를 이용해 다양한 타입의 값을 처리할 수 있다. 아래와 같이 같은 역할을 하는 contant 라는 함수가 있다고 해보자. 이때 제네릭을 이용해 모든 타입에 대해 동작하게 할 수 있다.

```tsx
function constant<T>(a: T): () => T {
return () => a;
}

const getFive = constant(5);
const ten: number = getFive() + getFive();
console.log(ten); // 10

const getHi = constant("Hi");
const hi2: string = getHi() + getHi();
console.log(hi2); // HiHi
```

# 2.2 멀티패러다임 언어에서 함수형 타입 시스템

## 2.2.1 이터레이션 프로토콜과 타입 다시 보기

**Iterator**: **`next()`** 메서드로 순회하며, **`{ done, value }`** 형태의 결과를 반환한다.

**Iterable**: **`[Symbol.iterator]()`** 메서드로 이터레이터를 반환하는 객체입니다. **`for...of`**나 전개 연산자에서 사용할 수 있다.

**IterableIterator**: 이터레이터이면서 동시에 이터러블인 객체로, 제너레이터 함수의 반환값이 대표적이다.

**`for...of`**나 전개 연산자 사용 가능 여부는 이터러블에 달려 있다.

제너레이터 함수는 **`IterableIterator`**를 반환합니다.

이터레이터와 이터러블 개념을 바탕으로 고차 함수(map, filter 등)를 구현할 수 있습니다.

## 2.2.2 함수형 고차 함수와 타입 시스템

reduce의 에러 처리는 4장에서 좀 더 자세히

## 2.2.3 함수 시그니처와 중첩된 함수들의 타입 추론

고차 함수들을 중첩해 사용할 때, 타입스크립트는 각 함수의 인자 타입을 자동으로 추론한다.

# 2.3 멀티패러다임 언어와 메타프로그래밍 - LISP로부터

메타프로그래밍이란 프로그램이 자기 자신이나 다른 프로그램을 데이터처럼 바라보며 분석, 변형, 생성하거나 실행하는 프로그래밍 기법이다.

## 2.3.1 Pipe Operator

pipe operator 를 이용하면 가독성이 좋아진다.

## 2.3.2 클래스와 고차 함수, 반복자, 타입 시스템을 조합하기

FxIterable **클래스**를 사용하여 다양한 고차 함수들을 메서드로 추가 가능하다.

```tsx
class FxIterable<A> {
constructor(private iterable: Iterable<A>) {}
}
```

그리고 FxIterable 클래스에 메서드를 구한하여 함수형 메서드 체이닝을 구현 가능하다.

```tsx
class FxIterable<A> {
constructor(private iterable: Iterable<A>) {}

map<B>(f: (a: A) => B): FxIterable<B> {
return new FxIterable(map(f, this.iterable));
}

filter(f: (a: A) => boolean): FxIterable<A> {
return new FxIterable(filter(f, this.iterable));
}

reduce<Acc>(f: (acc: Acc, a: A) => Acc, acc: Acc): Acc {
return reduce(f, acc, this.iterable);
}
}
```

또한 fx 함수로 new FxIterable 부분을 간결하게 표현 가능하다.

```tsx
function fx<A>(iterable: Iterable<A>): FxIterable<A> {
return new FxIterable(iterable);
}
```

```tsx
class FxIterable<A> {
constructor(private iterable: Iterable<A>) {}

map<B>(f: (a: A) => B): FxIterable<B> {
return fx(map(f, this.iterable));
}

filter(f: (a: A) => boolean): FxIterable<A> {
return fx(filter(f, this.iterable));
}

reduce<Acc>(f: (acc: Acc, a: A) => Acc, acc: Acc): Acc {
return reduce(f, acc, this.iterable);
}
}
```

## 2.3.3 LISP(클로저)에서 배우기 - 코드가 데이터, 데이터가 코드

LISP의 가장 큰 특징은 코드가 데이터이고 데이터가 코드라는 개념이다. 이를 통해 프로그래밍 언어의 구문을 데이터 구조로 표현하고 조작할 수 있다.

LISPdml S-표현식은 리스트 형태의 구문 표현을 의미한다. 이를 통해 코드와 데이터를 동일한 구조(리스트)로 다룰수 있다(`(+ 1 2)`).

## 2.3.4 클로저에서 map이 실행될 때

다음 코드는 익명 함수와 백터를 받아 벡터의 각 요소에 함수를 적용 시켜 값을 반환하는 map 함수가 조합돼 있다.

```clojure
(map #(+ % 10) [1 2 3 4])
```

여기서 `#(+ % 10)` 리더 매크로에 의해 `(fn [x] (+ x 10))` 형태의 익명 함수로 확장된다. 리더 매크로는 클로저 같은 언어가 소스 코드를 읽는 단계에 특정 기호, 패턴을 정해진 형태의 다른 코드로 치환하는 기능이다.
클로저에서는 함수 정의도 `(fn [x] (+ x 10))` 같은 리스트 형태로 표현하므로 함수 정이의 구문 자체를 코드이자 데이터 구조로 다룰 수 있다.

또한 이때 생성된 값은 지연 시퀀스로 값이 필요한 시점까지 평가를 지연시키다 값이 필요할 때 충첩된 리스트의 조합을 실제 로직으로 완성하고 실행시킨다.

## 2.3.5 멀티패러다임 언어에서 사용자가 만든 코드이자 클래스를 리스트로 만들기

타입스크립트에서 필요한 값만 가져오려면 어떻게 해야할까?? 우선 값을 평가하는 함수를 만들어보자.

```tsx
class FxIterable<A> {
constructor(private iterable: Iterable<A>) {}

// ...기존 코드 생략

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

const [first, second] = fx([1, 2, 3, 4])
.map((a) => a + 10)
.toArray();
```

위 코드는 정상적으로 동작 하겠지만 toArray 함수가 모든 값을 다 평가해 map 함수 내부에서 로그를 찍어본다면 평가할 값이 모두 찍혔을 것이다.

이때 FxIterable을 이터레이션 프로토콜을 따르는 값으로 만드는 것이다.
FxIterable은 FxIterable을 이터러블로 만드는 것만으로 충분하다.

```tsx
class FxIterable<A> {
constructor(private iterable: Iterable<A>) {}

[Symbol.iterator]() {
return this.iterable[Symbol.iterator]();
}

// ...기존 코드 생략
}

const [first, second] = fx([1, 2, 3, 4]).map((a) => a + 10);
```

위 코드는 필요한 값만 평가할 것이다. 즉 앞쪽 두 개의 값만 평가할 것이다.

## 2.3.6 LISP의 확장성

LISP 계열 언어에서 매크로는 단순한 텍스트 치환이 아니라 코드(리스트 형태)를 입력받아 코드(리스트 형태)를 반환하는 하나의 함수라 할 수 있다.

지금까지 구현 했던 FxIterable 함수에서 reject 함수를 구현한다면 아래와 같이 filter 함수를 사용해 볼수 있다.

```tsx
class FxIterable<A> {
constructor(private iterable: Iterable<A>) {}

[Symbol.iterator]() {
return this.iterable[Symbol.iterator]();
}

// ... 생략된 메서드들 ...

reject(f: (a: A) => boolean): FxIterable<A> {
return this.filter((a) => !f(a));
}
}
```

## 2.3.7 런타임에서 동적으로 기능 확장하기

앞서 만든 toArray 메서드는 체이닝을 이어나갈수 있게 해준다.

```tsx
const sorted = fx([5, 2, 3, 1, 4, 5, 3])
.filter((n) => n % 2 === 1)
.map((n) => n * 10)
.toArray() // Array<number>로 변환
.sort((a, b) => a - b); // Array.prototype.sort로 오름차순으로 정렬

const sorted2 = [
...fx([5, 2, 3, 1, 4, 5, 3])
.filter((n) => n % 2 === 1)
.map((n) => n * 10),
].sort((a, b) => a - b);
```

위 두 코드를 비교하면 가독성에서 차이가 난다.

아래처럼 개발자가 필요할 때 toArray 메서드처럼 다른 타입으로 만드는 메서드를 동적으로 확장할 수 있도록 하는 to 메서드이다.

```tsx
class FxIterable<A> {
constructor(private iterable: Iterable<A>) {}

[Symbol.iterator](): Iterator<A> {
return this.iterable[Symbol.iterator]();
}

// ... 생략된 메서드들 ...

to<R>(converter: (iterable: Iterable<A>) => R): R {
return converter(this.iterable);
}
}
```

추가로 FxIterable 자체로 곧 Iterable 이므로 아래와 같이 this만 넘기도록 구현해도 문제 없다.

```tsx
class FxIterable<A> {
constructor(private iterable: Iterable<A>) {}

[Symbol.iterator](): Iterator<A> {
return this.iterable[Symbol.iterator]();
}

// ... 생략된 메서드들 ...

filter(f: (a: A) => boolean) {
return fx(filter(f, this)); // <-- before: return fx(filter(f, this.iterable));
}

toArray() {
return [...this]; // <-- before: return [...this.iterable];
}

to<R>(converter: (iterable: this) => R): R {
return converter(this); // <-- before: return converter(this.iterable);
}
}
```

유사한 개념의 chain 메서드도 구현해보자. 이 메서드는 이터러블을 반환하는 함수를 인자로 받아 그 결과를 다시 FxIterable로 이어갈수 있게 해준다.

```tsx
class FxIterable<A> {
constructor(private iterable: Iterable<A>) {}

[Symbol.iterator](): Iterator<A> {
return this.iterable[Symbol.iterator]();
}

// ... 생략된 메서드들 ...

chain<B>(f: (iterable: this) => Iterable<B>): FxIterable<B> {
return fx(f(this)); // new FxIterable(f(this));
}
}
```