Skip to content

dohye1(2주차):함수형 프로그래밍과 타입 시스템 그리고 LISP #10

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,300 @@
# 요약
- 함수형 패러다임은 불변성, 순수함수, 고차함수, 지연평가와 같은 개념을 사용해 예측가능하고 읽기 쉬운 코드를 작성하는데 도움을 준다.
- LISP는 코드 자체를 리스트 형태의 값으로 취급하는 접근법 > 유연하고 강력한 메타프로그래밍을 가능하게함

# 핵심 개념
## LISP
> LISP란?
> <br />
> 프로그래밍 언어의 계열. 괄호를 사용하는 문법으로 유명하다
> <br />
> '코드는 데이터다'라는 아이디어에서 출발함

- LISP는 코드 자체를 리스트 형태의 값으로 취급하는 접근법 > 유연하고 강력한 메타프로그래밍을 가능하게함
- 코드가 데이터, 데이터가 코드

- 리습의 기본 코드 단위는 s-표현(symbolic expression)이다

`(A (B 3) (C D) (()))`
- 첫번째인자는 함수혹은 연산자이고, 그 뒤에 오는 값들은 인자로 취급

## Closure
- 클로저는 리습을 현실세계의 문제를 더 잘 다룰 수 있도록 혁신한 버전이다.
- 리스트만 있던 리습에 비해 클로저는 맵, 벡터, 셋과 같은 데이터 자료형이 추가되었다.
- 클로저는 불변성과 일급함수를 강조하며, 동시성 프로그래밍과 관련된 강력한 기능을 지원하는 언어이다.
- 함수정의도 리스트형태로 표현하므로 함수 정의 구문 자체를 "코드이자 데이터 구조"로 다룰수있다.

## S-표현식
- 리습의 기본 코드 단위는 s-표현(symbolic expression)이다

`(A (B 3) (C D) (()))`
- 첫번째인자는 함수혹은 연산자이고, 그 뒤에 오는 값들은 인자로 취급

- 리스트 형태의 구문표현
<br />
`(+ 1 2)`
- 함수호출이 리스트 구조로 이루어지며 첫번째 요소가 연산자이고, 그 뒤 요소들이 인자이다.


## 메타 프로그래밍
- 프로그램이 자기 자신이나 다른 프로그램을 **데이터**처럼 바라보며 분석/변형/생성하거나 실행하는 프로그래밍 기법
- 코드를 데이터로 다루면서 동적으로 조작하고 확장
- 코드구조나 평가과정을 직접 재정의하거나 매크로를 통해 언어구문을 자유롭게 다룰수있다.

## 데이터 스트림
- 데이터의 흐름을 명확하고 직관적으로 표현하는데 도움을 준다.

# 2.1 타입 추론과 함수 타입 그리고 제네릭
- 타입추론 : 타입을 명시적으로 선언하지않아도 문맥에따라 타입스크립트 컴파일러가 자동으로 변수, 함수, 표현식 등의 타입을 추론해주는 기능
- 제네릭 : 유연하게 타입 사용 가능. 다형성!
- 함수 오버로드 : 동일한 함수명으로 다양한 시그니처를 정의할 수 있다. 함수의 유연성을 높이고 다양한 입력타입 처리가능!

```typescript
function double(a: number): number;
function double(a: string): string;
function double(a: number | string): number | string {
if (typeof a === 'number') {
return a * 2;
} else {
return a + a;
}
}

const num: number = double(10); // 20
const str: string = double('Hi'); // 'HiHi'
```

# 2.2 멀티 패러다임 언어에서 함수형 타입 시스템
## 이터러블 헬퍼함수
- 반복자 패턴을 활용한 함수형 고차 함수들은 이터러블 자료구조를 중심으로 구성되므로 이터러블 헬퍼함수라고 부른다.
- 앞서 정의했던 forEach, map, filter 중 map을 예시로 보기
```typescript
function* map<A, B>(f: (a: A) => B, iterable: Iterable<A>): IterableIterator<B> {
for (const a of iterable) {
yield f(a);
}
}

const array = ['1', '2', '3'];
const mapped = map(a => parseInt(a), array); // [a: string]
// [const mapped: IterableIterator<number>]
const array2: number[] = [...mapped];
console.log(array2);
// [1, 2, 3]

const [head] = map(a => a.toUpperCase(), ['a', 'b', 'c']);
console.log(head); // [head: string]
// A
```
- 타입 안정성을 유지하면서도 유연하게 제네릭타입을 활용한 고차함수를 구현할수있음

