본문으로 바로가기

함수형 자바스크립트 1

category software engineering/javascript 2023. 5. 20. 18:42
728x90

개요

이전에 함수형 프로그래밍에 대한 얕은 이해 만 갖고 있고 Closure, Currying, Compose 을 포함해서 함수형 패러다임에 개념에 대해 더 깊이 알고자 합니다. 또한 함수형 패러다임에서 Ramda.js 와 RxJS 를 어떻게 활용하는지 궁금증을 해결하고자 합니다.

사내 도서관에서 "FUNCTIONAL JAVASCRIPT 함수형 프로그래밍 모던 웹 개발에 충실한 실전 함수형 프로그래밍 안내서"라는 책을 찾았고, 이 책을 통해 함수형 프로그래밍에 대한 내용을 학습하고 정리하여 공유하고자 합니다

예전 채용공고에서 스쳐 지나봤던 Adriel 이라는 회사의 팀 문화

포스트 계획

포스트 계획:

  1. "FUNCTIONAL JAVASCRIPT 함수형 프로그래밍 모던 웹 개발에 충실한 실전 함수형 프로그래밍 안내서" 도서 내용을 학습하고 개념적인 내용을 정리하여 첫 번째 포스트를 작성할 예정입니다.
  2. 두 번째 포스트에서는 함수형 프로그래밍을 활용하여 간단한 앱을 만들어보고, 그 과정에서 느낀 점과 소감을 공유하고자 합니다.
  3. 실제로 사용해보고 첫 번째 포스트의 잘못 전달된 내용을 업데이트 할 예정입니다.

 

함수형으로 생각하기

 

함수형 프로그래밍이란

객체지향은 가동부를 캡슐화하여 코드의 이해를 돕는다.
함수평 프로그래밍은 가동부를 최소화하여 코드의 이해를 돕는다.
- Michael Feathers, Tweeter

책에서 이야기하는 함수형 프로그래밍을 작성함은 애플리케이션의 side effect 를 방지하고 mutation of state 를 감소하기 위해 데이터의 제어 흐름과 연산을 추상 하는 것 으로 이야기를 시작합니다.

어떤 문제는 객체지향으로 해결될 수 있지만, 자바스크립트는 상태 공유가 동적인 언어라서 조금만 시간이 지나도 복잡해지면서 가독성이 떨어지고 관리하기 어려운 코드가 되기 십상입니다. 최근 유행하기 시작한 리액티브 프로그래밍은 데이터의 흐름과 변경 전파에 초점을 두는것처럼 자바스크립트로 비동기 또는 Event Driven 코드를 다룰 때도 이러한 부분이 중요합니다.

이 중 한가지라도 "예" 또는 "잘모르겠다" 면 함수형 프로그래밍이 당신을 구원해줄 수 있는 패러디임이라고 말합니다:

  1. 확장성: 추가 기능을 지원하기 위해 계속 코드 리팩터링을 해야하는가?
  2. 모듈화 용이성: 파일 하나를 고치면 다른 파일도 영향을 받는가?
  3. 재사용성: 중복이 많은가?
  4. 테스트성: 함수를 단위 테스트하기 어려운가?
  5. 헤아리기 쉬움: 체계도 없고 따라가기 어려운 코드인가?
// 명령형
increment();
increment();
print(counter);

// 함수형
const plus2 = run(increment, increment);
print(plus2(0));

위의 예시를 살펴보면 명령형은 counter의 초기값에 의존하고 다른 함수에 의해 값이 변경 되었다면 어떤 값이 나올지 예측할 수 없는 반면 함수형은 항상 예측 가능하여 side effect가 없습니다. 이런 함수는 테스트하기 쉽고 전체 로직을 파악하기도 쉽습니다.

const total = arr => arr.reduce(sum);
const size = arr => arr.length;
const divide = (a, b) => a / b;
const average = arr => divide(total(arr), size(arr));

average(input); // -> result

