horizontal-library / functional-programming-in-javascript

함수형 자바스크립트 스터디
1 stars 0 forks source link

[keyword] 4장 - 재사용 가능한, 모듈적인 코드로 [1/2] #13

Open ABizCho opened 1 year ago

ABizCho commented 1 year ago

주제

4장 - 재사용 가능한, 모듈적인 코드로 [1/2]을 읽고 내용을 요약하거나,
중요✨ 하다고 생각하는 키워드 및 관련 설명을 코멘트로 달아주세요

연관 챕터

12

ABizCho commented 1 year ago

(작성중)

4장에서는 함수 합성을 통해 느슨하게 결합된 파이프라인을 만들고...

3장에서는 고수준 함수를 써서 하나의 래퍼 객체를 중심으로 단단하게 결합하였다고 했다. 이는 래퍼 객체 내부에서 구현된 메서드라는 제약이 있었으나, 함수 합성을 통한 파이프라인 구축은 보다 유연한 독립적 컴포넌트, 즉, 외부에서 구현된 것을 사용할 수 있는 것 이라고 이해했다.


함수형 프로그래밍에서 함수란 입력 형식과 출력 형식 간의 수학적 매핑.


메서드 체이닝 (단단한 결합)

메서드 체이닝 예제 중.. : 이 예제는 로대시JS가 제공하는 연산만 쓸 수 있기 때문에 다른 라이브러리 함수를 쉽게 연결할 수 없습니다.

이것이 메서드체이닝이 래퍼 객체를 중심으로 단단하게결합되었기 때문에 발생하는 제한의 예이다.


함수를 파이프라인에 나열 (느슨하고 유연할 결합)

함수형 프로그래밍에서는 메서드 체이닝의 한계에서 벗어나 출신(자신을 소유한 객체)에 관계 없이 어떤 함수라도 유연하게 결합할 수 있습니다. 파이프라인이란 한 함수의 출력이 다음 함수의 입력이 되게끔 느슨하게 배열한 방향성함수 순차열입니다.


함수 호환 요건 (형식/인수)

  1. 형식이 호환되는 함수

    • js는 형식이 느슨한 언어이기 때문에 어떤 객체가 실제로 특정 형식처럼 작동하면 그 형식은 그냥 그 객체의 형식으로 본다. 이를 덕 타이핑이라고 부름

    • 자바스크립트는 동적 파견 체제덕분에 형식과 무관하게 객체에서 속성과 메서드를 가져올 수 있다?? -> REF: Dynamic Dispatch를 알면 코드가 빨라진다

  2. 함수와 항수: 항수란 함수가 받는 인수의 개수, 함수의 길이. JS에서의 FP는 함수가 취하는 인수 개수의 호환 여부가 더욱 중요하다. (물론 타입 매우 중요함) -> 타입의 일치는 아마 타입스크립트로 해결할 수 있는 문제인듯

    FP에서는 함수에 선언된 인수의 개수가 (참조투명성의 당연한 결과로서) 복잡도와 정확히 비례하는 경우가 많다.

이 책에서 권장하는 목표는 함수의 인수를 가능한 한 적게 만드는 것으로 단일책임의 유연하고 다목적의 함수로 만들어내는 것.


함수 커링

튜플은 항수를 줄이는 방법 중 하나이다. 튜플로 함수의 항수를 줄일 순 있지만, 더 나은 대체방안인 함수 커링이 있다. 이는 항수를 추상하는 동시에 모듈성, 재사용성을 높인다.

일반 평가 vs 커리된 평가 차이점

JS에서는 (비커리된)일반 함수를 호출할 때 인수가 모자라도 문제없이 실행시킨다

함수 f(a,b,c)에 대해 f()에 a 인수만 넣어도 js런타임은 빈 매개변수 b,c를 undefined로 자동 세팅 및 실행하는데, 함수 안에서 args 객체에 전적 의존하는 것은 문제를 키울 위험이 있다.


커리된 함수: 모든 매개변수가 명시된 커리된 함수에 일부 인수만 넣어 호출하면, 함수가 실행되는게 아니라 모자란 나머지 인수가 다 채워지기를 기다리는 새로운 함수가 반환된다. 이 끝에 다 채워졌을 때 결국 결과값이 반환된다.

// 수동으로 만드는 커리 함수 구조
function curry(f) { // 커링 변환을 하는 curry(f) 함수
  return function(a) {
    return function(b) {
      return f(a, b);
    };
  };
}

// arrow로 표현한 커리 함수
const func = (a) => (b) => (c) => res


람다JS를 사용해 순수함수형 스타일 개발이 가능. 모든 람다JS의 기능은 전역변수 R을 통해 접근 사용 가능. 이는 curry함수도 지원한다.

