Composing Software 시리즈 요약
소프트웨어 합성: 개요
합성은 전체를 구성하기 위해 부분이나 요소를 결합하는 행위다.
소프트웨어 개발이란 복잡한 문제를 작은 문제로 분해해 그 해법들을 조합해서 복잡한 문제를 해결하는 솔루션을 만드는 행위라고 배웠다.
그 말의 중요성을 늦게 깨달았다. 소프트웨어 설계의 본질이다.
소수의 개발자만 깨닫고 있었다. 대부분 우리가 활용할 가장 중요한 개념과 잘 적용하는 방법을 몰랐다.
•
함수 합성(function composition)이란?
•
객체 합성(object composition)이란?
// 뭔지 잘 몰랐음. 특히 객체 합성(함수 합성은 콜백 함수 던지는 방식이 아닐까, 객체 합성은 상속?)
우리는 의식하지 않고 나쁘게 합성을 한다. 그래서 버그와 이해하기 어려운 코드가 나온다. 유지보수 시간과 수십억명의 사람들에게 악영향을 준다. 자동차 사고는 인명을 잃게 한다. 해커는 버그를 통해 사람들을 감시하고 공격한다.
당신은 매일 소프트웨어를 합성한다.
모든 소프트웨어 개발자는 매일 함수와 데이터 구조를 합성한다. // 객체라고 한 게 여기서 데이터 구조
함수 합성
함수 합성이란 한 함수의 출력에 다른 함수를 결합시키는 과정이다.
대수학에서 f와 g 두 함수, 그리고 합성함수 (f ∘ g)(x) = f(g(x))가 있다.
원 기호는 합성 연산자로 “composed with”나 “after”로 발음된다.
g가 먼저 계산되고 그 결과를 f의 인수로 전달한다.
const g = n => n + 1;
const f = n => n * 2;
const doStuff = x => {
const afterG = g (x);
const afterF = f (afterG);
return afterF;
};
doStuff (20); // 42
TypeScript
복사
ES6의 Promise chain도 합성 함수다.
const g = n => n + 1;
const f = n => n * 2;
const wait = time => new Promise((resolve, reject) =>
setTimeout(
resolve,
time
)
);
wait(300) // 300 <- time
.then(() => 20) // () => 20 <- resolve
.then(g) // 20은 g의 인수로 전달된다.
.then(f) // 21은 f의 인수로 전달된다.
.then(value => console.log(value)) // 42
TypeScript
복사
배열 메서드 호출, lodash 메서드, observables(RxJs,,,)를 체인할 때 함수를 합성하게 된다.
반환값을 다른 함수에 전달하는 것도 합성이다.
this를 입력으로 두 개의 메서드를 순차적으로 호출하는 것도 합성이다.
함수를 합성해 doStuff()를 한줄로 고치는 코드다.
const g = n => n + 1;
const f = n => n * 2;
const doStuffBetter = x => f(g(x));
doStuffBetter (20); // 42
TypeScript
복사
이 방식은 디버깅이 어렵다는 얘기를 듣는다. 디버깅 로직을 담아본다.
const doStuff = x => {
const afterG = g(x);
console.log(`after g: ${ afterG }`);
const afterF = f(afterG);
console.log(`after f: ${ afterF }`);
return afterF;
}; // 변수가 늘어나서 별로 좋아보이진 않는다.
doStuff(20); // =>
/*
"after g: 21"
"after f: 42"
*/
TypeScript
복사
afterF와 afterG를 추상화하는 trace()라는 logger function을 만든다.
const trace = label => value => {
console.log(`${ label }: ${ value }`);
return value;
}; // 공통 로직을 뺐지만 코드 라인 개수이나 변수가 줄어들지는 않았다.
TypeScript
복사
const doStuff = x => {
const afterG = g(x);
trace('after g')(afterG);
const afterF = f(afterG);
trace('after f')(afterF);
return afterF;
};
doStuff(20); // =>
/*
"after g: 21"
"after f: 42"
*/
TypeScript
복사
Lodash 및 Ramda와 같은 인기있는 함수 프로그래밍 라이브러리에는 함수를 쉽게 구성할 수있는 유틸리티가 포함되어 있다.
import pipe from 'lodash/fp/flow';
const doStuffBetter = pipe(
g,
trace('after g'),
f,
trace('after f')
);
doStuffBetter(20); // =>
/*
"after g: 21"
"after f: 42"
*/
TypeScript
복사
pipe는 아래와 같다.
// pipe(...fns: [...Function]) => x => y
const pipe = (...fns) => x => fns.reduce((y, f) => f(y), x);
TypeScript
복사
아직은 몰라도 된다. 매우 중요한 코드이기 때문에 반복해서 정의되고 시연한다. 중요한 건 자동적으로 떠올릴 만큼 익숙해지는 것이다. 합성과 한몸이 되어야 한다.
pipe()는 한 함수의 출력을 다른 함수의 입력으로 전달하여 함수 파이프 라인을 생성한다.
pipe()(compose())를 사용하면 intermediary variables가 필요 없다. 이 방식을 point-free style 혹은 무인수 방식이라 한다.
함수를 명시적 선언하지 않고 다른 함수를 반환하는 함수를 호출한다. function이나 ⇒이 필요하지 않다.
무인수 방식은 조금씩 사용하는 것도 좋다. 중간 변수들이 불필요한 복잡성을 높이기 때문이다.
복잡성 감소에 이점이 몇 가지 있다.
단기 기억
인간 두뇌의 단기 기억력은 서로 다른 항목을 저장하기 위한 공간이 한정되어 있어 변수가 늘어날수록 각 변수의 의미를 기억하기 힘들어진다.
우리 뇌는 일반적으로 4-7개 항목을 단기 기억 공간에 저장할 수 있어 이보다 크면 오류율이 급증한다.
우리는 함수 파이프라이닝으로 변수를 줄였다. 이는 인지 부하를 줄여준다.
신호 대 잡음비
간결함은 이것을 향상시킨다. 주파수를 정확하게 튜닝하면 소음이 사라지고 음악 신호가 강해진다.
코드도 간결한 표현이 이해력을 향상시킨다. 전달해야하는 의미를 변화시키지 않는 선에서 코드량을 줄이면 더 쉽게 이해할 수 있다.(즉 더 적은 코드로 같은 의미를 전달하는 것이 좋다)
코드의 면적과 버그
함수형은 마치 코드가 다이어트한 것처럼 보인다. 이는 코드가 버그를 숨길 수 있는 표면적을 의미한다. 즉, 더 많은 버그를 줄일 수 있다.
객체 합성
“클래스 상속보다는 객체 합성을 우선해라" - GoF
컴퓨터 과학에서 복합 자료형은 프로그램에서 조합할 수 있는 모든 자료형이다. 복합 자료형을 만드는 행위는 composition으로 알려져 있다. - Wikipedia
다음은 원시형입니다.
const firstName = 'Claude';
const lastName = 'Debussy';
TypeScript
복사
그리고 이것은 복합체입니다.
const fullName = {
firstName,
lastName
};
TypeScript
복사
마찬가지로 모든 array, set, map, weak map, typed array 등은 복합 자료형입니다. 비 원시형 구조를 작성할 때마다 우리는 객체를 합성합니다.
Gang of Four의 composite pattern은 객체들의 관계를 재귀적으로 구성하여 부분-전체 계층을 표현하는 패턴으로, 사용자가 단일 객체와 복합 객체 모두 동일하게 다루도록 정의합니다.
일부 개발자는 컴포지트 패턴이 객체 합성의 유일한 형태 라고 생각하면서 혼란스러워 합니다. 혼동하지 마십시오. 객체 합성에는 여러 가지 종류가 있습니다.
Gang of Four는 계속해서 "객체합성은 여러 디자인 패턴에 다양하게 적용될 것 입니다"라고 말한 다음
객체가 합성될때 두 객체의 관계를 정의하기 위한 세가지 표현을 정의했습니다.
1.
delegation 위임 (state, strategy 및 visitor 패턴에서 사용 됨)
2.
acquaintance 인지 (객체가 참조로 다른 객체를 알고 있을 때 일반적으로 매개 변수로 전달됨
: 네트워크 요청 처리기가 로거에 대한 참조를 전달하여 요청을 기록 - 요청 처리기가 로거를 사용함)
3.
aggregation 집합 (자식 객체가 부모 객체의 일부를 형성하는 경우
: has - a 관계, 예를 들어 자식 DOM은 부모 노드의 구성 요소).
클래스 상속은 복합 객체를 구성하는 데 사용할 수 있지만 제한적이고 취약한 방법입니다. Gang of Four는 "클래스 상속보다는 객체 합성을 우선해라"라고 말하면서 객체를 합성하기 위해 클래스 상속이란 강고하고 단단히 결합 된 접근 방식보다는 유연한 접근 방식을 사용하도록 조언합니다.
// GoF는 클래스 상속보단 객체 합성을 우선하라고 했는데, Refactoring 저자는 클래스를 많이 사용했다.
클래스 상속은 복합 객체 생성의 한 종류에 불과합니다. 모든 클래스는 복합 객체를 생성하지만 모든 복합 객체가 클래스 또는 클래스 상속에 의해 생성되는 것은 아닙니다. "클래스 상속보다는 객체 합성을 우선해라"라는 말은 클래스 계층의 조상 (ancestor)에서 모든 속성을 상속하지 않고 작은 구성 요소 부분에서 복합 객체를 형성해야한다는 것을 의미합니다. 전자는 객체 지향 설계에서 잘 알려진 다양한 문제를 일으킵니다.
•
The tight coupling problem 단단한 결합 문제 : 자식 클래스는 부모 클래스의 구현에 의존하기 때문에 클래스 상속은 객체 지향 디자인에서 사용할 수있는 가장 조밀한 결합입니다.
•
The fragile base class problem 깨지기 쉬운 기초 클래스 문제 : 긴밀한 결합으로 인해 기초 클래스가 변경되면 잠재적으로 제 3자가 관리하는 코드에서 많은 수의 클래스가 손상 될 수 있습니다. 작성자는 알지 못하는 코드를 깨뜨릴 수 있습니다.
•
The inflexible hierarchy problem 경직된 계층 구조 문제 : 단일 조상으로 시작해 충분한 시간과 진화가 이루어진 후에는 사실상 새로운 유스 케이스에 대해 잘못된 클래스 이름을 가지게 될 것입니다.
•
The duplication by necessity problem 중복 필요성 문제 : 경직된 계층 구조로 인해 새로운 유스 케이스가 종종 확장이 아닌 복제에 의해 구현되고 이로 인해 불필요한 유사한 클래스들이 나타나게 됩니다. 유사한 클래스들이 존재하면 상속의 기준을 무엇으로 잡을지 불투명해 집니다.
•
The gorilla/banana problem 고릴라 / 바나나 문제 : "… 객체 지향 언어의 문제점은 객체가 모든 암묵적인 환경을 함께 가질 수 있다는 것입니다. 당신은 바나나를 원했지만 바나나와 정글 전체를 들고있는 고릴라가있었습니다. "~ Joe Armstrong, "Coders at Work "
JavaScript에서 객체 합성의 가장 일반적인 형태는 객체 연결 (mixin composition)이라고합니다.
클래스 상속을 사용하여 복합 객체 만들기 :
class Foo {
constructor () {
this.a = 'a'
}
}
class Bar extends Foo {
constructor (options) {
super(options);
this.b = 'b'
}
}
const myBar = new Bar(); // {a: 'a', b: 'b'}
TypeScript
복사
믹스 인 성분으로 복합 객체 만들기 :
const a = {
a: 'a'
};
const b = {
b: 'b'
};
const c = {...a, ...b}; // {a: 'a', b: 'b'}
TypeScript
복사
앞으로 객체를 합성하는 다양한 방법에 대해 알아볼 것입니다. 지금까지의 논의를 정리하자면 다음과 같습니다.
1.
어떤 것을 수행하는 데는 한 가지 이상의 방법이 있습니다.
2.
어떤 방법은 다른 방법보다 낫습니다.
3.
당장의 작업을 위해 가장 단순하고 유연한 솔루션을 선택하려고 합니다.
결론
이 글은 FP (Functional Programming)와 OOP (Object-Oriented Programming) 또는 프로그래밍 언어에 대한 글이 아니다.
소프트웨어를 합성하기 위한 컴포넌트는 함수, 자료 구조, 클래스 등의 형태를 취할 수 있다.
프로그래밍 언어마다 컴포넌트에 대해 서로 다른 접근방식을 취한다.
자바는 클래스를 제공하고, 하스켈은 함수를 제공합니다. 그러나 어떤 언어, 어떤 패러다임을 선호하든 관계없이 함수와 자료구조를 합성할 수 밖에 없다. 결국, 모두 뒤죽박죽이 될지라도 말이지요.
우리는 지금까지 대부분의 논의를 함수형 프로그래밍에 관하여 했습니다. 그 이유는 함수야말로 JavaScript에서 가장 합성을 하기 쉬운 요소이고, 함수 프로그래밍 커뮤니티가 함수 합성 테크닉을 공식화하는 데 많은 시간과 노력을 투자했기 때문이다.
이 글은 함수형 프로그래밍이 객체 지향 프로그래밍보다 낫다는 논의를 하려는게 아니다. 만약 그렇다면 두 패러다임중 하나를 선택하라 할테지요. OOP 대 FP는 잘못된 이분법이다. 최근 몇 년 동안 본 모든 Javascript 애플리케이션은 FP와 OOP를 광범위하게 혼합한다.
객체를 합성하여 FP에서 사용될 자료구조를 만들고 함수형 프로그래밍으로 OOP에서 객체를 만들어볼 것이다.
소프트웨어를 작성하는 방법에 상관없이 훌륭한 합성을 해야한다
소프트웨어 개발의 핵심은 합성입니다.
이제는 단순하게 생각할 때입니다. 어떤 것을 단순화하는 가장 좋은 방법은 본질에 도달하는 것입니다.
문제는, 소프트웨어 산업에 있는 대부분의 사람들이 본질에 대해 무관심하다는 것입니다.
우리 업계는 소프트웨어 개발자인 당신을 제대로 가르치지 못했습니다.
업계는 개발자를 더 잘 훈련시켜야할 책임이 있습니다. 우리는 이를 고쳐야 합니다. 우리가 책임을 져야합니다.
경제에서 의료 장비에 이르기까지 모든 것에서 소프트웨어가 실행됩니다.
이 행성의 모든 인간의 삶은 소프트웨어 품질에 영향을받고 있습니다. 우리가 뭘 하고있는지 알아야합니다.
지금부터 소프트웨어 합성 방법을 배우면 됩니다.
함수형 프로그래밍의 역사
6살부터 Basic으로 쓴 게임에 대한 책을 보면서 대수학을 배우기 전에 주제를 다 익혔다.
합성 가능한 소프트웨어의 부상
컴퓨터 과학의 초기, 대부분의 연구가 실제 컴퓨터에서 이루어지기 전 Alonzo Church와 Alan Turing이라는 두 명의 위대한 컴퓨터 과학자가 있었습니다.
그들은 서로 다른 동시에 동등한 두가지 보편적인universal 계산 모델을 만들었는데, 두 모델 모두 계산가능한 모든 것을 계산할 수 있었습니다.
Alonzo Church는 람다 대수를 lambda calculus 만들었습니다. 람다 대수는 함수 합성을 기반으로 하는 계산의 보편적인 모델입니다.
Alan Turing은 튜링 머신로 유명합니다. 튜링 머신은 이론상으로 존재하는 기계이며 테이프의 기호을 조작하는 보편적인 계산 모델입니다.
그들은 함께 람다 대수와 튜링 머신이 결국 같은 것임을 보여주기 위해 협력했습니다.
람다 대수는 함수 합성에 관한 것입니다. 함수 합성의 관점에서 생각하는 것은 소프트웨어를 작성하는데 있어 매우 직관적이고 표현적인 접근방식입니다. 이번 편에서는 소프트웨어 설계에서 함수 합성이 얼마나 중요한지 논의할 것입니다.
람다 대수를 특별하게 하는 세 가지 중요한 점이 있습니다 :
1.
함수는 항상 익명입니다. JavaScript에서 const sum = (x, y) => x + y 의 오른쪽은 익명의 함수 표현식 (x, y) => x + y 입니다.
2.
람다 대수의 함수는 단일 입력만 허용합니다. 그들은 단항입니다. 하나 이상의 매개 변수가 필요한 경우 함수는 하나의 입력을 받아 다음 함수를 사용하는 등의 새로운 함수를 반환합니다. n 항 함수 (x, y) => x + y 는 x => y => x + y 와 같은 단항 함수로 표현 될 수 있습니다. 다항 n-ary 함수에서 단항 함수로의 변환은 currying으로 알려져 있습니다.
3.
함수는 first class이므로 함수를 다른 함수에 대한 입력으로 사용할 수 있으며 함수는 함수를 반환 할 수 있습니다. // 일급객체에 대한 얘기인듯
이러한 특성들로 기본 빌딩 블록을 구성하게되면 소프트웨어를 작성하기위한 간단하면서도 표현력있는 어휘가 됩니다.
JavaScript에서 익명 함수 및 currying은 선택사항입니다.
JavaScript는 람다 대수의 중요 특성을 지원하지만 강제하지는 않습니다.
고전적인 함수 합성은 한 함수의 출력을 가져 와서 다른 함수의 입력으로 사용합니다. 예를 들어, 합성 :
f . g
TypeScript
복사
다음과 같이 구현할 수 있습니다 :
compose2 = f => g => x => f(g(x))
TypeScript
복사
사용 방법은 다음과 같습니다.
double = n => n * 2
inc = n => n + 1
compose2(double)(inc)(3)
TypeScript
복사
compose2() 함수의 첫번째 인수로 double 함수를 사용하고 두 번째 인수로로 inc 함수를 사용합니다. 그리고 마지막으로 인수 3 을 적용apply합니다.
compose2() 함수 서명function signature을 다시 보면, f 는 double() , g 는 inc() , x 는 3 입니다.
compose2(double)(inc)(3) 은 실제로는 세번의 호출과정을 거칩니다.
1.
첫 번째는 double 을 받고 새 함수를 리턴합니다.
2.
리턴 된 함수는 inc 를 받고 새 함수를 리턴합니다.
3.
다음에 리턴 된 함수는 3 취하여 f(g(x))를 평가하는데, 이제는 double(inc(3)) 됩니다.
4.
x 는 3으로 평가되고 inc() 로 전달됩니다.
5.
inc(3) 은 4로 평가됩니다.
6.
double(4) 는 8 로 평가됩니다.
7.
최종적으로 8이 리턴됩니다.
소프트웨어가 합성되면 합성 함수 그래프로 나타낼 수 있습니다.
append = s1 => s2 => s1 + s2
append('Hello, ')('world!')
TypeScript
복사
위 코드를 시각적으로 표현한 것입니다.
람다 대수는 소프트웨어 설계에 엄청난 영향을 주었고 1980년 이전 컴퓨터 과학분야에서 영향력있는 많은 사람들은 함수를 합성하며 소프트웨어를 작성했습니다. Lisp은 1958 년에 만들어졌고 람다 대수의 영향을 많이 받았습니다. 오늘날, Lisp은 여전히 널리 쓰이는 두 번째로 오래된 언어입니다.
저는 AutoLISP으로 Lisp을 처음 접했습니다. 이는 CAD 소프트웨어 인 AutoCAD에서 사용하는 스크립트 언어입니다. AutoCAD는 인기가 높으며, 거의 모든 CAD 응용 프로그램애플리케이션이 AutoLISP를 지원하므로 호환성이 좋습니다. 또한 Lisp은 컴퓨터 과학 커리큘럼에서 인기있는 교육용 언어이기도합니다.
1.
Lisp은 매우 단순해서 하루 만에 기본 문법과 의미를 배울 수 있습니다.
2.
Lisp은 모두 함수 합성에 관한 것이고, 함수 합성은 어플리케이션을 구조화하는 우아한 방법이다.
3.
// 많이 들어봤지만 배운 적이 없다. 왜일까?
합성 가능한 소프트웨어의 몰락
1970 년에서 1980 년 사이에 소프트웨어 작성 방법이 단순히 합성하는 것에서 벗어나
컴퓨터에게 일련의 명령Instruction을 내리는 방식으로 바뀌었습니다. 그리고 객체지향 프로그래밍(OOP)이 등장했습니다.
구성 요소를 캡슐화 하고 메세지를 전달한다는 위대한 아이디어는 대중적인 프로그래밍 언어들에 의해 왜곡되었습니다.
이들은 기능을 재사용하기 위해 상속에 의한 계층 구조와 is-a 관계라는 끔찍한 아이디어를 생각해냈습니다.
결국 함수형 프로그래밍은 학계로 밀려났습니다.
괴짜 중의 괴짜 프로그래머들, 아이비 리그의 교수들, 그리고 1990 ~ 2010년대의 강제 자바 수용 메타에서 탈출 한 일부 운 좋은 학생들만이 사용하게 되었습니다.
30년의 암흑기 동안 대부분의 사람들에게 소프트웨어를 만드는 것은 마치 악몽과 같았습니다.
합성 가능한 소프트웨어의 부상
2010년 경 JavaScript의 사용이 폭발적으로 증가했습니다. 2006년 이전 JavaScript는 웹 브라우저에서 귀여운 애니메이션을 만드는 데 사용되는 장난감 언어로 취급받았지만
사실 강력한 기능이 숨겨져있었습니다.
즉, 람다 대수의 중요한 특징들이 포함되어 있었습니다. 사람들은 "함수형 프로그래밍"이라는 새롭고 멋진 무언가에 대해 그림자 속에서 속삭이기 시작했습니다.
2015년, 함수 합성으로 소프트웨어를 개발하려는 아이디어가 다시 인기를 얻었습니다. JavaScript는 10년 만에 이루어진 첫 번째 주요 업그레이드에서 화살표 함수를 추가하여 함수, currying 및 lambda 표현을 쉽게 읽고 만들 수 있게 했습니다.
함수형 프로그래밍 붐에서 JavaScript의 화살표 함수는 로켓 연료와 같았습니다. 오늘날 널리 쓰이는 응용 프로그램 애플리케이션들 중에서 함수형 프로그래밍 기술이 사용되지 않은 경우는 거의 없습니다.
합성은 소프트웨어의 동작을 명료하게 모델링하는 간단하고 우아한 표현 방법입니다. 작은 소프트웨어 구성 요소들을 합성하여 더 큰 구성 요소와 기능을 만드는 프로세스는 조직화, 이해, 디버그, 확장, 테스트 및 유지 관리가 더 쉬운 소프트웨어를 만듭니다.
다음 글부터는 여러 예제들을 함께 실습해 볼 것입니다. 어린 시절 우리는 사물들을 가지고 놀며 학습했습니다. 발견의 즐거움을 재발견하십시오. 마술을 부려 볼 시간입니다.
왜 JavaScript로 함수형 프로그래밍을 배우는가?
그간의 js에 대한 생각을 접어두고 초심자의 마음으로 읽으세요.
JS나 순수 함수 언어에 익숙한 노련한 개발자면 js를 함수 프로그래밍을 탐구하기 위한 선택이라 느낄 수 있다.
열린 마음으로 읽어라. js 프로그래밍은 다른 레벨이 있다. 당신도 결코 모르는 것이다.
JS는 함수형 프로그래밍에 가장 중요한 기능을 가지고 있다.
1.
first class 함수 : 함수를 값으로 사용하는 기능, 함수를 인수로 전달하고 함수를 반환하고, 함수를 변수 및 객체 속성에 할당한다. 이는 고차 함수를 허용하여 부분 적용(partial application), currying 및 합성을 가능하게 한다.
2.
익명 함수와 간결한 람다 구문 : x ⇒ x * 2 는 JS에서 유효한 함수 표현식이다. 간결한 람다는 고차원 함수로 작업하기가 더 쉽습니다.
3.
클로저 : 클로저는 어휘 스코프와 함수의 묶음이다. 클로저는 함수가 만들어질 때 함께 만들어진다. 함수가 다른 함수 내부에서 정의되면 외부 함수가 종료된 후에도 외부 함수의 바인딩에 액세스할 수 있다. 클로저는 부분 적용에서 고정된 인수를 얻는 방법이다.(반환된 함수의 클로저 범위에 바인딩된 인수)
add2(1)(2)에서 1은 add2(1)에 의해 리턴된 함수의 고정 인수다. // 자유 변수 free variables를 말한건가?
JavaScript가 놓치고 있는 것들
JavaScript는 여러 가지 스타일로 프로그래밍 할 수 있는 멀티 패러다임 언어입니다. 우리는 JavaScript로 절차적(명령형) 프로그래밍(C와 같은)을 할 수 있으며 이 때 함수란 반복적으로 호출할 수 있는 명령어의 서브 루틴을 나타냅니다. 함수가 아닌 객체가 기본 빌딩 블록인 객체지향 프로그래밍을 할 수도 있으며 물론 당연히 함수형 프로그래밍도 가능합니다. 멀티 패러다임 언어의 단점은 절차적 및 객체 지향 방식의 프로그래밍으로 접근할 경우 거의 모든 것이 변경 가능해야한다는 것을 암시하는 경향이 있다는 것입니다.
변이 mutation는 내부에서 일어나는 데이터 변화입니다. 예를 들자면 :
const foo = {
bar: 'baz'
};
foo.bar = 'qux'; // mutation
TypeScript
복사
메소드는 객체의 속성을 업데이트하기 때문에 객체는 일반적으로 변경 가능해야 합니다.
명령형 프로그래밍에서 대부분의 자료 구조는 객체 및 배열을 효율적으로 조작하기 위해 변경가능합니다.
다음은 JavaScript에 없는 일부 함수형 언어의 특징입니다.
1.
순수 Purity : 일부 FP 언어에서는 순수성이 언어에 의해 시행됩니다. 부수효과가 발생할 수 있는 표현식은 허용되지 않습니다.
2.
불변성 Immutability : 일부 FP 언어는 변이를 금지합니다. 기존 데이터 구조 (예 : 배열 또는 객체)를 변경하는 대신 표현식은 새로운 데이터 구조를 생성합니다. 이것을 비효율적이라고 생각할 수 있지만 대부분의 함수형 언어의 기저에는 트라이 trie 기반 데이터 구조가 있습니다. 즉, 이전 객체와 새 객체가 동일한 데이터에 대한 참조를 공유한다는 의미입니다.
3.
재귀 recursion : 재귀는 반복을 목적으로 함수를 참조하는 기능입니다. 많은 FP 언어에서 재귀는 반복작업을 수행 할 수있는 유일한 방법입니다. for , while 또는 do 루프와 같은 루프 문은 없습니다.
[1] JavaScript로 재귀적 코드를 작성할 수 없다는 것이 아닌 재귀만을 사용해 반복적인 작업을 수행해야 하는 제약이 없다는 뜻입니다.
순수 : JavaScript에서는 코딩 컨벤션으로 순수함을 달성해야합니다.
순수 함수로 응용 프로그램의 대부분을 작성하지 않으면 함수형 스타일로 프로그래밍했다고 할 수 없습니다.
유감스럽게도 JavaScript에서는 의도치 않게 불순한 함수를 작성하기 쉽습니다.
불변성 : 순수 함수형 언어에서 불변성이 종종 강요됩니다.
JavaScript는 대부분의 함수 언어에서 사용되는 효율적이고 불변의 트라이 기반 데이터 구조가 부족하지만 Immutable.js 와 Mori등 이를 도와주는 라이브러리가 있습니다.
저는 향후 ECMAScript 스펙이 불변 데이터 구조를 지원해주길 바라고 있습니다.
ES6에서 const 키워드를 추가한 것을 보면 가능성 있는 이야기 입니다. const 로 선언된 변수에 다른 값을 재 할당 할 수 없습니다. 그러나 const 가 불변값을 뜻하는 것은 아닙니다. // 불변과 재할당 불가능은 다르다.
const 로 선언된 변수에 완전히 다른 객체를 참조하기 위해 다시 할당 할 수는 없지만 참조하는 객체의 속성 이 변경 될 수 있습니다. JavaScript는 또한 객체를 freeze() 수 있지만 그 객체는 루트 수준에서만 고정되어 있습니다. 즉, 중첩된 객체의 속성은 변형 될 수 있습니다. 다시 말하자면 JavaScript에서 진정한 불변성을 보기까지는 아직 갈 길이 멉니다.
재귀 : JavaScript는 재귀를 지원하지만 대부분의 함수형 언어에는 꼬리 호출 최적화tail call optimization라는 기능이 있습니다. 꼬리 호출 최적화는 재귀 함수가 스택 프레임을 재사용하여 재귀호출을 할 수 있게 해주는 기능입니다.
꼬리 호출 최적화가 없으면 스택이 계속 증가하여 스택 오버플로를 일으킬 수 있습니다. JavaScript는 ES6 명세에서 꼬리 호출 최적화를 명시했습니다.
불행하게도 아직까지는 주요 브라우저 엔진 중 하나만이 이를 구현했으며, 최적화가 부분적으로 구현된 후 Babel (구 브라우저에서 사용하기 위해 ES6에서 ES5로 컴파일하는 데 사용되는 가장 보편적 인 표준 JavaScript 컴파일러)에서 제거되었습니다. //
크롬은 되냐?
결론 : 꼬리 위치에서 함수를 주의해서 호출하더라도 대용량 반복에 대해 재귀를 사용하는 것은 여전히 안전하지 않습니다.
JavaScript가 순수한 함수형 언어로서 부족한 점
순수 주의자들은 자바 스크립트의 변이 가능성이 가장 큰 단점이라고 말합니다. 그러나 부수효과와 변이가 도움이 되는 경우가 있습니다.
Haskell과 같은 순수 함수형 언어에서는 모나드 monad 라는 상자를 사용하여 부수효과를 순수 함수로 감쌉니다. 따라서 모나드가 나타내는 부수효과가 불명확하더라도 프로그램을 순수하게 유지할 수 있습니다.
모나드의 사용법이 간단하더라도 모나드가 많은 사람들에게 익숙하지 않다는 문제점이 있습니다. 이를 처음 보는 사람에게 모나드가 무엇인지 설명하는 것은 색맹인 사람에게 "푸른 색"이 어떻게 보이는지 설명하는 것과 비슷합니다.
“모나드는 endofunctor라는 범주에 속한 한 monoid에 불과해. 뭐가 문제야?” 제임스 아이리 (James Iry)는 필립 와들러 (Phillip Wadler)의 의역을 들먹이며 손더스 맥 레인 (Shanders Mac Lane)을 인용했다. “A Brief, Incomplete, and Mostly Wrong History of Programming Languages”
일반적으로 패러디란 재미있는 부분을 더 재미있게 만들기 위해 과장하는 경향이 있습니다. 위의 인용문에서 모나드에 대한 설명은 실제로 다음 에 나오는 원래의 인용구에서 단순화 된 것 입니다.
" x 의 모나드는 x의 endofunctor라는 범주에 속한 한 monoid이다. ×의 연산은 endofunctor들의 조합과 항등 endofunctor의 단위로 구성된 일련의 연산 집합들로 치환 가능하다"~ Saunders Mac Lane._ “Categories for the Working Mathematician” // endofunctor를 알아야 할듯
그럼에도 불구하고, 전 모나드에 대한 두려움이 과장되어 있다고 생각합니다. 모나드를 배우는 가장 좋은 방법은 그 주제에 관한 책과 블로그 글을 읽지 않고, 바로 사용해 보는 것입니다.
함수형 프로그래밍의 다른 주제들과 마찬가지로, 학문적 어휘는 그것의 개념과 사용법보다 이해하기가 훨씬 어렵습니다. 믿어주세요. 함수형 프로그래밍을 하기 위해 손더스 맥 레인(Saunders Mac Lane)을 이해할 필요는 없습니다.
모든 프로그래밍 스타일에 완전히 적합하다고 할 수는 없지만 JavaScript는 다양한 프로그래밍 스타일과 배경을 가진 사람들이 사용할 수 있도록 고안된 범용 언어입니다.
"… C ++ 또는 (우리가 희망하는) Java로 컴포넌트를 작성하는 프로그래머. HTML에 직접 삽입 된 코드를 작성하는 ‘스크립터’, 아마추어 또는 프로. "
원래 Netscape는 두 가지 언어를 지원할 것이고 스크립트 언어는 아마도 Scheme (Lisp의 방언)과 비슷할 예정이었습니다.
브랜던 아이크 :
“저는 넷플릭스에 채용될 때 브라우저에서 Scheme을 구현하는 계획을 맡게 됐습니다.”
JavaScript는 새로운 언어여야 했습니다.
"엔지니어링 경영진의 요구사항은 '자바처럼 보일 것’이었습니다. 이는 Scheme과 함께 Perl, Python, Tcl을 배제하는 결과로 이어졌습니다. "
그 때 브랜던의 머리에서 나온 아이디어는 다음과 같습니다.
1.
브라우저에서 돌아가는 Scheme.
2.
자바처럼 보일 것.
결국 잿더미가 되고 말았습니다.
"나는 자랑스럽지 않지만
Scheme스러운 first class 함수와
Self-ish (단 하나 임에도 불구하고) 프로토 타입을 이 언어의 특징으로 채택하게되어 기쁩니다.
자바의 영향, 특히 y2k 날짜 버그뿐만 아니라 원시타입와 객체의 구분 (예 : string 대 String)은 불행했습니다. "
여기에 Java와 유사한 "불행한"기능들을 추가하여 마침내 JavaScript가 됐습니다.
불행한 기능들:
•
생성자 함수 및 new 키워드는 팩토리 함수와 동일한 일을 하기 위해 다른 호출 방식과 문법을 사용하게 합니다.
•
class 키워드는 단일 조상으로부터 extend 하는 기본 상속 메커니즘입니다.
•
많은 이들이 class 를 정적 타입인 것처럼 생각하는 경향이 있습니다 (그렇지 않습니다).
충고를 드리자면: 가능한 사용하지 마세요.
오늘날 Java, Flash 및 ActiveX 확장 프로그램은 대부분의 브라우저에서 더이상 지원되지 않습니다.
즉 스크립팅 방식이 "컴포넌트" 접근 방식 보다 우위를 점하는 것으로 밝혀 졌고 JavaScript는 유연한 언어가 되었습니다.
결국 JavaScript는 브라우저에서 직접 지원하는 유일한 언어가 되었습니다.
브라우저는 JavaScript라는 단일 언어 바인딩 만 지원하기 때문에 덜 비대해지고 버그가 적어졌습니다. WebAssembly가 예외라고 생각할 수도 있지만 WebAssembly의 디자인 목표 중 하나는 호환 가능한 추상 구문 트리 (AST)를 사용하여 JavaScript의 언어 바인딩을 공유하는 것입니다. 실제로 첫 번째 데모에서는 WebAssembly를 ASM.js라는 JavaScript의 하위 집합으로 컴파일했습니다.
웹 플랫폼을 위한 유일한 표준 범용 프로그래밍 언어로서의 위치는 JavaScript가 소프트웨어 역사상 가장 큰 인기의 물결을 탈 수 있게 해 주었습니다.
앱은 세상을 먹었고 웹은 앱을 먹었고 JavaScript는 웹을 먹었습니다.
Apps ate the world, the web ate apps, and JavaScript ate the web.
자바 스크립트는 함수형 프로그래밍을위한 이상적인 도구는 아니지만 규모가 크고 분산 된 팀에서 대규모 응용 프로그램을 작성하기 위한 훌륭한 도구입니다. 서로 다른 팀에서는 응용 프로그램을 작성하는 방법에 대한 아이디어가 다를 수 있습니다.
스크립팅을 주로 하는 팀은 명령형 프로그래밍을 사용해 모듈을 붙이는데 집중할 수 있습니다.
다른 사람들은 아키텍처 추상화에 집중할 수 있습니다. 이 때는 (신중한) 객체 지향적 접근방식이 괜찮은 방법일 수도 있습니다.
또 다른 사람들은 함수형 프로그래밍을 채택해 순수 함수를 사용하여 응용 프로그램 상태를 결정적인 동시에 테스트 가능하도록 관리하고 사용자의 작업을 줄입니다.
이러한 팀들의 구성원은 모두 같은 언어를 사용하므로 서로 쉽게 아이디어를 교환하고 서로에게서 배우며 서로의 작업을 도와줄 수 있습니다.
JavaScript에서는 이러한 모든 아이디어가 공존 할 수 있습니다. 따라서 더 많은 사람들이 JavaScript를 채택 하게 되었고 이는 세계에서 가장 큰 오픈 소스 패키지 저장소 (2017년 2월), npm의 등장으로 이어졌습니다 .
JavaScript의 진정한 강점은 다양한 사고방식을 가진 사용자가 함께 있는 생태계입니다. 순수 함수형 프로그래밍주의자들을 위한 이상적인 언어는 아닐지라도 Java, Lisp, C와 같은 대중적인 언어에서 온 사람들이 모든 플랫폼에서 작동하는 동일한 문법을 사용하여 함께 작업하기에 이상적인 언어일 수 있습니다. JavaScript는 그러한 배경을 가진 사람들에게 이상적일 만큼 편하지는 않지만 언어를 배우고 빠르게 생산적이 될 만큼 편합니다.
저는 JavaScript가 함수형 프로그래머를위한 최고의 언어가 아니라는 것에 동의합니다.
그러나 다른 모든 함수형 언어는 누구나 이해하고 사용할 수 있다고 말할 수 없으며 JavaScript는 ES6에서 함수형 프로그래밍에 관심이있는 사용자의 요구에 부응 할 수 있음을 보여줬고 점차 나아질 것입니다. 전 세계의 거의 모든 회사에서 사용하는 JavaScript와 놀라운 생태계를 포기하지 마십시오, 이를 포용해서 소프트웨어 구성을 위한 더 나은 언어를 만들어 가는게 좋지 않을까요?
JavaScript는 이미 함수형 프로그래밍 언어로 충분 합니다. 즉, 사람들은 함수형 프로그래밍 기술을 사용하여 JavaScript에서 유용하고 흥미로운 모든 것을 구축하고 있습니다.
Netflix (및 Angular 2+로 작성된 모든 앱)는 RxJS 기반의 함수형 유틸리티를 사용합니다. Facebook 은 React의 순수 함수, 고차 함수 및 고차원 컴포넌트의 개념을 사용하여 Facebook 및 Instagram을 구축합니다. PayPal, KhanAcademy 및 Flipkart 는 상태 관리를 위해 Redux를 사용합니다.
Angular, React, Redux 및 Lodash는 JavaScript 생태계의 주요 프레임 워크이자 라이브러리이며, 모두 함수형 프로그래밍의 직접적인 영향을 받았습니다. Lodash 및 Redux의 경우 실제 JavaScript 애플리케이션에서 함수형 프로그래밍 패턴을 가능하게하는 목적을 가지고 제작되었습니다.
“어째서 JavaScript인가?” JavaScript는 실제로 회사가 실제 소프트웨어를 만드는 데 사용하는 언어이기 때문에. 좋아하든 싫어하든, JavaScript는 수십 년 동안 지속되었던 Lisp의 "가장 인기있는 함수형 프로그래밍 언어"라는 타이틀을 빼앗았습니다. 사실, Haskell이 오늘날의 함수형 프로그래밍 개념에 훨씬 더 적합한 표준 무기이지만, 사람들은 하스켈로 실제 응용 프로그램을 거의 구축하지 않고 있습니다.
언제나 미국에는 수십만 개의 JavaScript 일자리가 있으며, 전 세계적으로 수십만 가지가 있습니다. 하스켈을 배우는 것은 함수 프로그래밍에 대해 많은 것을 가르쳐 주지만, JavaScript를 배우는 것은 실제 직업을 위한 프로덕션 애플리케이션을 구축하는 것에 대해 가르쳐 줄 것입니다.
함수형 프로그래머를 위한 JavaScript 개요 // 복습용
사실 이번 편은 단순히 주제의 겉표면을 훑으며 관심을 환기하기 위해 작성됐다.
NodeJS 또는 브라우저 콘솔의 REPL을 사용해도 됩니다.
표현식과 값 Expressions and Values
표현식은 값으로 평가되는 코드 덩어리입니다.
다음은 JavaScript에서 유효한 표현식입니다.
7;
7 + 1; // 8
7 * 2; // 14
'Hello'; // Hello
TypeScript
복사
표현식의 값에는 이름을 붙일 수 있습니다. 이 때 표현식이 먼저 평가되고 결과 값이 이름에 저장됩니다. 변수를 선언하기 위해 const 키워드를 사용합니다. 여러 방법이 있지만, const를 가장 많이 사용하게 될 것입니다. 따라서 우리는 지금부터 const 를 사용 할 것입니다 :
const hello = 'Hello';
hello; // Hello
TypeScript
복사
var, let 및 const
JavaScript는 const 외에도 var 과 let이라는 두 종류의 변수 선언 키워드가 있습니다. 저는 이들을 순서에 맞게 사용할 것입니다. 기본적으로 가장 엄격한 선언인 const를 선택합니다. const 키워드로 선언 된 변수는 재할당할 수 없습니다. 즉, 선언할 때 최종 값이 저장됩니다. 이는 엄격하다고 여겨질 수도 있지만, 제약은 좋은 것입니다. "이 변수에 할당 된 값은 변경되지 않을 것입니다"라는 신호입니다. 함수 전체나 블록 스코프를 찾아볼 필요 없이 변수의 의미를 즉시 이해할 수 있습니다.
변수를 재할당하는 것이 유용할 때가 있습니다.
예를 들어 함수형으로 접근하지 않고 직접 명령을 반복 실행할 경우 let으로 선언된 카운터 변수에 반복하여 할당할 수 있습니다.
var은-이건 적어도 변수입니다-라는 약한 의미를 가집니다. ES6으로 프로그래밍하기 시작한 이후 저는 실제 프로젝트에서 var 를 의도적으로 선언 한 적이 없습니다.
let 또는 const 로 선언된 변수를 다시 선언하면 오류가 발생합니다. REPL (Read, Eval, Print Loop) 환경에서 실험적인 목적으로 코딩을 할 경우 이들 대신 var 를 사용하여 변수를 선언하는게 적합합니다.
실제 프로그램을 작성할 때 기본적으로 const를 사용하기 때문에 이 글에서도 const를 사용할 것입니다. 다만 실험을 위해서라면 자유롭게 var 을 사용하십시오.
타입
지금까지 두 가지 타입을 보았습니다
: 숫자와 문자열. 이 외에도 JavaScript에는 부울 ( true 또는 false ), 배열, 객체 등이 있습니다. 우리는 나중에 다른 타입을 얻을 것입니다.
배열은 순서가 있는 목록입니다. 다양한 항목을 담을 수있는 상자라고 생각하면 됩니다. 다음은 배열 리터럴 표기법입니다.
[1, 2, 3];
TypeScript
복사
변수에 할당할 수 있습니다.
const arr = [1, 2, 3];
TypeScript
복사
JavaScript의 객체는 key : value 쌍의 모음입니다. 표기법은 다음과 같습니다.
{
key : 'value'
}
TypeScript
복사
물론 변수에 할당 할 수 있습니다.
const foo = {
bar : 'bar'
}
TypeScript
복사
변수의 이름과 값으로 객체를 생성할 수 있습니다.
const a = 'a';
const oldA = {a : a}; // long, redundant way
const oA = {a}; // short an sweet!
TypeScript
복사
다시 한번 해보겠습니다.
const b = 'b';
const oB = {b};
TypeScript
복사
객체들로 새로운 객체를 쉽게 합성할 수 있습니다.
const c = {...oA, ...oB}; // {a : 'a', b : 'b'}
TypeScript
복사
...은 객체 스프레드 연산자입니다. oA 의 프로퍼티들을 반복하여 새로운 객체에 할당한 다음 oB 대해 동일한 작업을 수행합니다. 이 때 키가 중복되는 경우 이미 존재하는 키를 덮어씁니다. 이 글을 쓰는 시점에서 객체 스프레드는 아직 대중적인 브라우저에서 사용할 수 없는 실험적인 기능입니다. 브라우저가 지원하지 않을 경우 Object.assign()으로 대체 할 수 있습니다.
const d = Object.assign ({}, oA, oB);
// {a : 'a', b : 'b'}
TypeScript
복사
Object.assign() 은 객체 스프레드보다 조금만 더 타이핑하면 됩니다. 수많은 객체를 합성해야 하는 경우 타이핑을 줄일 수 있습니다. Object.assign() 을 사용할 때 최종적으로 리턴될 객체를 첫 번째 매개 변수로 전달해야합니다. 속성들을 복사 할 개체 말입니다. 만약 이를 생략하면 첫 번째 인수로 전달한 객체가 변경됩니다.
제 경험상, 새로운 객체를 만드는 것이 아닌 기존의 객체를 변경하는 것은 일반적으로 버그를 일으킬 여지가 많습니다. Object.assign()을 사용할 때는 이를 주의하십시오.
해체 혹은 비구조화destructuring[1]
객체와 배열 모두 해체를 지원합니다. 즉, 객체에서 값을 꺼내 변수에 할당 할 수 있습니다.
const [t, u] = [ 'a', 'b'];
t; // 'a'
u; // 'b'
const blep = {
blop: 'blop'
};
// The following is equivalent to
// const blop = blep.blop;
const {blop} = blep;
blop; // 'blop'
// Also equivalent to
// const a = this.state.a;
const {a} = this.state;
TypeScript
복사
위의 배열 예제 처럼 동시에 여러 변수에 할당할 수 있습니다. 다음은 Redux 프로젝트에서 자주 볼 수 있는 코드입니다.
const {type, payload} = action;
TypeScript
복사
Reducer에서 다음과 같이 사용합니다. (Reducer는 나중 글에서 설명할 것입니다)
const myReducer = (state = {}, action = {}) => {
const {type, payload} = action;
switch (type) {
case 'FOO': return Object.assign ({}, state, payload);
default : return state;
}
};
TypeScript
복사
새로운 이름으로 할당 할 수 있습니다.
const { blop: bloop } = blep;
bloop; // 'blop'`
TypeScript
복사
blep.blop 을 bloop에 할당했다 라고 읽으면 됩니다.
비교 및 삼항 연산자
값을 비교할 때는 완전 항등 연산자("triple equals"라고 함)를 사용합니다.
3 + 1 === 4; // true
TypeScript
복사
물론 다른 항등 연산자가 있습니다. 공식적으로 "동등"연산자라고합니다. 비공식적으로 "double equals"라고 합니다. double equals는 한두가지 정도 의미있게 사용할 상황이 있습니다. 그 외에는 항상 === 연산자로 비교하는 것이 좋습니다.
이외에도 다음과같은 비교 연산자들이 있습니다.
•
> 보다 큼
•
< 보다 작음
•
>= 크거나 같음
•
<= 작거나 같음
•
!= 같지 않음
•
!== 엄격하게 같지 않음
•
&& 논리 곱
•
|| 논리 합
삼항 표현식은 삼항 연산자를 사용한 표현식입니다. 조건이 참이냐 거짓이냐에 따라 다른 값으로 평가됩니다.
14 - 7 === 7? 'Yep!' : 'Nope.'; // Yep!
TypeScript
복사
함수
JavaScript에는 함수 표현식이 있으며 이는 변수에 할당 할 수 있습니다.
const double = x => x * 2;
TypeScript
복사
위 코드는 수학에서의 함수 f(x) = 2x 와 같습니다. 소리내어 읽을 경우 f x는 2x 라고 읽습니다. 이 함수는 x에 특정한 값을 적용 할 때만 의미가 생깁니다. 다른 식에서 이 함수를 사용하려면 f(2)라고 쓰면 됩니다. f(2) 는 4와 같은 의미입니다.
즉, f(2) = 4 입니다. 수학의 함수는 입력에서 출력으로의 매핑이라고 생각할 수 있습니다. 이 경우 f(x) 는 x 에 대한 입력 값을 입력 값과 2 의 곱과 동일한 해당 출력 값에 매핑하는 것입니다.
자바 스크립트에서 함수 표현식의 값은 함수 그 자체입니다.
double; // [Function : double]
TypeScript
복사
.toString() 메서드를 사용하여 함수 정의를 볼 수 있습니다.
double.toString (); // 'x => x * 2'
TypeScript
복사
특정 인수(값)에 함수를 적용하려면 함수를 호출해야합니다. 함수 호출이란 인수에 함수를 적용하고 평가된 값을 리턴받는 것입니다.
<functionName>(argument1, argument2, ...rest) 와 같은 문법으로 함수를 호출 할 수 있습니다. 예를 들어 double 함수를 호출하려면 괄호를 추가하고 double 값을 전달하면됩니다.
double(2); // 4
TypeScript
복사
일부 함수형 언어들과는 달리 괄호가 필수입니다. 괄호가 없으면 함수는 호출되지 않습니다 :
double 4; // SyntaxError : Unexpected number
TypeScript
복사
서명 혹은 시그니처
함수들은 다음과 같이 서명을 가집니다.
1.
함수 이름(선택사항)
2.
인자 타입 목록(매개 변수의 이름은 선택사항)
3.
리턴 타입
JavaScript의 함수서명에는 타입을 명시하지 않아도 됩니다. JavaScript 엔진은 런타임에 타입을 파악합니다. 충분한 단서들을 제공할 경우 IDE (Integrated Development Environment) 및 Tern.js 와 같은 개발자 도구에서는 데이터 흐름을 분석하여 서명을 유추해내기도 합니다.
자바 스크립트는 함수 서명에 대한 표준이 없기 때문에 여러 표준들이 경쟁을 하는 상황입니다. JSDoc은 오랫동안 쓰여왔지만 장황하고 다소 어색합니다. 대부분의 사람들은 코드에 대한 주석을 최신화하는데 별로 관심이 없기에 많은 JS 개발자가 더 이상 사용하지 않습니다.
가장 인기 있는 두 표준 TypeScript와 Flow가 있습니다. 저는 둘중 어떠한 것이 더 나은지 확실하지 않습니다. 따라서 저는 Rtype을 사용합니다. 어떤 사람들은 커리curry를 위해 하스켈 전용 Hindley-Milner Type 을 꺼내들기도 합니다.
저는 JavaScript 문서화를 위해 얼른 좋은 표기법이 표준화되어야 한다고 봅니다. 그러나 지금 나와있는 해결책 중 어느것도 완벽하다고 할 수 없습니다. 당분간 여러분이 사용하고있는 것과는 약간 다른 문법으로 쓰인 다양한 유형의 서명들을 이해하기 위해 최선을 다해야 할 것입니다.
functionName (param1 : Type, param2 : Type) => Type
TypeScript
복사
double 함수의 서명은 다음과 같습니다.
double (x : n) => Number
TypeScript
복사
JavaScript는 강제로 주석을 달지 않아도 된다는 사실에도 불구하고, 함수를 사용할 때 그리고 합성할 때 효율적으로 의사소통을 하기 위해 서명에 의미를 재빨리 파악할 수 있는 것이 중요합니다.
함수 합성에 관련된 대부분의 라이브러리들은 동일한 타입의 서명을 가진 함수를 전달할 것을 요구합니다.
기본 매개 변수 값default parameter values
JavaScript는 기본 매개 변수 값을 지원합니다. 다음 함수는 전달받은 값을 그대로 리턴하는 항등 identity 함수입니다. 그러나 undefined를 인수로 전달받거나 아무 인수도 전달받지 않은 경우 0을 리턴합니다.
const orZero = (n = 0) => n;
TypeScript
복사
기본값을 설정하려면 n = 0 처럼 = 연산자를 사용하여 매개 변수에 값을 할당하기만 하면 됩니다. 이런 식으로 기본값을 지정하면 Tern.js , Flow 또는 TypeScript를 사용할 때 함수의 형식과 서명을 주석으로 명시하지 않아도 이를 자동으로 유추할 수 있습니다.
기본 매개 변수값을 지정하고 텍스트 에디터나 IDE에 관련 플러그인을 설치하면 함수를 사용하려고 할 때 함수 서명이 표시됩니다. 또한 함수를 사용하는 방법을 파악하기 쉽습니다. 이는 코드 자체가 곧 문서가 될 수 있는 중요한 접근방법입니다.
참고 : 기본값이 있는 매개 변수는 함수의 .length 속성에서 제외됩니다. 즉 length값을 참조하는 autocurry 라이브러리를 사용할 때 문제가 발생할 수 있습니다. 그러나 일부 currying 라이브러리 (예 : lodash/curry )에서는 함수 인자의 개수arity를 임의로 전달 하는 옵션이 있습니다.
해체-할당과 기본값
JavaScript 함수에서 객체 리터럴을 받아 해제-할당하면 이를 보통 인수처럼 사용할 수 있습니다. 이 때도 마찬가지로 기본 매개 변수 기능을 사용하여 기본값을 할당 할 수 있습니다.
const createUser = ({
name = 'Anonymous',
avatarThumbnail = '/avatars/anonymous.png'
}) => ({
name,
avatarThumbnail
});
const george = createUser ({
name : 'George'
avataThumbnail : 'avatars/shades-emoji.png'
});
george;
/*
{
name : 'george'
avatarThumbnail : 'avatars/shades-emoji.png'
}
*/
TypeScript
복사
나머지와 스프레드 연산자 문법
나머지 연산자...를 사용해서 함수의 정해지지 않은 인수들을 배열로 참조할 수 있습니다.
예를 들어 다음 함수는 첫 번째 인수를 버리고 나머지를 배열로 리턴합니다.
const aTail = (head, ...tail) => tail;
aTail(1, 2, 3); // [2, 3]
TypeScript
복사
나머지 구문은 개별 요소를 배열로 만듭니다. 스프레드는 반대의 역할을 합니다. 즉, 주어진 배열을 개별 요소로 퍼트립니다. 나머지와 스프레드는 같은 형태를 가지지만 사용법이 다릅니다. 이를 주의해서 다음 코드를 참고하세요:
const shiftToLast = (head, ...tail) => [...tail, head];
/* 첫 번째 ...tail은 나머지연산자가 사용되었고 뒤에는 스프레드
연산자가 사용되었습니다.*/
shiftToLast(1, 2, 3); // [2, 3, 1]
TypeScript
복사
JavaScript의 배열에 있는 iterator는 스프레드 연산자가 사용될 때 함께 호출됩니다. iterator는 배열의 요소 값들을 반복해서 리턴합니다. [...tail, head] 표현식에서 iterator는 나머지 구문으로 전달받은 tail 배열에서 값을 하나씩 꺼내 새로운 배열 리터럴에 복사합니다. head는 이미 개별 요소이기 때문에 배열의 끝 부분에 붙이면 됩니다.
커링currying
커리된 함수는 한 번에 하나씩 여러 인자를 받는 함수입니다. 인자를 받아 그 다음인자의 입력을 기다리는 함수를 리턴합니다. 모든 인자가 채워지면 최종 값이 리턴됩니다.
커링 및 부분 적용 partial application은 함수를 반환하는 행위입니다.
const highpass = cutoff => n => n >= cutoff;
const gt4 = highpass(4); // highpass() returns a new function
TypeScript
복사
꼭 화살표 구문을 사용할 필요는 없습니다. JavaScript에는 function 키워드가 있습니다. 다만 function 키워드를 쓰면 타이핑을 조금 더 해야할 뿐입니다. 즉 아래의 highpass는 위의 정의와 동일합니다.
const highpass = function highpass(cutoff) {
return function(n) {
return n >= cutoff;
};
};
TypeScript
복사
화살표는 "함수"를 뜻합니다. 그러나 몇 가지 중요한 차이가 있습니다. ( => 은 기본적으로 this가 없기 때문에 생성자로 사용할 수 없습니다) 이 주제에 관해선 나중에 더 깊게 알아볼 것입니다. 그러니 지금 당장은 x => x 라는 코드를 " x 를 받아 x를 리턴하는 함수"라고 생각하십시오. 따라서
const highpass = cutoff => n => n >= cutoff;
TypeScript
복사
라는 코드는 다음처럼 읽으면 됩니다:
highpass 는 cutoff 를 받고 n을 받은 이후에 n >= cutoff 의 결과를 리턴하는 함수를 리턴하는 함수
함수를 리턴하는highpass() 를 사용해 보다 특화된 함수를 만들 수 있습니다.
const gt4 = highpass(4);
gt4(6); // true
gt4(3); // false
TypeScript
복사
autocurry를 사용하면 최대한 유연한 방식으로 함수를 커링할 수 있습니다.
const add3 = curry((a, b, c) => a + b + c);
TypeScript
복사
autocurry된 add3 함수는 다양한 방법으로 사용할 수 있습니다.
add3(1, 2, 3); // 6
add3(1, 2)(3); // 6
add3(1)(2, 3); // 6
add3(1)(2)(3); // 6
TypeScript
복사
하스켈 팬에게는 죄송한 일이지만, JavaScript에서는 autocurry를 하기 위해 Lodash와 같은 라이브러리를 사용해야 합니다.
$ npm install --save lodash
Plain Text
복사
npm으로 설치 한 다음 코드 상단에서 import하면 됩니다 :
import curry from 'lodash/curry';
TypeScript
복사
아니면 다음처럼 마술을 부리면 됩니다.
// Tiny, recursive autocurry
const curry = (f, arr = []) => (...args) =>
(a => a.length === f.length ? f(...a) : curry(f, a))
([...arr, ...args]);
TypeScript
복사
함수 합성
당연히 함수를 합성 할 수 있습니다. 함수 합성은 한 함수의 리턴 값을 다른 함수의 인수로 전달하는 과정입니다. 수학 표기법 :
f . g
Plain Text
복사
는 JavaScript에서 다음과 같이 표현됩니다.
f (g (x))
Plain Text
복사
그리고 내부적으로 다음과 같이 계산됩니다 :
1.
x 가 평가됩니다.
2.
x에 g()가 적용됩니다.
3.
g(x)의 반환 값에 f()가 적용됩니다.
예 :
const inc = n => n + 1;
inc(double(2)); // 5
TypeScript
복사
값 2 는 double() 으로 전달되어 4 를 생성합니다. 4 는 inc() 로 전달되고 5 평가됩니다.
어떤 표현식을 함수의 인수로 전달할 수 있습니다. 이 때 표현식이 먼저 평가된 후 함수가 적용됩니다.
inc(double(2) * double(2)); // 17
TypeScript
복사
double(2) 은 4 로 평가되므로 inc(4 * 4) 가 된 후 inc(16) 로 평가되고 17이 리턴됩니다.
함수 합성은 함수형 프로그래밍의 핵심입니다.
배열
배열에는 몇 가지 내장 된 메소드가 있습니다. 메소드는 객체와 관련된 함수입니다. 일반적으로 객체의 속성property으로 존재합니다.
const arr = [1, 2, 3];
arr.map(double); // [2, 4, 6]
TypeScript
복사
이 경우 arr 이 객체이고 .map() 함수가 값으로 할당되어있는 속성입니다. 이 함수를 호출하면 인수에 적용될 뿐만 아니라 this 라는 특수 매개 변수에도 적용됩니다.
this 매개 변수는 메소드가 호출 될 때 자동으로 설정됩니다. map() 메소드에서 배열의 값에 접근할 때this를 참조합니다.
double 함수를 호출하는 map에 전달하는걸 잘 보십시오. map 은 함수를 인수로 받아여 배열의 각 항목에 적용합니다. 그리고 double() 의해 리턴 된 값으로 새로운 배열을 생성하여 리턴합니다.
원래의 arr 값은 변하지 않습니다.
arr; // [1, 2, 3]
TypeScript
복사
메소드 체이닝
메소드를 호출을 연결할 수도 있습니다. 메소드 체인은 리턴 값을 따로 변수에 저장하여 다시 참조 할 필요없이 함수의 리턴 값에 메소드를 계속해서 호출하는 프로세스입니다.
const arr = [1, 2, 3];
arr.map(double).map(double); // [4, 8, 12]
TypeScript
복사
predicate 는 부울 값 ( true 또는 false )을 리턴하는 함수입니다.
.filter() 메소드는 predicate를 인수로 받아 배열의 개별 항목에 적용합니다. 이 때 조건을 통과한(true를 반환) 항목만 선택하여 새 배열에 포함시켜 리턴합니다.
[2, 4, 6].filter(gt4); // [4, 6]
TypeScript
복사
배열에서 조건에 맞는 항목을 골라 매핑하는 상황은 자주 발생합니다.
[2, 4, 6].filter(gt4).map(double); //[8, 12]
TypeScript
복사
참고 : 이 시리즈의 뒷부분에는 선택과 매핑을 동시에 하는 더 효율적인 방법인 transducer가 나옵니다.
결론
머리가 핑핑 돌고 있습니까? 걱정하지 마세요. 지금까지 배운 것들은 단지 맛보기에 지나지 않습니다. 앞으로도 계속 이 주제들이 반복적으로 그리고 좀 더 깊은 이해를 위해 등장할 것입니다.
고차 함수
고차 함수 higher order function 는 함수를 인수로 받거나 함수를 리턴하는 함수입니다.
반면에 1차 함수first order function는 함수를 인수로 사용하거나 함수를 출력으로 리턴하지 않습니다.
이전 글에서 우리는 .map() 과 .filter() 예제를 보았습니다. 둘 다 인수로 함수를 사용합니다. 즉, 둘 다 고차 함수입니다.
단어 목록에서 네 글자로 이루어진 단어를 선택하는 1차 함수의 예를 살펴 보겠습니다.
const censor = words => {
const filtered = [];
for (let i = 0, { length } = words; i < length; i++) {
const word = words[i];
if (word.length !== 4) filtered.push(word);
}
return filtered;
};
censor(['oops', 'gasp', 'shout', 'sun']);
// [ 'shout', 'sun' ]
TypeScript
복사
이제 's’로 시작하는 모든 단어를 선택하려면 어떻게 해야 할까요? 또 다른 함수를 만들면 됩니다 :
const startsWithS = words => {
const filtered = [];
for (let i = 0, { length } = words; i < length; i++) {
const word = words[i];
if (word.startsWith('s')) filtered.push(word);
}
return filtered;
};
startsWithS(['oops', 'gasp', 'shout', 'sun']);
// [ 'shout', 'sun' ]
TypeScript
복사
딱봐도 두 함수가 동일한 코드를 많이 반복하고 있습니다. 코드를 더 일반화된 해결책으로 추상화하는 패턴이 있습니다. 두 함수는 공통점이 많습니다. 둘 다 목록을 순회 iterate 하고 주어진 조건으로 필터링합니다.
순회와 필터링을 위한 코드가 자기들을 추상화 해달라고 구걸하고 있습니다. 모든 종류의 유사한 함수들을 작성할 때 공유하고 재사용해달라고 말합니다. 사실 어떤 목록에서 물건을 선택하는 것은 매우 일반적인 작업입니다.
다행스럽게도 JavaScript의 함수는 일급first class입니다. 그게 무슨 뜻이냐구요? 숫자, 문자열 또는 객체와 마찬가지로 함수는 다음과 같은 일을 할 수 있습니다.
•
식별자 (변수)값으로 할당
•
객체 속성 값에 할당
•
인수로 전달
•
함수에서 리턴됨
기본적으로 프로그램에 있는 다른 데이터들처럼 함수를 사용할 수 있으므로 추상화하기가 훨씬 쉬워졌습니다. 예를 들어 목록을 순회하는 과정을 추상화하고 데이터를 처리하는 함수인 reducer를 전달하여 리턴 값을 누적하는 함수를 만들 수 있습니다. 이 함수를 reduce 라고 부릅니다.
const reduce = (reducer, initial, arr) => {
// shared stuff
let acc = initial;
for (let i = 0, { length } = arr; i < length; i++) {
// unique stuff in reducer() call
acc = reducer(acc, arr[i]);
// more shared stuff
}
return acc;
};
reduce((acc, curr) => acc + curr, 0, [1,2,3]); // 6
TypeScript
복사
reduce()함수는 reducer 함수, 누적값accumulator의 초기값 그리고 순회할 배열을 인자로 받습니다. 배열의 각 항목마다 reducer가 호출되어 누적값과 현재 배열 요소를 전달합니다. 누적값에는 계속해서 값이 누적되며 배열의 모든 요소에 대해 순회한 이후 최종적인 누적값이 리턴됩니다.
맨 아래줄에서는 reducer함수로 (acc, curr) => acc + curr를 전달하는데 이는 배열 요소를 계속하여 누적하는 프로세스가 됩니다. 다음으로 초기 값인 0 과 순회할 데이터 배열을 전달합니다.
반복과 누적 accumulation 이 추상화되면서 이제는 좀 더 일반화 된 filter() 함수를 구현할 수 있습니다.
const filter = (
fn, arr
) => reduce((acc, curr) => fn(curr) ?
acc.concat([curr]) :
acc, [], arr
);
TypeScript
복사
filter()에서는 인수로 전달 된 fn()함수를 제외한 모든 것이 다른 곳에서 재사용될 수 있는 것들입니다. 이 때 fn()은 술어predicate라고합니다. 술어 는 부울 값을 리턴하는 함수입니다.
전달받은 배열에서 값을 하나씩 순회하며 fn()을 적용합니다. fn(curr) 테스트가 true 리턴하면 curr 값을 빈 배열에 계속하여 연결합니다. 테스트가 실패할경우 현재 배열값을 넘깁니다.
이제 filter() 를 사용해 네 글자로 이루진 단어를 필터링하는 censor()를 구현해보겠습니다.
const censor = words => filter(
word => word.length !== 4,
words
);
TypeScript
복사
인상적이지 않습니까? 다양한 작업(필터링, 순회)들이 추상화되었고 censor() 는 아주 짧은 함수가 됐습니다.
startsWithS()도 마찬가지입니다.
const censorstartsWithS = words => filter(
word => word.length !== 4startsWith('s'),
words
);
TypeScript
복사
몇몇 독자들은 이미 JavaScript가 이러한 추상화를 제공한다는걸 알고 있을 겁니다. Array.prototype메서드에는 .reduce() .filter() .map()와 같은 다양한 함수이 이미 존재합니다.
고차 함수는 다양한 데이터유형에서 동일하게 작동하도록 추상화하는데도 사용됩니다. 예를 들어 .filter()가 꼭 문자열 배열에서만 작동하라는 법은 없습니다. 인자로 전달하는 함수가 다른 데이터 유형을 처리하게만 하면 됩니다. highpass() 예제를 기억하십니까?
const highpass = cutoff => n => n >= cutoff;
const gt3 = highpass(3);
[1, 2, 3, 4].filter(gt3); // [3, 4];
TypeScript
복사
즉, 고차 함수를 사용하여 함수에 다형성을 부여할 수 있습니다. 보시다시피 고차 함수는 1차 함수보다 훨씬 다재다능합니다. 일반적으로, 실제 응용 프로그램애플리케이션은 고차함수와 매우 간단한 1 차 함수를 함께 사용합니다.