모든 경우 위와 같은 방식으로 추론하진 않지만 함수형 프로그램으로 작성하는데는 이러한 사고방식이 내재되어 있습니다.

이러한 방식의 추론을 통해 ES6 map 과 같은 함수를 사용해봤다면 쾌적하고 강력한 함수형 패러다임을 느껴봤을텐데 이후 강력한 함수형 라이브러리인 Lodash.js 와 Ramda.js 를 이용하여 함수 체인을 조립하여 사용하는 방법을 소개해줍니다.

체인의 예시를 위해 '복수 과목을 수강한 학생들의 평균 점수를 계산하는 프로그램'을 명령형과 함수형으로 작성해본다면,

let enrollement = [
    { enrolled: 2, grade: 100 },
    { enrolled: 2, grade: 80 },
    { enrolled: 1, grade: 89 }
]
// 명령형
let totalGrades = 0;
let totalStudents = 0;

enrollment.forEach(student => {
    if(student.enrolled > 1) {
        totalGrades += student.grade;
        totalStudents++;
    }
})

const average = totalGrades / totalStudends; // -> 90

위 예제를 함수형으로 바라본다면,

  1. 수강 과목이 2개 이상인 자료 집합을 적절히 선택한다.
  2. 학생의 점수를 얻는다.
  3. 평균 점수를 계산한다.

이고 각 단계를 Lodash.js 로 묶으면 아래와 같은 함수 체인이 생성됩니다.

// 함수형
_.chain(enrollment)
 .filter(student => student.enrollment > 1)
 .pluck('grade')
 .average()
 .value(); // -> 90 // value()를 호출해야 체인에 연결된 모든 연산들이 실행됩니다.

이 코드를 보며 느끼는 강력한점은 함수를 쪼개어 체이닝 한다면 if-else 문의 분기가 필요 없어지고 루프를 반복할 필요가 없다는 점입니다.

복잡한 비동기 어플리케이션에서도 RxJS를 통한다면 신속하게 반응할 수 있습니다. 리액티브 프로그래밍은 함수형 프로그래밍에서 가장 흥미진진한 응용 분야인데, 많은 개발자들이 매일매일 씨름하는 비동기 코드, 이벤트 중심 코드의 복잡도를 현저하게 줄이는데 도움이 됩니다. 리액티브 패러다임의 가장 큰 장점은 더 높은 수준으로 코드를 추상화하여 이벤트 기반 프로그램(*마우스 클릭, 텍스트 변경, 포커스 변경, HTTP 요청, DB 쿼리, 파일 쓰기 등)을 무시하고 비지니스 로직에만 전념할 수 있게 하는 점입니다.

아래는 학생번호를 읽고 올바른지 검증하는 명령형 프로그램입니다.

let valid = false;
let elem = document.querySelector('#student-ssn');
elem.onkeyup = function(event) {
    let val = elem.value; // 외부 데이터에 접근
    if(val != null && val.length !==0) {
        val = val.replace(/^\s*|\s*$|\-s/g, ''); // 입력된 데이터를 정제
        if(val.length === 9) { // 중첩된 분기 (if 내 if)
        console.log('valid', val);
            valid = true; // 외부 데이터에 접근
        }
    } else {
        console.log('invalid', val);
    }
}

비지니스 로직이 한곳에 집중되어 있고 모듈성도 결여되어 있습니다. 무엇보다 외부 상태에 의존하는 탓에 재사용도 어렵습니다.

리액티브 패러다임은 옵저버블을 중심으로 움직입니다. 이를 통해 데이터 스트림을 구독하여 원하는 연산을 우아하게 합성 및 체이닝할 수 있습니다. 이번엔 학생 함수형으로 다시 작성해보겠습니다.