### 🚨 reduce 함수 오버로드
- reduce의 초기값을 생략가능
<details>
<summary>코드</summary>
<div markdown="1">

```typescript
function baseReduce<A, Acc>(
f: (acc: Acc, a: A) => Acc, acc: Acc, iterator: Iterator<A>
): Acc {
while (true) {
const { done, value } = iterator.next();
if (done) break;
acc = f(acc, value);
}
return acc;
}

// (1)
function reduce<A, Acc>(
f: (acc: Acc, a: A) => Acc, acc: Acc, iterable: Iterable<A>
): Acc;
// (2)
function reduce<A, Acc>(
f: (a: A, b: A) => Acc, iterable: Iterable<A>
): Acc;
function reduce<A, Acc>(
f: (a: Acc | A, b: A) => Acc,
accOrIterable: Acc | Iterable<A>,
iterable?: Iterable<A>
): Acc {
if (iterable === undefined) { // (3)
const iterator = (accOrIterable as Iterable<A>)[Symbol.iterator]();
const { done, value: acc } = iterator.next();
if (done) throw new TypeError("'reduce' of empty iterable with no initial value");
return baseReduce(f, acc, iterator) as Acc;
} else { // (4)
return baseReduce(f, accOrIterable as Acc, iterable[Symbol.iterator]());
}
}
```
</div>
</details>

**흥미로웠던 부분**
1. 두 함수 시그니처의 첫번쨰 인자 타입이 다름
2. 함수 구현체의 인자 타입

- 4장에서 에러핸들링과 옵셔널한 상황에 대한 여러관점 제시한다고해서 기대됨

# 2.3 멀티 패러타임 언어와 메타프로그래밍 - LISP로부터
- 객체지향의 class와 함수형의 고차함수를 사용해 pipe 형식 만들어보기

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

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

const mapped = new FxIterable(['a', 'b'])
.map(a => a.toUpperCase())
.map(b => b + b);

// [const mapped: FxIterable<string>]
// [a: string]
// [b: string]
```
- 반환값이 새로운 FxIterable 인스턴스이다. 그래서 체이닝 방식으로 map을 연속적으로 실행할 수 있다.
- 코드를 위에서 아래로 읽을수있다.

### 3가지 방식 비교
```typescript
// 함수 중첩
forEach(printNumber,
map(n => n * 10,
filter(n => n % 2 === 1,
naturals(5))));

// 파이프 오퍼레이터
naturals(5)
|> filter(n => n % 2 === 1, %)
|> map(n => n * 10, %)
|> forEach(printNumber, %)

// 체이닝
fx(naturals(5))
.filter(n => n % 2 === 1)
.map(n => n * 10)
.forEach(printNumber);
// 10
// 30
// 50
```

- 위에서 아래의 흐름대로 코드를 읽는게 가독성이 더 좋긴함.
- ⭐️ 연속적인 메서드 호출을 통해 데이터 변환방식을 직관적으로 표현할수있고, 각 단계가 명확하게 드러나기때문에 코드 흐름을 쉽게 파악할수있다.

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

`(+ 1 2)`
- 함수호출이 리스트 구조로 이루어지며 첫번째 요소가 연산자이고, 그 뒤 요소들이 인자이다.

### 리스트를 평가하는 함수
```typescript
type Evaluatable<A, B> = [(...args: A[]) => B, ...A[]];

function evaluation<A, B>(expr: Evaluatable<A, B>) {
const [fn, ...args] = expr;
return fn(...args);
}

const add = (a: number, b: number) => a + b;
const result: number = evaluation([add, 1, 2]);
console.log(result); // 3
```
- 타입스크립트로 함수를 호출하는 과정을 리스트형태의 데이터로 표현
- 이를 평가하는 방식으로 LISP의 "코드가 데이터"라는 개념 일부를 설명
- `[add, 1, 2]` 자체는 배열이자 데이터이다.
- 이때 이 데이터를 평가하는 함수가있다면 데이터를 코드로 만들어 평가한다.
> ✅ 데이터(배열)로 표현된 코드(함수 호출)을 evaluation함수를 통해 실제로 실행

### 기존에 없던 연산을 클로저에 추가
- reject
<details>
<summary>코드</summary>
<div markdown="1">

```lisp
(defn reject [pred coll]
(filter (complement pred) coll))

