⚡️함수형 프로그래밍 입문
📈 Trend
React Hook
Reactive Programming
Java8+, C++ 11, NET 3.5
💡 Concept
- 선언적 프로그래밍
- 순수함수
- 참조 투명성
- 불변성
🚩Goal
부수효과를 방지하고 상태변이를 감소하기 위해 데이터 제어 흐름과 연산을 함수를 통해 추상화
🤖명령형 코드의 전환
// interactive(명령형)
// 배열에서 length 만큼 홀수만걸러서 제곱한 후 모두 더해서 출력
function test(list, length) {
let i = 0;
let acc = 0;
for(const a of list) {
if(a % 2) {
acc = acc + a * a;
if(++i === length) break;
}
}
console.log(acc);
}분리
합성
선언적 코드
// declaretive(선언적)
go(
list,
filter(v => v % 2),
map(v => v * v),
take(10),
reduce(add),
console.log
)
선언적 코드
- 재사용 가능한 작은 함수로 코드 유연성 증가
- 작은 단위 함수는 테스트하기 용이
- 간결한 코드로 생산성 및 가독성 향상
💸 Benefit
// declaretive(선언적)
go(
list,
filter(v => v % 2),
map(v => v * v),
take(10),
reduce(add),
console.log
)
JS에서의 순수 함수
- No Side-effect
- Same Input, Same Output
- No this
// Bad
let counter = 0;
function increment() {
return ++counter;
}
// Good
const increment = counter => counter + 1;
ex) Math.sin, pow....
고차 함수
- 함수를 인자로 받는 함수
- 함수를 반환하는 함수
함수를 인자로 사용
런타임에 행위를 전달받아서
미리 정해진 제어흐름에 수행
(제어흐름 추상화)
const arr = [1,2,3,4,5];
// normal loop
for( let i = 0 ; i < 10 ; i ++ ) {
console.log(arr[i]);
}
// abstract loop
const forEach = (f, iter) => {
for(const a of iter) {
f(a);
}
}
forEach(arr, console.log);함수를 반환하는 함수
Closure를 활용한 State 저장
const showToggle = (element) => {
let show = false;
return () => {
show = !show;
element.style.display = `${show ? 'block' : 'none'}`
}
};
const h1 = document.querySelector('#heading');
const h1Toggle = showToggle(h1);
button.onclick = h1Toggle;
// or
// showToggle(h1)();Array 고차함수
filter
map
reduce
every
some
forEach
filter
배열에 부분집합을 반환
map
배열에 요소를 사상(mapping)
reduce
리스트에 결과를 축약(결과)
const filter = (f, list) => {
const arr = [];
for(let i = 0 ; i < arr.length ; i ++) {
if(f(arr[i]) arr.push(arr[i]);
}
return arr;
}함수의 다형성 높이기
const filter = (f, list) => {
const arr = [];
for(let i = 0 ; i < arr.length ; i ++) {
if(f(arr[i]) arr.push(arr[i]);
}
return arr;
}함수의 다형성 높이기
const filter = (f, iter) => {
const arr = [];
for(const a of iter) {
if(f(a)) arr.push(a);
}
return arr;
};Lazy Evaluation
// declaretive(선언적)
go(
range(100000000),
filter(v => v % 2),
map(v => v * v),
reduce(add),
take(10),
console.log
)
100000000 loop 배열 생성
filter -> map -> take -> reduce
Lazy Evaluation
"필요한 시점에 호출해서 사용"
const filter = (f, iter) => {
const arr = [];
for(const a of iter) {
if(f(a)) arr.push(a);
}
return arr;
};L.filter = function* (f, iter) => {
for(const a of iter) {
if(f(a)) {
yield a
}
}
};Lazy Evaluation
// declaretive(선언적)
go(
L.range(100000000),
L.filter(v => v % 2),
L.map(v => v * v),
L.reduce(add),
take(10),
console.log
)
전체적으로 10번 호출 -> 성능 향상
호출순서
go
// 함수를 순차적으로 적용해서 축약
const go = (...args) => reduce((a, f) => f(a), args);
go(
10,
a => a + 10,
a => a + 20,
console.log
);
// h(g(f(10)))
/**
go(
10,
f,
g,
h
)
*/
const list = [{ count: 1 }, { count: 5 }, { count: 10 }, { count: 2 }];
go(
list,
list => map(v => v.count, list),
list => filter(v => v < 5, list),
list => reduce(add, list),
console.log
);pipe
// 함수를 파이프라인으로 엮은 새로운 함수를 리턴
// 평가시점을 뒤로 미룸
const pipe = (...fs) => (v) => go(v, ...fs);
const list = [{ count: 1 }, { count: 5 }, { count: 10 }, { count: 2 }];
const sum =
//함수를 미리 조합해놓고
pipe(
list => map(v => v.count, list),
list => filter(v => v < 5, list),
list => reduce(add, list)
);
//호출하는 시점에 list를 넘겨서 평가
sum(list);Currying
f(x, y, z)
g(x)(y)(z)
함수를 재사용, 조합할때 사용
Curry 활용
// 함수를 파이프라인으로 엮은 새로운 함수를 리턴
// 평가시점을 뒤로 미룸
const pipe = (...fs) => (v) => go(v, ...fs);
const list = [{ count: 1 }, { count: 5 }, { count: 10 }, { count: 2 }];
const filter = (f) => function*(iter) { for(const a of iter) if(f(a)) yield a }
const sum =
pipe(
map(v => v.count),
filter(v => v < 5),
reduce(add)
);
sum(list);Curry 함수
// 함수를 파이프라인으로 엮은 새로운 함수를 리턴
// 평가시점을 뒤로 미룸
const pipe = (...fs) => (v) => go(v, ...fs);
const list = [{ count: 1 }, { count: 5 }, { count: 10 }, { count: 2 }];
const filter = curry(function*(f, iter) { for(const a of iter) if(f(a)) yield a });
// filter(f)(iter)
// filter(f, iter)
// const testfilter = filter(f)
// testfilter(iter);
const map = curry(function*(f, iter) { for(const a of iter) yield f(a) });
const sum =
pipe(
map(v => v.count),
filter(v => v < 5),
reduce(add)
);
const sumCount = sum(v => v.count);
sumCount (list);FP Library

Lodash.js
Ramda.js

Ramda.js Example
Validator
Validator UseCase
const userSchema = {
name: [isString, min(3), max(30), isRequired],
email: [isString, pattern(emailPattern), min(10), max(30), isRequired],
age: [isNumber, isRequired]
};
const form = {
name: "aaaa",
email: "test",
age: 33
};
// false
const result = validate(userSchema, form);Validate Functions
const R = require("ramda");
const min = R.curry((a, b) => a < b.length);
const max = R.curry((a, b) => a > b.length);
const isTypeOf = type => val => typeof val === type;
const emailPattern = /^([a-zA-Z0-9])+([\.\+a-zA-Z0-9_-])*@([\+a-zA-Z0-9_-])+(\.[a-zA-Z0-9_-]+)+$/;
const pattern = R.curry((pattern, val) => pattern.test(val));
const isString = isTypeOf("string");
const isNumber = isTypeOf("number");
const isRequired = val => !!val;Validator
const every = R.reduce((a, b) => a && b, true);
const validateFns = val =>
R.pipe(
R.map(testFn => testFn(val)),
every
);
const findInvalidColumn = form =>
R.pipe(
Object.entries,
R.map(([k, v]) => [k, validateFns(form[k])(v)]),
R.filter(([_, isValid]) => !isValid),
R.take(1),
R.flatten,
([k]) => k
);
const validate = R.curry((schema, form) =>
R.pipe(
findInvalidColumn(form),
R.isNil
)(schema)
);Node.js Middleware
Validator 활용
// validate middleware factory
const validateSchema = schema => (req, res, next) => {
if(validate(schema, req.body)) {
next();
}
res.status(422).send({ message: 'validate error'});
}
router.post('/register', validateSchema(userSchema), userController.register);Tip
- Break Point 로 Debugging이 힘드므로 log함수를 만들어 사용
const debug = val => {
console.debug(val);
return val;
};
R.pipe(
filter...,
log,
map...
...
)- 고차함수는 ES7의 Decorator 로도 활용 가능
후기..
- 함수형은 Typescript와는 ....
- 익숙하지 않다면 함수형 스타일을 부분적용