Rx.Observable.fromEvent(document.querySelector('#studnet-ssn'), 'keyup')
    .pluck('srcElement', 'value') // -> .map(el => el?.srcElement?.value)
    .map(ssn => ssn.replace(/^\s*|\s*$|\-/g, ''))
    .filter(ssn => ssn !== null && ssn.length === 9)
    .subscribe(validSsn => {
        console.log('valid', validSsn);
    })

모든 연산이 완전한 불변이고 비지느 로직은 모두 개별 함수로 나뉘었습니다. 함수형과 리액티브 프로그래밍을 섞어쓸 필요는 없지만 함수형으로 사고하다보면 결국 함수형 리액티브 프로그래밍이라는 아키텍처에 눈을 뜨게 될겁니다.

자바스크립트는 아직도 진화하고 꾸준히 개선중입니다. 이 책이 작성된 시점은 ES6가 주력인 시점으로 화살표 함수, 상수, 이터레이터, Promise 등 많은 함수형 프로그래밍에 걸맞는 기능들이 추가되었습니다.

렌즈(lense ) 또는 함수형 레퍼런스(functinoal reference)

  • 책에서는 lense 로 소개되었지만 lense.prop -> assoc 로 대체되었습니다. (2023.05기준)

OOP에서는 메서드를 호출해서 객체의 내부 내용을 바꾸는 일이 많습니다. 한가지 에를 들어 Person 클래스의 단순 세터 함수입니다.

set lastname(lastname) {
    return new Person(this._firstname, lastname, this._ssn); <- 다른 속성의 상태를 전부 새 인스턴스에 복사해야 합니다.
}

도메인 모델이 존재하는 경우 모든 속성에 이러한 작업을 해야한다고 가정하면 판박이 코드로 도배하지 않고 은밀하게 상태 객체를 바꿀 방법이 필요한데 이 때 렌즈 또는 함수형 레퍼런스라고 불리는 기법을 통해 상태적 자료형의 속성에 접근하여 불변화 하는 함수형 프로그래밍 기법을 사용할 수 있습니다. (내부적으로 copy-on-write 전략과 동일하게 동작합니다)

Ramda.js 라는 라이브러리를 사용해보겠습니다. Person의 lastname 속성은 R.assoc을 써서 다음과 같이 감싸면 됩니다.

const person = new Person('First', 'Last', '444-444-4444');
const lastnameLense = R.lenseProp('lastname');
const newPerson = R.set(lastnameLense, 'NewLast', person);

R.view(lastnameLense, person); // -> 'Last'
newPerson.lastname; // -> 'NewLast'
person.lastname // -> 'Last'

렌즈를 통해 구현한 세터는 불변이라 중첩 객체를 변경해서 새 객체를 반환할 수 있습니다. 함수형으로 작동하는 Getter/Setter 뿐만 아니라 필드에 접근하는 로직을 객체로부터 분리하여 this에 의존할 일을 없애고 어떤 객체라도 그 내용물에 접근하여 조작할 수 있는 강력한 함수를 제공하는 FP의 철학과 잘 어울립니다.

 

Higher-order Javascript

자바스크립트에서 모든 함수는 Function 타입을 갖기에 apply(), call() 와 함께 호출하여 context 가 있는 함수를 호출할 수 있습니다. 익명 함수를 사용해서 extend 하거나 arguements 를 전달할 수 있습니다. 예를 들어, native 함수인 sort()를 살펴보겠습니다.

var fruit = ['Coconut', 'apples'];
fruit.sort(); //->['Coconut', 'apples']

var ages = [1, 10, 21, 2];
ages.sort(); //->[1, 10, 2, 21]

과일의 이름을 알파벳 순서대로 정렬하고 싶은 경우 제공된 native 함수를 사용하면 유니코드의 경우 대문자가 소문자보다 먼저 오기에 원하는 정렬을 할 수 없고, 나이 순서대로 정렬하고 싶은 경우에도 integer 만 존재하더라도 유니코드로 변환되어 비교되기에 비교 함수를 구현해줘야합니다. 이런 경우에 people.sort((p1, p2) => p1.getAge() - p2.getAge()); 와 같은 방법으로 구현할 수 있습니다.

 