(let [[first second] (reject odd? (map #(+ % 10) [1 2 3 4 5 6]))]
(println first second))
;; 12 14
```
</div>
</details>
- 개발자가 원하는 로직을 직접 함수로 정의해서 언어기능에서 통합해서 사용

## 매크로
```lisp
(defmacro unless [test body]
`(if (not ~test) ~body nil))
```
- 코드(리스트 형태)를 입력받아 코드(리스트 형태)를 반환하는 하나의 함수
- 컴파일 타임에 작동하여 코드가 아직 실행되지않은 구문상태일때 원하는 형태로 재구성
- ⁉️ 함수 호출에서는 인자들이 먼저 평가된 뒤 함수에 전달되지만, 매크로에서는 인자들이 평가되지 않은 "원본 코드 형태"로 주어집니다. 이 말은 unless 매크로가 test와 body를 마치 함수의 인자처럼 받되, 그 값을 실행하지 않고 코드 구조(리스트) 자체로 취급한다는 의미입니다.
```lisp
(unless false
(println "조건이 거짓이므로 이 문장은 실행됩니다."))
```
- 여기서 false는 unless 매크로에서 test 인자로, (println "조건이 거짓이므로 이 문장은 실행됩니다.")는 body 인자로 전달됩니다. 이때 이들은 평가되지 않은 코드 조각(리스트) 형태 그대로 매크로에 넘어갑니다. 그리고 unless 매크로는 이 코드 조각들을 활용해 **컴파일 타임에 다음과 같은 새로운 코드를 생성합니다.**
```lisp
(if (not false)
(println "조건이 거짓이므로 이 문장은 실행됩니다.")
nil)
```


## 코드, 객체, 함수가 협력하여 구현한 언어의 확장
- 명령형 문법인 구조 분해 할당(Destructuring Assignment Syntax), 객체 지향 디자인 패턴인 메서드 체이닝 패턴(Method Chaining Pattern), 그리고 함수형 고차 함수(Higher-Order Functions)가
이터레이션 프로토콜을 매개로 긴밀하게 협력하여, 언어를 확장한 것 같은 높은 수준의 추상화와 유연성을 확보
```typescript
const [first, second] = fx([1, 2, 3, 4, 5, 6])
.map(a => a + 10)
.reject(isOdd);
```

## [개인의견] LISP를 통해 말하고자하는것
- LISP자체에 대한 이해가 아닌, LISP의 컨셉과 철학을 어떤 언어에서든 가져와서 상황에 맞게 사용할수 있는 방향으로 발전?해나가고 있다는것

## 런타임에서 동적으로 기능 확장하기
```typescript
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));
}
}

const result = fx([5, 2, 3, 1, 4, 5, 3])
.filter(n => n % 2 === 1)
.map(n => n * 10) // [50, 30, 10, 50, 30]
.chain(iterable => new Set(iterable)) // Set으로 중복 제거, Set도 이터러블
.reduce((a, b) => a + b); // [FxIterable<number>.reduce<number>(f: ...): number]

console.log(result); // [result: number]
// 90

const result2 = fx([5, 2, 3, 1, 4, 5, 3])
.filter(n => n % 2 === 1)
.map(n => n * 10) // [50, 30, 10, 50, 30]
.chain(iterable => new Set(iterable)) // Set으로 중복 제거, Set도 이터러블
.map(n => n - 10) // [FxIterable<number>.map<number>(f: ...): FxIterable<number>]
.reduce((a, b) => `${a}, ${b}`); // [FxIterable<number>.reduce<string>(f: ...): string]

console.log(result2); // [result2: string]
// 40, 20, 0
```
- 개발자가 언어를 즉시 확장할 수 있다는점이 메타 프로그래밍의 갖는 장점! > chain 처럼 다양한/동적 형태의 데이터 처리가 가능