Mary Rose Cook이 작성한 A practical introduction to functional programming을 기반으로 작성한 문서입니다. 원문의 파이썬 코드 대신 자바스크립트 코드 예제로 내용을 설명합니다.
함수형 프로그래밍은 사이드 이펙트(side effect)가 없다는 것이 특징입니다. 이는 함수 외부에 있는 데이터 의존하지 않으며, 함수 외부에 있는 데이터를 변경하지 않는 것을 의미합니다. ‘함수형'의 모든 것은 이러한 속성에서 파생된다는 점을 명심해야 합니다.
다음은 비 함수형 function의 예제입니다.
다음은 비 함수형 function의 예제입니다.
var a = 0;
function increment1() {
return a += 1;
}
(역주 : increment1 함수가 외부 데이터 변수인 a 에 의존합니다.)
다음은 함수형 function의 예제입니다.
increment2(a) {
return a + 1;
}
반복문 대신에 map과 reduce를 사용하십시오.
Map
map은 함수와 배열 원소를 사용합니다. 먼저 비어있는 배열을 새롭게 생성합니다. 원본 배열의 각 원소에 대해 함수를 실행하고 반환 값을 새로운 배열에 삽입합니다. 모든 과정이 끝나면 새로운 배열을 반환합니다.
아래의 비 함수형 코드는 이름의 배열을 임의로 할당된 코드명으로 변경합니다.
var names = [“Mary”, “Isla”, “Sam”];
var code_names = [“Mr. Pink”, “Mr. Orange”, “Mr. Blonde”];
for(var i in code_names) {
var randomIndex = Math.floor(Math.random() * code_names.length);
var randomValue = code_names[randomIndex];
names[i] = randomValue;
}
(역주 : 실제 이름 리스트인 names를 반복문 수행 과정 내에서 직접 변경합니다.)
console.log(names);
# => ["Mr. Blonde", "Mr. Pink", "Mr. Pink"]
위 코드를 for 반복문 대신 map 함수를 사용하면 다음과 같습니다.
var names = [“Mary”, “Isla”, “Sam”];
var code_names = [“Mr. Pink”, “Mr. Orange”, “Mr. Blonde”];
names = names.map(function(item) {
var randomIndex = Math.floor(Math.random() * code_names.length);
var randomValue = code_names[randomIndex];
return randomValue;
});
(역주 : names에 대한 변경은 map 함수가 완료된 후 return 과정에서 이루어집니다.)
console.log(names);
# => ["Mr. Orange", "Mr. Orange", "Mr. Blonde"]
Reduce
reduce 또한 함수와 배열 원소를 사용합니다. 각 원소를 조합하여 생성된 값을 반환합니다.
다음 예제는 ‘Sam’이라는 단어를 문자열 목록에서 찾아 카운트합니다.
var sentences = [
‘Mary read a story to Sam and Isla’,
‘Isla cuddled Sam’,
‘Sam chortled’
];
var sam_count = 0;
for(var i in sentences) {
var results = sentences[i].match(/Sam/g);
if(results) {
sam_count += results.length;
}
}
console.log(sam_count);
# => 3
동일한 코드를 reduce로 작성하면 다음과 같습니다.
var sentences = [
‘Mary read a story to Sam and Isla’,
‘Isla cuddled Sam’,
‘Sam chortled’
];
var sam_count = sentences.reduce(
function(previousValue, currentValue) {
var results = currentValue.match(/Sam/g);
if(results) {
previousValue += results.length;
}
return previousValue;
},
0
);
console.log(sam_count);
# => 3
map과 reduce의 장점
- 한 줄로 간단하게 처리할 수 있습니다.
- map과 reduce에서는 반복에서 가장 중요한 부분인 배열, 연산, 반환 값이 항상 같은 위치에 있다.
- 반복문의 코드는 이전에 선언된 변수에 영향을 미칠 수 있다. map과 reduce는 일반적으로 함수형으로 작동합니다.
(역주 : map과 reduce는 함수형 프로그래밍 개념에 따라 기존 변수에 대한 사이드 이펙트가 없도록 구현하는 것이 원칙입니다. - map과 reduce는 원소 단위의 연산입니다. 반복문은 사람이 한 줄 단위로 로직을 읽고 이해해야 합니다. 반면에 map과 reduce는 복잡한 알고리즘과 원소의 추상적인 상태를 빠르게 이해할 수 있는 블록 단위의 코드를 제공합니다.
- map과 reduce와 유사한 기능을 가진 다양한 함수(filter, all, any, find 등)가 있습니다.
명령형으로 작성하지 말고 선언형으로 작성하십시오.
아래 예제 프로그램은 세 대의 차량에서 레이스를 진행합니다. 각 단계에서 차량은 앞으로 이동하거나 정지합니다. 프로그램은 각 단계 별 차량의 위치를 출력합니다. 5단계가 진행된 후 레이스를 종료합니다.
레이스의 출력 결과는 다음과 같습니다.
-
--
--
--
--
---
---
--
---
----
---
----
----
----
-----
프로그램 코드는 다음과 같습니다.
var time = 5;
var car_positions = [1, 1, 1];
while(time > 0) {
time -= 1;
console.log(‘’);
var carsCount = this.car_positions.length;
for(var i=0; i<carsCount; i++) {
if(Math.random() > 0.3) {
this.car_positions[i] += 1;
}
var output_str = '- '.repeat(car_position);
console.log(output_str);
}
}
위 코드는 명령형으로 작성되었습니다. 함수형 프로그래밍은 선언형으로 코드를 작성합니다. 이는 어떻게 하는지에 대한 정의(how)가 아니라 무엇을 할 것인지 정의(what)합니다.
함수형 프로그램밍으로 구현하기
각 코드를 함수로 구현하여 프로그램을 선언형으로 작성할 수 있습니다.
function move_cars() {
var carsCount = car_positions.length;
for(var i=0; i<carsCount; i++) {
if(Math.random() > 0.3) {
car_positions[i] += 1;
}
}
}
function draw_car(car_position) {
var output_str = '- '.repeat(car_position);
console.log(output_str);
}
function run_step_of_race() {
this.time -= 1;
move_cars();
}
function draw() {
console.log(‘’);
for(var i in car_positions) {
draw_car(car_positions[i]);
}
}
while(time > 0) {
run_step_of_race();
draw();
}
프로그램의 메인 루프는 다음과 같습니다.
“남은 시간이 있다면 레이스를 실행하고 출력합니다. 그리고 시간을 다시 확인합니다.” 각 레이스의 단계에 대해 더 자세히 알고 싶다면 구현된 함수를 읽으면 됩니다.
코드 자체가 내용을 설명하고 있기 때문에 주석을 추가하지 않아도 됩니다.
코드를 함수로 나누는 것은 코드를 더 읽기 쉽게 만들기 때문에 고민이 필요하지 않습니다.
위 코드는 함수를 사용하지만 외부 데이터(변수)에 의존적이기 때문에 함수형 프로그래밍의 조건을 만족하지 않습니다. 코드의 함수는 상태 값을 매개변수로 전달받아 사용하지 않습니다. 이러한 방식은 외부 변수의 변화에 의해 함수의 결괏값이 달라질 수 있습니다. 각 함수가 실제로 무엇을 하는지 확인하기 위해 정독이 필요합니다. 함수 내에서 외부 변수를 사용한다면 변수의 출처를 찾고 다른 함수에서 해당 변수를 변경하는지 확인해야 합니다.
상태 제거하기
자동차 레이스의 함수형 프로그래밍 버전은 다음과 같습니다.
function move_cars(car_positions) {
return car_positions.map(function(item) {
if(Math.random() > 0.3) {
item += 1;
}
return item;
});
}
function draw_car(car_position) {
var output_str = ‘- ‘.repeat(car_position);
console.log(output_str);
}
function run_step_of_race(state) {
state[‘time’] -= 1;
state[‘car_positions’] = move_cars(state[‘car_positions’]);
return state;
}
function draw(state) {
console.log(‘’);
state[‘car_positions’].map(function(item) {
draw_car(item);
return item;
});
}
function race(state) {
draw(state);
if(state[‘time’] > 0) {
state = run_step_of_race(state);
race(state);
}
}
race({
‘time’: 5,
‘car_positions’: [1, 1, 1]
});
위 코드는 함수형 프로그래밍으로서 다음과 같은 특징을 갖습니다.
- 공유 변수를 사용하지 않습니다. time과 car_positions는 race()로 전달됩니다.
- 함수는 매개 변수를 사용합니다.
- 함수 내에서 변수가 인스턴스화 되지 않습니다. 모든 데이터는 반환 값을 통해서 변경됩니다. race()는 run_step_of_race()의 결과와 함께 반복됩니다. 각 단계마다 새로운 상태가 생성되어 다음 단계로 전달됩니다.
파이프라인을 사용하십시오.
이전 섹션에서는 일부 명령형 반복문이 보조 함수를 호출하는 재귀 형태로 사용되었습니다. (역주 : race 함수 내에서 race를 다시 호출하는 재귀형 구조입니다.) 이번에는 파이프라인 기법을 활용하여 명령형 반복문을 다른 형태로 다시 작성해보겠습니다.
아래의 반복문은 밴드의 이름, 출신을 수정하는 역할을 수행합니다.
(역주 : 출신을 ‘Canada’로 수정하고 이름은 단어 단위로 제일 앞 문자를 대문자로 수정합니다.)
var bands = [
{‘name’: ‘sunset rubdown’, ‘country’: ‘UK’, ‘active’: false},
{‘name’: ‘women’, ‘country’: ‘Germany’, ‘active’: false},
{‘name’: ‘a silver mt. zion’, ‘country’: ‘Spain’, ‘active’: true}
];
function format_bands(bands) {
for (var i in bands) {
bands[i][‘country’] = ‘Canada’;
var name = bands[i][‘name’];
name = name.replace(‘.’, ‘’);
var nameParts = name.split(‘ ‘);
for(var j in nameParts) {
nameParts[j] = nameParts[j].charAt(0).toUpperCase() + nameParts[j].slice(1);
}
bands[i][‘name’] = nameParts.join(“ “);
}
}
format_bands(bands);
//print bands
console.log(JSON.stringify(bands));
위 코드 함수에서 ‘format’의 의미는 매우 애매모호합니다. 코드를 살펴보면 동일 반복문 안에서 세 가지 작업이 수행됩니다.
- ‘country’ 키를 ‘Canada’로 설정합니다.
- 구두점(.)을 밴드 이름에서 제거합니다.
- 밴드 이름을 대문자로 변환합니다.
이는 코드가 의도한 바를 파악하기 어렵고 코드가 수행되는 상태를 파악하기 어렵습니다. 이러한 코드는 재사용하기 어렵고 테스트나 병렬 화도 어렵습니다.
다음과 비교해 보겠습니다.
pipeline_each(
bands,
[set_canada_as_country, strip_punctuation_from_name, capitalize_names]
)
위 코드는 이해하기 더 쉽습니다. 각 기능이 서로 연관되어 있는 것처럼 보입니다. 이전 함수의 출력 결과는 다음 함수의 출력 결과로 입력됩니다. 각 함수가 함수형 프로그램의 특성을 갖고 있다면 재사용, 테스트, 병렬화가 용이합니다.
pipeline_each의 역할은 밴드를 set_canada_as_country()와 같은 변환 함수로 한 번에 하나씩 전달하는 것입니다. 함수가 모든 밴드에 적용된 후 pipeline_each는 변환된 밴드를 다음 함수로 전달합니다.
변환 함수를 살펴보겠습니다.
var set_canada_as_country = function set_canada_as_country(band) {
band['country'] = "Canada";
return band;
}
var strip_punctuation_from_name = function strip_punctuation_from_name(band) {
band['name'] = band['name'].replace('.', '');
return band;
}
var capitalize_names = function capitalize_names(band) {
var nameParts = band['name'].split(‘ ‘);
for(var j in nameParts) {
nameParts[j] = nameParts[j].charAt(0).toUpperCase() + nameParts[j].slice(1);
}
band['name'] = nameParts.join(“ “);
return band;
}
pipeline_each 구현은 다음과 같습니다.
function pipeline_each(data, functions) {
return functions.reduce(
function(newData, currentFunction) {
return newData.map(function(item) {
return currentFunction.call(this, item);
});
},
data
);
}
bands = pipeline_each(
bands,
[set_canada_as_country, strip_punctuation_from_name, capitalize_names]
);
console.log(JSON.stringify(bands));
Conclusion
함수형 프로그래밍은 ‘어떻게(how)’가 아니라 ‘무엇(what)’을 정의합니다. 이는 코드를 깔끔하게 추상화하여 나중에 쉽게 최적화할 수 있는 장점이 있습니다.
'Javascript' 카테고리의 다른 글
타입스크립트(Typescript) 내용 정리 (0) | 2019.12.05 |
---|---|
[javascript ] javascript 로 form이나 element 값 얻어오거나 설정하기 (0) | 2018.07.07 |
javascript에서 ajax로 외부 도메인 요청 시 Access-Control-Allow-Origin 에러가 발생하는 경우 해결법 (0) | 2016.11.25 |
javascript로 타이머 구현하기 (0) | 2015.05.03 |