아래는 US에 사는 사람들을 로그로 프린트하는 명령형 함수입니다.

function printPeopleInTheUs(people) {
    for (let i = 0; i < people.length; i++) {
    	var thisPerson = people[i];
        if(thisPerson.address.country === 'US') {
            console.log(thisPerson);
        }
    }
}
printPeopleInTheUs([p1, p2, p3]);

이를 일반적인 함수형 프로그래밍으로 사고하여 표현한다면 아래와 같이 작성할 수 있습니다.

function printPeople(people, selector, printer) {
    people.forEach(function (person) {
    	if(selector(person)) {
        	printer(person);
        }
    });
}

var inUs = person => person.address.country === 'US';
printPeople(people, inUs, console.log);

그리고 이걸 Ramda.js lense 를 이용하여 함수형 답고 국가명 또한 유동적으로 변경할 수 있는 코드를 작성할 수도 있습니다.

var countryPath = ['address', 'country'];
var countryL = R.lens(R.path(countryPath), R.assocPath(countryPath));
var inCountry = R.curry((country, person) =>
R.equals(R.view(countryL, person), country));

people.filter(inCountry('US')).map(console.log);

 

함수형으로 작성하다보면 객체지향의 #private 과 같이 변수에 접근하여 변경이 불가능하도록 은닉이 필요한 경우에 클로저를 사용할 수 있습니다. 클로저를 사용하면 전역 참조 변수를 줄이는데 도움을 주고 IIFE를 통해 외부에 필요한 함수만을 노출할 수 있습니다.

var MyModule = (function MyModule(export) { // Stack Trace 를 위해 MyModule 임을 다시 명시
    let _myPrivateVar = ...; // 외부에서 접근 불가한 변수
    export.method1 = function () { // 외부에서 호출할 함수
    	// do work
    };
    export.method2 = function () { // 외부에서 호출할 함수2
    	// do work
    };
}(MyModule || {})); // 이제 MyModule.method1(), MyModule.method2()를 사용

 

함수형으로 전환하기

어플리케이션의 제어 흐름을 분석하는데 명령형 함수는 분기와 루프에 따라 움직이게 구성되어 있는 반면 함수형 프로그램은 한 연산에서 다른 연산으로 독립적으로 흘러가며 서로 연결된 블랙박스 연산을 제어합니다.

책에서는 이후 map 을 직접 구현해주고 함수형으로 표현하여 사용하면 얼마나 편리한지 소개해줍니다. 그리고 lodash 에 내장된 많은 유틸성 함수들과 이들을 chaining 하여 위의 차트와 같이 서로 연결된 함수를 나타냅니다.

_.chain(persons)
.filter(isValid)
.map(_.property('address.country')) // -> Ramda.js의 R.view()와 비슷하지만 less feature-rich
.reduce(gatherStats, {})
.values()
.sortBy('count')
.reverse()
.first()
.value() // -> 실제로 여기서 실행
.name; //-> 'US'

체인함수는 변수의 생성을 줄이고, 루프를 효과적으로 제거할 수 있다는 장점이 있지만 또 다른 강력한 기능 중 하나는 lazily 동작한다는 점입니다. 

일반적으로 파이프라인의 순서대로 동작하는 방법
value() 를 이용하여 lazily 동작하게 하는 방법

이러한 전략을 통해 99,999 개의 요소를 탐색 하게 작동되는 코드를 예를 들어 1,000개만 검사하여 종료시킬 수 있어 경우에 따라 벤치마크 상 엄청난 성능향상 또한 챙길 수 있다고 한다.

Lodash 는 mixin 이라는 기능을 제공하는데 아래와 같이 extends 하여 SQL과 같은 존재하지 않는 함수형 또한 구현할 수 있습니다.

_.mixin({'select': _.pluck,
         'from': _.chain,
         'where': _.filter,
         'groupBy': _.sortByOrder}); // mixin 정의

