Functional Programming(함수형 프로그래밍)

개요

성공적인 프로그래밍?

모든 프로그래밍 패러다임은 성공적인 프로그래밍을 위해 존재한다. 성공적인 프로그래밍은 좋은 프로그램을 만드는 일이다. 좋은 프로그램은 사용성, 성능, 확장성, 기획 변경에 대한 대응력 등이 좋다. 이것들을 효율적이고 생산적으로 이루는 일이 성공적인 프로그래밍이다.

함수형 프로그래밍

함수형 프로그래밍은 성공적인 프로그래밍을 위해 부수 효과(side-effect)를 최소화하고 조합성을 강조하는 프로그래밍 패러다임이다.
⇒ 순수함수를 통해 부수 효과를 줄임으로써 오류를 줄이고 안정성을 높인다.
⇒ 모듈화 수준을 높임을 통해서 생산성을 높인다.
⇒ 함수형 사고방식은 문제의 해결 방법을 동사(함수)들로 구성(조합)하는 것

순수함수

순수 함수는 수학의 함수를 프로그래밍의 세계로 가져온 모델이다. 프로그래밍의 세계에는 무언가를 저장하고 변경하고 불러올 수 있는 상태라는 개념이 존재하지만, 수학의 세계에는 그런 개념이 없기 때문에 모든 함수는 함수 외부의 무언가에 절대 영향을 받지 않고 독립적으로 존재한다. 때문에 순수함수는 항상 동일한 함수를 반환한다. 항상 동일한 결과를 만들기 위해서 부수효과를 발생시킬 수 있는 요인이 없는 상태의 함수를 의미한다. 외부 변수를 포함하거나 외부의 상태를 변화시키지 않는(부수 효과를 발생시키지 않는) 함수이다.
function add (a, b) { // pure function return a + b; } var obj1 = { val: 10 }; function add2 (obj, x) { // not pure obj.val += b; } add2(obj1, 2); var obj2 = { val: 20 }; function add3 (obj, x) { // pure function return { val: obj.val + b }; }
JavaScript
복사
순수함수가 아닌 함수의 경우는 함수를 호출하는 시점에 따라서 결과가 다르기 때문에 시점을 파악하는 것이 중요하다. 예를들어 add2 라는 함수에서는 obj 의 상태에 따라서 결과가 다르기 때문에 obj의 상태를 개발자가 파악하는 것이 중요해진다. 이는 호출 시점 뿐만이 아니라 개발 시점에서도 마찬가지다.
반면 순수함수는 주어진 함수의 인자에 따라 항상 동일한 결과를 반환하기 때문에 다른 부수 효과에 집중하지 않고 함수의 로직 그 자체에만 집중할 수 있기 때문에 부수 효과를 걱정하며 사용하지 않아도 된다.

일급 객체

쉽게 말하면 함수를 값으로 다룰 수 있다는 의미이다. (변수에 담을 수 있고, 인자로 넘길 수 있거나, 변수, 인자를 통해 받은 함수를 또한 실행할 수 있다.) 자바스크립트에서는 함수는 일급 함수다.
function add_maker(a) { return function(b) { // closure return a + b; }; } const add10 = add_maker(10); console.log(add10(20)) .// 30
JavaScript
복사

중요 규칙

함수 외부의 변수에 접근, 재할당해서는 안된다
함수의 인자를 변경하면 안 된다

객체지향프로그래밍 vs 함수형프로그래밍

duck.moveLeft(); duck.moveRight(); dog.moveLeft(); dog.moveRight(); moveLeft(dog); moveRight(duck); moveLeft({x:5,y:2}); moveRight(dog);
JavaScript
복사
var users = [ { id: 10, name: 'ID', age: 36 }, { id: 20, name: 'BJ', age: 32 }, { id: 30, name: 'JM', age: 32 }, { id: 40, name: 'PJ', age: 27 }, { id: 50, name: 'HA', age: 25 }, { id: 60, name: 'JE', age: 26 }, { id: 70, name: 'JI', age: 31 }, { id: 80, name: 'MP', age: 23 }, { id: 90, name: 'FP', age: 13 }, ];
JavaScript
복사

다형성

메소드는 함수가 아니다 ⇒ 객체에 종속적이다. 해당 클래스의 인스턴스에서만 사용 가능하다. ⇒ 다형성을 지원하기 어렵다
Javascript 에서는 array 만 존재하는 것이 아니라 array like 객체가 존재한다 (ex - jquery 검색 결과)
함수가 먼저 만들어 진 후 데이터가 나옴. 데이터가 존재하지 않더라도 함수는 존재한다? 평가 시점이 더 유연해진다.
외부의 다형성은 함수가 책임지지만(ex - filter) 내부의 다형성은 보조함수 (iterate)가 책임진다

커링 (curry)

_curry

function _curry (fn) { return function (a) { return function (b) { return fn(a, b); } } } function _curry (fn) { return function (a, b) { return arguments.length === 2 ? fn (a, b) : function (b) { return fn(a,b); }; } } function add = _curry(function (a, b) { return a + b; }); add(5)(3); // 8 const add10 = add(10); add10(20); // 30 add10(10); // 20
JavaScript
복사
본체함수를 인자로 받고 최종적으로 본체함수의 실행을 지연 실행한다

_curryR

function _curryR (fn) { return function (a, b) { return arguments.length === 2 ? fn (a, b) : function (b) { return fn(b, a); }; } } var _sub = _curryR(function (a, b) { return a - b; }) console.log(sub(10, 5)) // 5 var sub10 = sub(10); console.log(sub10(5)); // -5
JavaScript
복사

_get

function _get (obj, key) { return obj === null ? undefined : obj[key]; } users[10].name // error _get(users[10], 'name') // undefined var _getR = _curryR(_get); _getR('name')(users[1]); var getName = _getR('name'); getName(users[1]);
JavaScript
복사

_each 함수

function _each (list, iter) { for(var i = 0; i < list.length; i++) { iter(list[i]); } } ////// function _each (list, iter) { var keys = _keys(list); for(var i = 0; i < keys.length; i++) { iter(list[keys[i]], keys[i]); } }
JavaScript
복사

_map함수

function _map (list, mapper) { var newList = []; _each(list, function (val, key) { newList.push(mapper(val, key)); }); return newList; }
JavaScript
복사

_filter

function _filter (list, predi) { var newList = []; _each(list, function (val, key) { if (predi(val, key)) newList.push(val); }); return newList; }
JavaScript
복사

_reduce 함수

function _reduce (list, iter, memo) { if(memo === undefined){ memo = list[0]; for(let i = 1; i < list.length; i++){ memo = iter(memo, list[i]); } } else { for(let i = 0; i < list.length; i++){ memo = iter(memo, list[i]); } } return memo; }
JavaScript
복사

_pipe 함수

좀 더 추상화된 reduce 함수
function _pipe() { var fns = arguments; return function (arg) { return _reduce(fns, function (memo, fn) { return fn(memo); }, arg); } } var f1 = _pipe( function (a) { return a + 1; }, function (a) { return a * 2 }, ); console.log( f1(1) ); // 4
JavaScript
복사

_go 함수

바로 실행하는 pipe 함수
function _go (arg) { var fns = _rest(arguments); return _pipe.apply(null, fns)(arg); } _go(1, function (a) { return a + 1; }, function (a) { return a * 2 }, function (a) { return a * a; }, console.log ); // 16
JavaScript
복사
// 응용 console.log( _map( _filter(users, function (user) { return user.age < 30 }), _get('age') ) ); => _go(users, function (users) { return _filter(users, function (user) { return user.age < 30 }); }, function (users) { return _map(users, _get('age')); }, console.log )
JavaScript
복사

다형성 및 에러

_each 수정

// 기존 each 에는 null 을 넣으면 에러 발생. null, undefined 를 넣어도 에러가 나지 않도록 만들기 var _length = _get('length'); // 커링된 함수 function _each(list, iter) { for(var i = 0, len = _length(list); i < len; i++) { iter(list[i]); } } // each 를 수정함으로써 map, filter 에 null, undefined 에 대해서 에러를 내지 않고 실행 가능함. // map, filter 등이 each를 사용하기 때문.
JavaScript
복사

_keys

// Object.keys 함수는 null 넣으면 에러가 발생 ex - Object.keys(null) - error // _keys 함수를 만들자 function _is_object(obj) { return typeof obj === 'object' && !!obj; } function _keys(obj) { return _is_object(obj) ? Object.keys(obj) : []; }
JavaScript
복사

_each 함수 외부 다형성 높이기

// _each 에 object 타입을 넣어도 순차적으로 실행 가능하도록 function _each(list, iter) { var keys = _keys(list); for(var i = 0, len = keys.length; i < len; i++) { iter(list[keys[i]]); } }
JavaScript
복사

컬렉션 중심 프로그래밍의 4가지 유형과 함수

수집하기

map, values, pluck 등

_values

function _values (data) { return _map(data, _identity) } console.log(_keys(users[0])); console.log(_values(users[0])); // with curryR var _mapr = _curryR(_map); console.log(_mapr(_identity)(users[0])) // _values 와 동일한 결과 반환 // var _values = _mapr(_identity); // 이렇게도 만들 수 있음
JavaScript
복사

_identity

function _identity (val) { return val; }
JavaScript
복사

_pluck

function _plcuk (data, key) { return _.map(data, function (obj) { return obj[key]; }); } console.log(_pluck(users, 'ages')) // [32, 33, 33, .....]
JavaScript
복사

거르기

filter, reject, compact, without 등

_reject

// filter 의 반대 function _reject (data, predi) { return _filter(data, function (val) { return !predi(val); }); } function _negate(func) { return function (val) { return !func(val); } } function _reject (data, predi) { return _filter(data, _negate(predi)); }
JavaScript
복사

_compact

var _filterR = _curryR(_filter); var _compact = _filterR(_identity); // curryR 이 적용된 filter
JavaScript
복사

찾아내기

find, some, every 등

_find

function _find (list, predi) { var keys = _keys(list); for(var i = 0; i < keys.length; i++) { var val = list[keys[i]]; if (predi(val)) return list[keys[i]]; } }
JavaScript
복사

_findIndex

function _findIndex (list, predi) { var keys = _keys(list); for(var i = 0; i < keys.length; i++) { if (predi(list[keys[i])]) return i; } return -1; }
JavaScript
복사

_some

function _some(list, predi) { return (_findIndex(list, predi || _identity) !== -1); } // 만약 predi 함수가 안들어오면 identity 함수를 자동으로
JavaScript
복사

_every

function _every(list, predi) { return _findIndex(data, _negate(predi || _identity)) === -1; }
JavaScript
복사

접기

reduce, min, max, group_by, count_by 등

_min

function _min (data) { return _reduce (data, function (a, b) { return a < b ? a : b; }); }
JavaScript
복사

_max

function _max (data) { return _reduce (data, function (a, b) { return a > b ? a : b; }); }
JavaScript
복사

_minBy

function _minBy (data, iter) { return _reduce (data, function (a, b) { return iter(a) < iter(b) ? a : b; }); }
JavaScript
복사

_maxBy

function _maxBy (data, iter) { return _reduce (data, function (a, b) { return iter(a) > iter(b) ? a : b; }); } _go(users, _filterR(user => user.age >= 30), _mapR(_get('age')), _minR, console.log, ) _go(users, _rejectR(user => user.age >= 30), _maxBy(_get('age')), _get('name'), console.log, )
JavaScript
복사

_groupBy

function _groupBy(data, iter) { return _reduce (data, function (grouped, val) { var key = iter(val); (grouped[key] = grouped[key] || []).push(val); return grouped; }, {}) } _go( users, (user) => user.age - (user.age % 10), console.log )
JavaScript
복사

_countBy

function _countBy (data, iter) { return _reduce(data, function (count, val) { var key = iter(val); count[key] = count[key]++ : 1; return count; }); }
JavaScript
복사

기타 함수

_isObject

function _isObject(obj) { return typeof obj === 'object' && !!obj; }
JavaScript
복사

_first

function _first(list) { return _get(list, 0); }
JavaScript
복사

_pairs

function _pairs (data) { return _.map(data, (val, key) => [key, val]); }
JavaScript
복사

지연평가

지연평가 시작시ㅣ고 유지시키는 함수 1. map 2. filter, reject 끝을 내는 함수 1. take 2. some, every, find
var mi = 0; var fi = 0; _.go( _.range(100) _.map((val) => { mi++; return val * val; }), _.filter((val) => { fi++; return val % 2; }), _.take(5), console.log ) console.log(mi, fi) // 100, 100 => _.go( _.range(100) L.map((val) => { mi++; return val * val; }), L.filter((val) => { fi++; return val % 2; }), L.take(5), console.log ) console.log(mi, fi) // 10, 10 // map 에 1개 넣어서 실행 후 다음에 넣어서 filter 에넣고, // take 에 실행, 그 다음 데이터 똑같은 루프를 돌림
JavaScript
복사

비동기

function square (a) { return new Promise(function (resolve) { setTimeout(function () { resolve(a * a); }, 500); }); } console.log(1) square(10).then(function (res) { console.log(2); console.log(res); // 100 }); console.log(3); // 1 => 3 => 2 square(10) // 실행됨. 그러나 Promise 일 때만 실행 가능 .then(square) .then(square) .then(console.log); _.go(square(10), // 자동으로 Promise 를 받으면 지연 실행 square, square, console.log ) var list = [2, 3, 4] new Promise(function (resolve) { (function recur(res) { if (list.length === res.length) return resolve(res); square(list[[res.length]]).then(function (val) { res.push(val); recur(res); }) })([]); }).then(console.log); /////// const then = curry(function (fn, arg) { return arg instanceof Promise ? arg.then(fn) : fn(arg); }); function num(a) { return a; } function numP(a) { return new Promise(function(resolve) { setTimeout(function() { resolve(a); }, 1000); }); } // then 함수를 pipe 안에 사용하면 비동기도 사용 가능할 듯 const then = _curry((f, a) => a instanceof Promise ? a.then(f) : f(a)); const add10 = then(a => a + 10); const add100 = then(a => a + 100); const log = then(console.log); log(add100(add10(num(5)))); // 즉시 115 log(add100(add10(numP(6)))); // 1초 뒤 116
JavaScript
복사

요약

1.
함수를 되도록 작게 만들기
2.
다형성 높은 함수 만들기
3.
상태를 변경하지 않거나 정확히 다루어 부수 효과를 최소화하기 부수효과를 없앨 수는 없지만 부수효과가 최종 결과(dom 변경이라던가 등등)가 되도록, 그 전에는 부수효과가 없도록
4.
동일한 인자를 받으면 항상 동일한 결과를 리턴하는 순수 함수 만들기
5.
복잡한 객체 하나를 인자로 사용하기보다 되도록 일반적인 값 여러 개를 인자로 사용하기
6.
큰 로직을 고차 함수로 만들고 세부 로직을 보조 함수로 완성하기
7.
어느 곳에서든 바로 혹은 미뤄서 실행할 수 있도록 일반 함수이자 순수 함수로 선언하기
8.
모델이나 컬렉션 등의 커스텀 객체보다는 기본 객체를 이용하기
9.
로직의 흐름을 최대한 단방향으로 흐르게 하기

참조