R.curry를 쓰면 인수 개수 상관 없이 순수함수형 언어의 자동 커링 장치 모방 가능. 자동 커링은 선언된 인수 개수만큼 중첩된 함수 스코프 인위적으로 생성해줌



커링을 어디에 쓰나?

  1. 함수 팩토리 모방
  2. 재사용 가능한 모듈적 함수 템플릿 구현


1. 함수 팩토리 모방

const fetchStudentFormDb = R.curry(function (db, ssm) {
return find(db, ssn)});

const fetchStudentFormArray = R.curry(function (arr, ssn) {
return arr[ssn]});
// 동적 정의 및 평가지연 (arg1)
const findStudent = useDb ? fetchStudentFromDb(db)
                    : fetchStudentFromArray(arr);

// 평가부 (arg2: 공통부분)
findStudent('444-44-4444');

위의 예제는 유용하게 쓰일 것 같아서 기록한다. (단위 테스트에서 조건부로 사용해도 좋을듯)

재사용 가능한 함수 템플릿 구현

...

무엇보다 커링의 가장 중요한 의의는 다인수 함수를 단항 함수로 바꾼다는 것이다.



부분 적용(partial), 매개변수 바인딩(bind)

partial 함수

작성요망

뜯어서 이해해보기

언어의 핵심을 확장?

지연된 함수에 바인딩?

leejaeseung commented 1 year ago

partial 함수

function partial() {
    let fn = this, boundArgs = Array.prototype.slice.call(arguments);
    let placeholder = <<자리끼우개 객체>>;
    let bound = function() {
        let position = 0, length = boundArgs.length;
        let args = Array(length);
        for (let i = 0; i < length; i++) {
            args[i] = boundArgs[i] === placeholder ? arguments[position++] : boundArgs[i];
            // partial 이 호출될 때, partial 로 넘어온 arguments 수만큼 boundArgs 로 채워지고
            // bound 가 호출될 때, bound 로 넘어온 arguments 수만큼 나머지가 채워진다.
        }

        while (position < arguments.length) {
            args.push(arguments[position++]);
        }
        return fn.apply(this, args); // 위에서 채워진 args 로 함수가 호출된다.
    };
    return bound; // bound 는 아직 함수로 리턴되므로 아무 일도 일어나지 않는다.
}
leejaeseung commented 1 year ago

함수 바인딩이란? : 함수의 this 를 고정하는 것.

자바스크립트의 모든 함수는 bind 메서드를 가지고 있다.

someFunction.bind(context)
// someFunction 의 this 는 context 가 된다.
leejaeseung commented 1 year ago

p.148

함수 하나만 불순해도 전체 프로그램이 금세 불순해지기 십상이지요.

함수형 프로그래밍을 하기 위해선 최대한 모든 코드가 순수해야 한다. 물론 이는 현실적으로 불가능하긴 하다.

내부 코드의 불순함으로 인해 코드가 예측을 벗어났던 예시

(for {
            result <- process(event)
            endTime <- getCurrentTime
            _ <- logInfo(record, event, endTime - startTime)
          } yield result)
            .catchAll(logErrorInternal(record, Option(event), _, startTime))
            .catchAllDefect(logErrorInternal(record, Option(event), _, startTime))

위 코드를 모두 이해할 필요는 없고 catchAllcatchDefect 만 보면 된다.

catchAll 은 메인 비즈니스 로직에서 실패한 케이스를 잡는 구문이다. 비즈니스 로직을 모두 순수하게 짰기 때문에 모든 실패 케이스가 catchAll 로 와야 정상이었다. 하지만 서버가 종료되는 과정에서 내부 비즈니스 로직이 비정상적으로 종료되고, 정상 실패케이스가 아닌 비정상 실패케이스가 발생했다. (이는 catchAll 에서 잡지 못했다.)

결국 catchAllDefect 로 비정상 실패케이스를 잡아주어야 했다. 자바의 throw 를 try catch 로 잡는 것과 느낌으로 보면 될 것 같다.

ABizCho commented 1 year ago

좋은 예시 공유 감사합니다!

그렇다면 혹시, catchAllDefect로 넘어간 비정상 실패케이스의 원인, 즉, 해당 케이스의 불순했던 부분은 서버가 종료되는 과정에서 발생한 예측치 못한 동작이라고 생각하면 될까요?

leejaeseung commented 1 year ago

넵 위 코드는 ZIO 라는 프레임워크의 코드인데, ZIO 내부적으로 실패 라는 타입을 정의해서 실패가 반환되면 catchAll 에서 잡게 되도록 되어 있는데, 시스템 상의 예상치 못한 에러는 catchAllDefect 에서만 잡을 수 있었습니다. 사실 동작 간의 모든 에러를 잡는 건 불가능하니 catchAllDefect 를 따로 제공하는 거기도 하겠지요