_.from(persons)
	.where(p => p.birthYear > 1900 && p.address.country !== 'US')
	.groupBy(['firstname', 'birthYear'])
	.select('firstname', 'birthYear')
	.value();

 

Currying 과 Composing

단순히 TL;DR 로 작성했던 currying 에서는 알지 못했던 점도 책에서는 소개해줬는데, f(a,b,c) 라는 함수를 실행했다고 가정해보자. 만약 이 함수를 a 인자만 존재하고 eagerly evalute 가 된 경우 missing parameter 를 undefined 로 채워 평가하여 f(a, undefined, undefined) 로 실행된다. 하지만 curried function 은 f(a) 가 평가되더라도 나머지 인자를 기다려 f(b,c) 를 기다리고 순차적으로 모든 인자가 채워질 때까지 단계적으로 평가를 하고 f(a,b,c) 모두 채워진 결과물을 return 한다.

이게 가능한 이유가 사실 이전 포스팅에서 소개했던 currying 에서 직접 구현한 내용을 보면 function(a)(b)(c) 고 이걸 단순히 lodash.js, ramda.js 와 같은 라이브러리에서 위와 같은 방식으로 제공한다는 점이었다.

Compose는 단순한 task 로 나눠진 순수 함수를 의미가 있는 하나의 함수로 합치는데 const cleanInput = R.compose(normalize, trim); 와 같이 합치는걸 라이브러리에서 제공한다는 내용이었다. 또 다른 예시는 아래와 같다.

const smartestStudent = R.compose(
R.head,
R.pluck(0),
R.reverse,
R.sortBy(R.prop(1)),
R.zip);

 

Point-free 라는 개념도 등장했는데, functional Javascript 코드를 좀 더 Haskell 또는 Unix 철학과 비슷하게 만들어주는 개념이었습니다. Ramda.js 에서 제공하는 compose 혹은 pipe 를 사용하게 되면, 함수 평가 단계에서 low-level 의 걱정을 줄여줄 수 있습니다.

`R.compose(first, getName, reverse, sortByGrade, combine);` 와 같은 각 파라미터를 제공해야하는 일반적인 선언형 함수가 아닌 아래 코드와 같이 다른 파라미터 없이 단순히 name 만 제공하는 방식의 코드 스타일로 표현할 수 있습니다.

const runProgram = R.pipe(
R.map(R.toLower),
R.uniq,
R.sortBy(R.identity));

runProgram(['Functional', 'Programming', 'Curry',
 'Memoization', 'Partial', 'Curry', 'Programming']);

 

물론 모든 코드를 이렇게 사용한다면 모호하고 읽기 어려운 함수들이 많아지기에 경우에 따라 2~3 개의 함수로 나눠도 좋습니다. 이쯤에서 그럼 Ramda.js 와 Lodash.js 는 뭐가 다른지 찾아봤더니 Stackoverflow 의 답변 중 이러한 답변이 있었습니다.

Lodash.js - Lodash 는  유연성과 성능에 중점을 둡니다.일관성, 호환성, 사용자 정의 및 성능에 중점을 두고 가능한 한 많은 개발자에게 고품질 유틸리티 방법을 제공합니다. 어떤 범용 라이브러리도 사용자가 작성한 코드만큼 빠를 수는 없지만 lodash는 최대한 비슷합니다.

Ramda.js - Ramda는 성능보다는 단순하고 깔끔한 API 설계에 더 많은 관심을 기울입니다. Ramda의 아이디어는 함수가 한 가지 작업만 수행해야 하고 하나의 명확한 인터페이스를 가져야 한다는 것입니다. Ramda의 초점은 함수를 간단하게 구성하고 모든 데이터가 변경 불가능한 것처럼 작동하며 부작용을 피하는 데 있습니다.

그리고 주요 차이점으로, Lodash 함수는 대부분 데이터를 먼저 가져오고, 데이터에 대해 작동하는 작업이 그 뒤를 따르고, 때로는 동작을 변경하는 선택적 인수가 뒤따릅니다. Ramda는 변경 가능성이 가장 낮은 인수를 먼저 배치하고 변경 가능성이 가장 높은 인수를 마지막에 배치합니다. 이것은 데이터 변환 함수에서 데이터가 마지막이라는 것을 의미합니다. 그리고 Ramda는 선택적 인수를 모두 피합니다.

Ramda는 모든 함수와 반환되는 거의 모든 함수를 커링합니다. Lodash에는 curry함수가 있지만 명시적으로 호출해야 합니다. 이것은 함수 구성에 대한 Ramda의 아이디어에서 상당히 중심적입니다.

아래 코드를 보면 Lodash 는 reference-equality에 초점을 두는 반면 Ramda는 value-equality에 초점을 둡니다.

// lodash
_.union (
  [{x: 1}, {x: 2}, {x: 3}, {x: 4}, {x: 5}], 
  [{x: 2}, {x: 3}, {x: 5}, {x: 7}, {x: 11}]
)
//=> [{x: 1}, {x: 2}, {x: 3}, {x: 4}, {x: 5}, {x: 2}, {x: 3}, {x: 5}, {x: 7}, {x: 11}]
_.intersection (
  [{x: 1}, {x: 2}, {x: 3}, {x: 4}, {x: 5}], 
  [{x: 2}, {x: 3}, {x: 5}, {x: 7}, {x: 11}]
) //=> []

// Ramda
R.union (
  [{x: 1}, {x: 2}, {x: 3}, {x: 4}, {x: 5}], 
  [{x: 2}, {x: 3}, {x: 5}, {x: 7}, {x: 11}]
)
//=> [{x: 1}, {x: 2}, {x: 3}, {x: 4}, {x: 5}, {x: 7}, {x: 11}]
R.intersection (
  [{x: 1}, {x: 2}, {x: 3}, {x: 4}, {x: 5}], 
  [{x: 2}, {x: 3}, {x: 5}, {x: 7}, {x: 11}]
) //=> [x: 2}, {x: 3}, {x: 5}]

Ramda의 디자인은 기능적 시스템과 더 밀접하게 일치하지만 동등성 검사가 포함될 때마다 큰 성능 비용이 발생합니다. Lodash는 그러한 작업에 대해 Ramda보다 아마도 두 배 더 빠를 것입니다.

Ramda는 주로 짧거나 긴 파이프라인에서 구성을 통해 기능을 구축하도록 설계되었습니다. Lodash는 주로 명령형 코드와 함께 작동하도록 설계되었습니다. 작업에 라이브러리를 사용할 수 있지만 일반적인 Ramda 코드와 동등한 Lodash 코드는 다음과 같습니다.

// ramda
const myFn = R.pipe (
  R.fn1,
  R.fn2 ('arg1', 'arg2'),
  R.fn3 ('arg3'),
  R.fn4
)

// lodash
const myFn = (x, y) => {
  const var1 = _.fn1 (x, y)
  const var2 = _.fn2 (var1, 'arg1', 'arg2')
  const var3 = _.fn3 (var2, 'arg3')
  return _.fn4 (var3)
}

성능에 대한 Lodash의 초점은 작업에 대해 잘 최적화된 많은 기능을 제공합니다. isArray, isArguments, isArrayBuffer, isArrayLike, isArrayLikeObject 뿐만 아니라 20가지가 넘는 타입 체크 함수를 제공하지만 Ramda는 is, isEmpty, isNil 만 제공하되 const isArray = is (Array) 와 같이 재사용 가능하도록 만들었습니다.

현재 lodash는 typescript와 rollup으로 다시 작성되었습니다. Ramda의 철학인 순수 함수성과 불변성을 강조하고 auto curried, iteratee-first and data-last을 wrapping 하여 제공하여 포인트 프리 방식으로 사용할 수 있는 lodash-fp도 제공합니다.

 

함수형의 에러 핸들링

함수형 프로그래밍에서는 try-catch 나 null check 와 같은 에러 처리를 하지 않습니다. 에러 핸들링을 처리하는 과정에 앞서 functor 와 monad 디자인 패턴이 등장하는데 이 과정에선 아주 단순하게 이해하고 넘어가기로 하고 정리하자면 (이 분이 보기좋게 잘 정리해주신거 같으나 나중에 보겠습니다.), functor 는 '값을 들고 있는 구조와 구조를 유지한 채 그 값에 함수를 집어넣을 수 있는 인터페이스 조합' 으로 우리가 자주 사용하던 map 과 filter 도 타입을 유지해주는 functor 에 해당합니다.

typescript 가 익숙하신 분이라면, 이읗님의 정의인 '함수를 갖다주면 제너릭 타입으로 돌려주는 변환'이 정말 이해하기 쉬웠습니다. (T, U) -> (F<T>, F<U>) 로 이야기하면 T와 U타입의 임의의 타입을 기준으로 F<T>,F<U> 함수로 돌려주는 변환하는 것으로요. 대신 Functor 는 항등성이 보존되어야 하고, 합성되더라도 항등성이 보존되어야 하는 조건이 있습니다. 타입에는 Optional이 떠올리기 쉬운 예인데 t->f(t), nothing->nothing이 보존되기 때문입니다. ES6 map도 좋은 예시입니다.

map :: (A -> B) -> Array(A) -> Array(B)
filter :: (A -> Boolean) -> Array(A) -> Array(A)

책에서는 다음과 같은 예시가 있었습니다.

const plus = R.curry((a,b)=> a+b);
const plus3 = plus(3);

const two = wrap(2); // wrapper functor
const five = two.fmap(plus3); // -> Wrapper(5)
five.map(R.identity); // -> 5

R.identity(A function that does nothing but return the parameter supplied to it. Good as a default or placeholder function. 아무 것도 하지 않고 제공된 매개변수를 반환하는 함수입니다. 기본 또는 자리 표시자 기능으로 적합합니다.) 를 사용해서 side-effect 가 없고 wrapping 된 값이 보존됨을 인증해줍니다.

하지만 단순히 1차원 functor가 아닌 (F<T>,F<U>)->F<V> 과 같은 다차원 (예시는 2차원) 를 원하는 경우

  1. (T,U) -> V
  2. ((T,U)->V) -> ((F<T>,U)->F<V>)
  3. ((T,U)->V) -> ((F<T>,F<U>)->F<V))

로 변환하여 functor 를 구할 수 있습니다. 이 때 n개의 값이 주어지면 f(t1)(t2)(t3)...(t{n})으로 나타낼 수 있습니다.

커링을 사용하는 이유:
인수를 통해 한 번에 다 보내야하는 다인수 함수보다 커링이 유연합니다. 처음 arguments로 사용할 값이 정해져있고 변하지 않는다면 그걸 값으로 저장하여 memoziation 할 수 있는 장점도 있습니다.

Functor에서 다인수 함수를 변환하는 함수를 정의할 수 있지만, 반환 타입에 Generic이 여러개 걸리는 단점이 있습니다. 일부 사용하는 경우도 있지만 Optional과 같은 경우 ((T,U)->V) -> ((Optional<T>,Optional<U>)->Optional<Optional<V>>) 반환 타입에 한개만 존재해도 되는데 이런 경우가 많습니다. 이를 Functor와 Monad 사이에 Applicative Functor 라는 단계가 존재하는데 이 단계에서 M<M<V>>를 flat 하여 M<V>로 나타낼 수 있습니다. Monad는 Functor에 불편한 점을 좀 더 편하게 사용할 수 있게 unit과 flat을 추가한게 전부입니다 (A Functor M is also Monad if unit: T->M<T>, flat: M<M<T>> -> M<T>).

 

아래는 일반적인 함수에서 인자의 null 타입 검사를 하는 예시이다.

function getCountry(student) {
    let school = student.school();
    if(school !== null) {
    	let addr = school.address();
    	if(addr !== null) {
 			return addr.country();
 		}
 	}
	return 'Country does not exist!';
}

함수형에서는 어떻게 할까? 다음과 같이 우아하게 표현하여 넘어가는 방식을 취한다. 모나드를 이용하여 Maybe, Either, Just, Nothing 함수를 unit과 flat을 통해 타입 오류를 정하는 방법을 택한다.

const getCountry = (student) => student
 .map(R.prop('school'))
 .map(R.prop('address'))
 .map(R.prop('country'))
 .getOrElse('Country does not exist!');
 
 const safeFindStudent = R.curry((db, id) => Maybe.fromNullable(find(db, id)));
 
 const country = R.compose(
	getCountry,
	safeFindStudent
);

ramda-fantasy, lodash-fantasy와 같은 라이브러리에서도 이를 제공하는것 같다. 전체적인 함수형 프로그래밍의 내용은 다음과 같았고 책에서 이해가 되지 않거나 부족한 부분은 유튜브와 블로그를 통해 보니 이해하는데 도움이 되었습니다. 사실 처음 책을 펼쳤을 때는 Ramda를 사용한 코드 부분이 오히려 더 이해하기 불편해보였지만 현재는 깔끔하고 직관적으로 보이긴 합니다.

실제로 사용하면서 부족한 개념을 조금 채워넣고 파트2에서 소스코드와 결과물을 공개해보겠습니다.

 

참조

https://edykim.com/ko/post/introduction-to-lodashs-delay-evaluation-by-filip-zawada/

 

Lodash의 지연 평가 소개 by Filip Zawada

이상한모임 슬랙 #dev-frontend 채널에서 Lodash에 대해 이야기하다 지연 평가(Lazy Evaluation)를 지원한다는 이야기를 듣고 검색하게 되었다. 검색 결과로 찾은, Filip Zawada의 How to Speed Up Lo-Dash ×100? Introdu

edykim.com

https://www.coupang.com/vp/products/64822034?itemId=219258434&vendorItemId=3530119833&src=1042503&spec=10304982&addtag=400&ctag=64822034&lptag=10304982I219258434&itime=20230528144622&pageType=PRODUCT&pageValue=64822034&wPcid=16654091937862660710468&wRef=&wTime=20230528144622&redirect=landing&gclid=CjwKCAjw1MajBhAcEiwAagW9MXum4FTF_nmFasRlWjjL_i0jNlQaQ7vX8fFzOFqXvM7h2YgAd7M7ZhoCWVEQAvD_BwE&mcid=972bc74c914d45978ab133de435c03d7&campaignid=18626086777&adgroupid=&isAddedCart= 

 

함수형 자바스크립트:모던 웹 개발에 충실한 실전 함수형 프로그래밍 안내서

COUPANG

www.coupang.com

https://stackoverflow.com/questions/71401443/differences-between-lodash-and-ramda

 

Differences between Lodash and Ramda

So I've been researching a little bit about JS frameworks. From what I saw, two of the most popular and modern libraries are Lodash and Ramda (right?). I saw a similar question regarding Lodash and

stackoverflow.com

https://overthecode.io/i-am-js-developer-and-still-dont-know-monad/

 

JS개발자는 아직도 모나드를 모르겠어요

왜 수많은 모나드 튜토리얼을 읽고도 아직 이해가 안가는 것일까

overthecode.io

https://saengmotmi.netlify.app/javascript-study/2022-05-13-monad/

 

2022-05-13 함수형 프로그래밍 & 모나드

[주의] 쫄지마시오!

saengmotmi.netlify.app

https://www.youtube.com/watch?v=rHAKak5vXfY&ab_channel=%EC%9D%B4%EC%9D%97