diff --git "a/contents/09.\353\251\224\353\252\250\354\235\264\354\240\234\354\235\264\354\205\230/1.memoize\355\225\250\354\210\230.md" "b/contents/09.\353\251\224\353\252\250\354\235\264\354\240\234\354\235\264\354\205\230/1.memoize\355\225\250\354\210\230.md" new file mode 100644 index 0000000..bc8e249 --- /dev/null +++ "b/contents/09.\353\251\224\353\252\250\354\235\264\354\240\234\354\235\264\354\205\230/1.memoize\355\225\250\354\210\230.md" @@ -0,0 +1,373 @@ +#### [back](../../README.md)    |    write by [sangcho][sangcho] + +# 1. memoize 함수 + +> 이 글은 함수형 자바스크립트 프로그래밍을 참고하여 기록하였습니다. + +
+ +메모이제이션은 컴퓨터 프로그램이 동일한 계산을 반복해야 할 때, 이전에 계산한 값을 메모리에 저장함으로써 동일한 계산의 반복 수행을 제거하여, 프로그램 실행 속도를 빠르게 하는 기술. + +## 1. 메모이제이션 코드로 이해하기 + +#### 1.1 memoize + +```typescript +function memoize(func) { // memoize는 함수를 받는 함수다. + var cache = {}; // 이 객체에 결과를 남겨둘 것이다. + return function(arg) { + if (cache[arg]) { // 이미 동일 인자에 대한 결과가 있으면 리턴 + console.log('캐시로 결과 바로 리턴', arg); + return cache[arg]; + } + console.log('본체 실행', arg); + return cache[arg] = func.apply(this, arguments); // 받아둔 함수를 실행하면서 결과를 cache에 남겨둠 + } +} + +------------------------------------------------------------ +// 실행 +var mult5 = memoize(function(a) { + return a * 5; +}); + +console.log( mult5(1) ); +// 본체 실행 1 +// 5 + +console.log( mult5(2) ); +// 본체 실행 2 +// 10 + +console.log( mult5(1) ); +// 캐시로 결과 바로 리턴 1 +// 5 + +console.log( mult5(1) ); +// 캐시로 결과 바로 리턴 1 +// 5 +``` +[실행코드](https://codesandbox.io/s/hamsuhyeongpeurogeuraeming-9jang-lzh8jz?file=/src/9.1.js) + +
+ +#### 1.2 memoize의 한계 + +```typescript +var add = memoize(function (a, b) { + return a + b; +}); + +console.log(add(3, 5)); +// 본체 실행 3 +// 8 + +console.log(add(3, 10)); +// 캐시로 결과 바로 리턴 3 +// 8 +// 캐시가 동작했지만 3에만 의존하기 때문에 오류 + +var keys = memoize(function (obj) { + return _.keys(obj); +}); + +console.log(keys({ a: 1, b: 2 })); +// 본체 실행 Object {a: 1, b: 2} +// ["a", "b"] + +console.log(keys({ a: 1, b: 2 })); +// 캐시로 결과 바로 리턴 Object {a: 1, b: 2} +// ["a", "b"] + +console.log(keys({ a: 10, b: 20 })); +// 캐시로 결과 바로 리턴 Object {a: 1, b: 2} +// ["a", "b"] +// 잘 동작하는 듯 했지만 cache가 { [object Object]: ... } 이런식으로 되기 때문에 오류 +``` +[실행코드](https://codesandbox.io/s/hamsuhyeongpeurogeuraeming-9jang-lzh8jz?file=/src/9.1.js) + +
+ +## 2. Underscore.js의 _.memoize + +#### 2.1 _.memoize + +```typescript +_.memoize = function (func, hasher) { + // 본체 함수와 hasher 함수를 받음 + var memoize = function (key) { + var cache = memoize.cache; + var address = "" + (hasher ? hasher.apply(this, arguments) : key); + // hasher가 있으면 hasher에게도 인자들을 넘겨 cache의 key로 사용할 address를 생성 + // 없으면 첫 번째 인자를 그대로 사용 + if (!_.has(cache, address)) cache[address] = func.apply(this, arguments); + // (1) 결과가 없을 때만 함수를 실행하여 cache에 담음 + return cache[address]; // 결과 리턴 + }; + memoize.cache = {}; // (2) 클로저를 사용하지 않고 리턴할 함수 자체에 cache를 달아둠 + return memoize; +}; +``` + +- Underscore.js의 memoize는 cache의 key를 함수를 통해 만드는 것이 가능 +- (1)에서 _.has를 사용한 이유는 캐시한 결과 값이 null, 0, undefined일 수 있기 때문. +- (2)에서 함수에 .cache를 쓴 이유는 개발자가 메모리 관리를 할 수 있도록 하기 위함 -> 클로저를 사용할 경우 cache에 담긴 실행 결과들이 메모리에 상주하고 있어야 하기 때문. + +## 3. Partial.js의 _.memoize2 + +#### 3.1 _.memoize2 + +```typescript +var f1 = _.memoize2(function(obj) { + console.log('함수 본체에 들어옴'); + return obj.a + 10; +}); + +var obj1 = { a: 1 }; +var obj2 = { a: 2 }; + +console.log( f1(obj1) ); +// 함수 본체에 들어옴 +// {a:11} +console.log( f1(obj1) ); +// {a:11} (캐시 사용) +console.log( f1(obj1) ); +// {a:11} (캐시 사용) + +console.log( f1(obj2) ); +// 함수 본체에 들어옴 +// {a:12} +console.log( f1(obj2) ); +// {a:12} (캐시 사용) +``` + +- _.memoize가 캐시를 함수에 기록, _.memoize2는 캐시를 인자에 기록 +- _.memoize2는 불변 객체 컨셉과 함께 사용하기 위해 만든 함수 +- 각 함수들에 대한 결과값을 인자로 사용한 사용된 객체에 담아두므로 한 번 사용하고 버리는 객체 -> 메모리에서 비워짐 +- 사용한 인자에 결과 캐시가 쌓이므로 값의 유무에 따라 자동으로 메모리가 관리 + +#### 3.2 mutable + +```typescript +var evens = _.memoize2(function(list) { + console.log('함수 본체에 들어와서 loop 실행'); + return _.filter(list, function(num) { + return num % 2 == 0; + }) +}); + +var list = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; +console.log( evens(list) ); +// 함수 본체에 들어와서 loop 실행 +// [2, 4, 6, 8, 10] +console.log( evens(list) ); +// [2, 4, 6, 8, 10] (캐시를 사용하여 loop를 돌지 않음) + +list.push(11); // mutable +list.push(12); // mutable +console.log( list ); +// [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] + +console.log( evens(list) ); +// [2, 4, 6, 8, 10] (캐시가 사용되어 10이 그대로 남음) +``` + +#### 3.3 immutable + +```typescript +var list2 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; +console.log( evens(list2) ); +// 함수 본체에 들어와서 loop 실행 +// [2, 4, 6, 8, 10] +console.log( evens(list2) ); +// [2, 4, 6, 8, 10] (캐시를 사용하여 loop를 돌지 않음) + +list2 = list2.concat(11, 12); // immutable +console.log( list2 ); +// [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] + +console.log( evens(list2) ); +// 함수 본체에 들어와서 loop 실행 +// [2, 4, 6, 8, 10, 12] + +console.log( evens(list2) ); +// [2, 4, 6, 8, 10, 12] (캐시를 사용하여 loop를 돌지 않음) +``` + +#### 3.4 _.memoize2 with _.go + +```typescript +var users = [ + { id: 1, name: "ID", age: 32, count: { review: 3, cart: 5 } }, + { id: 2, name: "HA", age: 25, count: { review: 8, cart: 4 } }, + { id: 3, name: "BJ", age: 32, count: { review: 0, cart: 0 } }, + { id: 4, name: "PJ", age: 28, count: { review: 4, cart: 5 } }, + { id: 5, name: "JE", age: 27, count: { review: 5, cart: 2 } }, + { id: 6, name: "JM", age: 32, count: { review: 4, cart: 6 } }, + { id: 7, name: "JI", age: 31, count: { review: 7, cart: 2 } } +]; + +var best_reviewers = _.memoize2(function(list) { + console.log('함수 본체에 들어와서 loop 실행'); + return _.filter(list, function(user) { + return user.count.review > 5; + }) +}); + +var cart_is_empty = _.memoize2(function(list) { + console.log('함수 본체에 들어와서 loop 실행'); + return _.filter(list, function(user) { + return user.count.cart == 0; + }) +}); + +// _.go: 즉시 실행되는 pipe 라인 함수 + +_.go(users, + best_reviewers, + _.pluck('name'), + console.log); +// 함수 본체에 들어와서 loop 실행 +// ["HA", "JI"] + +_.go(users, + best_reviewers, + _.pluck('name'), + console.log); +// ["HA", "JI"] (캐시 사용) + +_.go(users, + cart_is_empty, + _.pluck('name'), + console.log); +// 함수 본체에 들어와서 loop 실행 +// ["BJ"] + +_.go(users, + cart_is_empty, + _.pluck('name'), + console.log); +// ["BJ"] (캐시 사용) +``` + +#### 3.5 _.memoize2 with _.im + +```typescript + +// _.im.set: immutable set 함수 +users = _.im.set(users, '(#3)->count', { + review: 10, + cart: 1 +}); +// id가 3에 해당하는 객체 갱신 + +_.go(users, + best_reviewers, + _.pluck('name'), + console.log); +// 함수 본체에 들어와서 loop 실행 +// ["HA", "BJ", "JI"] + +_.go(users, + best_reviewers, + _.pluck('name'), + console.log); +// ["HA", "BJ", "JI"] (캐시 사용) + +_.go(users, + cart_is_empty, + _.pluck('name'), + console.log); +// 함수 본체에 들어와서 loop 실행 +// [] + +_.go(users, + cart_is_empty, + _.pluck('name'), + console.log); +// [] (캐시 사용) +``` + +- user는 중첩 구조 데이터 -> 가변으로 값을 변경하면 위 함수들이 캐시에 남은 값을 리턴하는 문제 발생 +- _.im 을 이용하여 값을 변경 -> 캐시도 함께 갱신 +- _.im 을 이용하여 값을 변경 -> 변경이 되지 않은 객체는 살려둠. + +#### 3.6 _.memoize2 with _.im + predicate + +```typescript + +var predicate = _.memoize2(function(user) { + console.log('predicate 실행'); + return user.count.review > 5; +}); + +var best_reviewers2 = _.memoize2(function(list) { + console.log('함수 본체에 들어와서 loop 실행'); + return _.filter(list, predicate); +}); + +_.go(users, + best_reviewers2, + _.pluck('name'), + console.log); +// 함수 본체에 들어와서 loop 실행 +// predicate 실행 * 7번 실행 +// ["HA", "BJ", "JI"] + +_.go(users, + best_reviewers2, + _.pluck('name'), + console.log); +// ["HA", "BJ", "JI"] (캐시 사용) + +users = _.im.set(users, '(#3)->count->review', 2); +// BJ의 count.review를 다시 줄임 + +_.go(users, + best_reviewers2, + _.pluck('name'), + console.log); +// 함수 본체에 들어와서 loop 실행 +// predicate 실행 * 1번 실행 +// ["HA", "JI"] + +``` + +- users를 새로운 값으로 변경 -> 내부 객체의 BJ만 변경 -> 나머지는 변경하지 않음 -> predicate의 실행 스킵 가능 +- predicate or iteratee 등 반복 실행되는 함수 & 내부가 복잡한 함수에게는 성능적으로 이득 + +## 3. _.momoize2 내부와 JSON.stringify + +```typescript + +_.memoize2 = function(mid) { + return function(func) { + var memoize_id = ++mid; + var f = arguments.length == 1 ? func : __.apply(null, arguments); + return function(obj) { // 함수로 달아 놓음 + return _.has(obj._memoize || (obj._memoize = function(){}), memoize_id) ? + obj._memoize[memoize_id] : (obj._memoize[memoize_id] = f(obj)); + } + } +}(0); +``` +- 필자가 만든 _.memoize2 함수는 인자를 한 개만 받을 수 있음 -> 캐시가 필요한 함수에서 인자가 2개 이상 필요한 경우가 거의 없다고 판단되었기 때문 +- 캐시 객체를 함수로 달아둠 -> 객체에 달린 함수는 JSON.stringify에서 자동으로 지워지기 때문에.....(?) + +--- + +<참고자료> + +[책] [#함수형자바스크립트프로그래밍][함수형자바스크립트프로그래밍] - 유인동 지음 - +
+[사이트] + +--- + +##### 고차함수와 보조함수 end + +[함수형자바스크립트프로그래밍]: https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=123715872 +[sangcho]: https://github.com/SangchoKim +[taeHyen]: https://github.com/rlaxogus0517 +[kangHyen]: https://github.com/bebekh1216 +[sumin]: https://github.com/ttumzzi diff --git "a/contents/09.\353\251\224\353\252\250\354\235\264\354\240\234\354\235\264\354\205\230/2.\353\266\200\353\266\204\354\240\201\354\232\251.md" "b/contents/09.\353\251\224\353\252\250\354\235\264\354\240\234\354\235\264\354\205\230/2.\353\266\200\353\266\204\354\240\201\354\232\251.md" new file mode 100644 index 0000000..74d8b32 --- /dev/null +++ "b/contents/09.\353\251\224\353\252\250\354\235\264\354\240\234\354\235\264\354\205\230/2.\353\266\200\353\266\204\354\240\201\354\232\251.md" @@ -0,0 +1,297 @@ +#### [back](../../README.md)    |    write by [sangcho][sangcho] + +# 2. 부분 적용 + +> 이 글은 함수형 자바스크립트 프로그래밍을 참고하여 기록하였습니다. + +
+ +## 1. _.partial로 함수 만들기 + +#### 1.1 _.partial 사용법 + +```typescript +var pc = _.partial(console.log, 1); +pc(2); +// 결과: 1 2 +// 2 가 오른쪽으로 들어감 +pc(2, 3); +// 결과: 1 2 3 +// 2, 3이 오른쪽으로 들어감 + +var pc = _.partial(console.log, _, 2); +pc(1); +// 결과: 1 2 +// 1이 왼쪽의 _ 자리에 들어감 +pc(1, 3); +// 결과: 1 2 3 +// 1이 왼쪽의 _ 자리에 들어가고 3이 오른쪽으로 들어감 + +var pc = _.partial(console.log, _, _, 3); +pc(1); +// 결과: 1 undefined 3 +// 1이 왼쪽의 _ 자리에 들어가고 두 번째 _는 들어오지 않아 undefined가 됨 +pc(1, 2); +// 결과: 1 2 3 +// 1과 2가 순서대로 _, _를 채움 +pc(1, 2, 4); +// 결과: 1 2 3 4 +// 1과 2가 순서대로 _, _를 채우고 3의 오른쪽으로 4가 들어감 + +var pc = _.partial(console.log, _, 2, _, 4); +pc(1, 3, 5); +// 결과: 1 2 3 4 5 +// 1을 _ 자리에 채우고 2를 넘겨서 _에 3을 채우고 4의 오른쪽에 5가 들어감 + +var pc = _.partial(console.log, _, 2, _, _, 5); +pc(1, 3, 4, 6); +// 결과: 1 2 3 4 5 6 +// 1을 _ 자리에 채우고 2를 넘겨서 _에 3을 채우고 다음 _에 4를 채우고 5의 오른쪽에 6이 들어감 +``` +[실행코드](https://codesandbox.io/s/hamsuhyeongjabaseukeuribteupeurogeuraeming-th30pc?file=/src/4.2.1.js) + +- _.partial 함수를 이용하면 원하는 위치에 인자를 부분적으로 적용할 수 있음. + +
+ +#### 1.2 add_all + +```typescript +var add_all = _.partial(_.reduce, _, function(a, b) { return a + b }); + +add_all([1, 2, 3, 4]); +// 10 + +add_all([5, 2]); +// 7 +``` +[실행코드](https://codesandbox.io/s/hamsuhyeongjabaseukeuribteupeurogeuraeming-th30pc?file=/src/4.2.1.js) + +- _.partial을 이용해 _.reduce와 같은 고차 함수에 미리 보조 함수를 적용해 두는 식으로 해당 함수 구현 가능. + +
+ +## 2. _.partial과 _.compose로 함수 만들기 + +```typescript +_.compose( + console.log, + function (a) { + return a - 2; + }, + function (a) { + return a + 5; + } +)(0); +// console.log <- 5 - 2 <- 0 + 5 <- 0 +// 3 + +var falsy_values = _.compose( + _.partial(_.isEqual, -1), // (1) + _.partial(_.findIndex, _, _.identity) +); // (2) + + console.log( falsy_values([1, true, {}]) ); +// false + console.log(falsy_values([0, 1, false])); +// false + console.log(falsy_values([0, "", false])); + // true + +var some = _.negate(falsy_values); // (3) + + console.log( some([1, true, {}]) ); + // true + console.log( some([0, 1, false]) ); + // true + console.log( some([0, "", false]) ); + // false + +var every = _.compose( + _.partial(_.isEqual, -1), + _.partial(_.findIndex, _, _.negate(_.identity)) +); // (4) + +console.log( every([1, true, {}]) ); +// true +console.log( every([0, 1, false]) ); +// false +console.log( every([0, "", false]) ); +// false +``` +[실행코드](https://codesandbox.io/s/hamsuhyeongjabaseukeuribteupeurogeuraeming-th30pc?file=/src/4.2.2.js) + +- _.compose는 오른쪽의 함수를 실행한 결과를 왼쪽의 함수에게 전달하는 것을 반복하는 고차 함수. +- _.compose는 인자로 함수만 받음. + +
+ +## 3. 더 나은 _.partial 함수 + +#### 3.1 약간은 아쉬운 _.partial 함수 + +```typescript +function add(a, b) { + return a + b; +} + +function sub(a, b) { + return a - b; +} + +function m() { + var iter = arguments[arguments.length - 1]; + arguments.length--; + return _.reduce(arguments, iter); +} + +m(100, 50, add); +// 150 +m(100, 50, 10, add); +// 160 +m(100, 50, 10, 5, add); +// 165 + +m(100, 50, sub); +// 50 +m(100, 50, 10, sub); +// 40 +m(100, 50, 10, 5, sub); +// 35 + +// 동작가능 +---------------------------------------------------- +// 에러 발생 + +var f1 = _.partial(m, _, _, _, add); +// f1은 3개의 인자만 더할 수 있다. + +f1(1, 1, 1); +// 3 +f1(1, 1); +// NaN +f1(1, 1, 1, 1); +// Uncaught TypeError: iteratee is not a function +// _.reduce에 1이 넘어가면서 에러 + +``` +[실행코드](https://codesandbox.io/s/hamsuhyeongjabaseukeuribteupeurogeuraeming-th30pc?file=/src/4.2.3.js) + +- _.partial 함수는 _로 구분하여 인자가 적용될 위치를 지정해 둘 수 있음 -> 인자 개수가 유동적일 때, 함수의 마지막 인자가 유동적일 때 아쉬운 부분이 존재. +- f1 함수처럼 맨 왼쪽의 인자나 맨 왼쪽에서 두 번째 인자를 적용해두는 것은 가능 +- 그러나 맨 오른쪽 인자나 맨 오른쪽에서 두 번째에만 인자를 적용하는 경우는 불가능 + +
+ +#### 3.2 새롭게 보완한 _.partial 함수 + +```typescript +var ___ = {}; +_.partial = function(fn) { + var args1 = [], args3, len = arguments.length, ___idx = len; + for (var i = 1; i < len; i++) { + var arg = arguments[i]; + if (arg == ___ && (___idx = i) && (args3 = [])) continue; + if (i < ___idx) args1.push(arg); + else args3.push(arg); + } + return function() { return fn.apply(this, mergeArgs(args1, arguments, args3)); }; +}; + +function _toUndef(args1, args2, args3) { + if (args2) args1 = args1.concat(args2); + if (args3) args1 = args1.concat(args3); + for (var i = 0, len = args1.length; i < len; i++) if (args1[i] == _) args1[i] = undefined; + return args1; +} + +function mergeArgs(args1, args2, args3) { + if (!args2.length) return args3 ? _toUndef(args1, args3) : _toUndef(args1.slice()); + + var n_args1 = args1.slice(), args2 = _.toArray(args2), i = -1, len = n_args1.length; + while (++i < len) if (n_args1[i] == _) n_args1[i] = args2.shift(); + if (!args3) return _toUndef(n_args1, args2.length ? args2 : undefined); + + var n_arg3 = args3.slice(), i = n_arg3.length; + while (i--) if (n_arg3[i] == _) n_arg3[i] = args2.pop(); + return args2.length ? _toUndef(n_args1, args2, n_arg3) : _toUndef(n_args1, n_arg3); +} +``` +- 새로운 구분자 ___가 추가. +- ___를 기준으로 왼편의 인자들을 왼쪽부터 적용, 오른편의 인자들을 오른쪽부터 적용할 준비를 해 둔 함수를 리턴. +- 부분 적용된 함수를 실행하면, 그때 받은 인자들로 왼쪽과 오른쪽을 먼저 채운 후, 남은 인자들로 가운데 ___ 자리를 채움. + +
+ +#### 3.3 사용법 + +```typescript +var pc = _.partial(console.log, ___, 2, 3); +pc(1); +// 결과: 1 2 3 +// ___ 자리에 1이 들어가고 2, 3은 맨 오른쪽에 들어감 +pc(1, 4, 5, 6); +// 결과: 1 4 5 6 2 3 +// ___ 자리에 1, 4, 5, 6이 들어가고 2, 3은 맨 오른쪽에 들어감 + +var pc = _.partial(console.log, _, 2, ___, 6); +pc(1, 3, 4, 5); +// 결과: 1 2 3 4 5 6 +// _에 1이 들어가고 2를 넘어가고 ___ 자리에 3, 4, 5가 채워지고 6이 맨 오른쪽에 들어감 +pc(1, 3, 4, 5, 7, 8, 9); +// 결과: 1 2 3 4 5 7 8 9 6 +// _에 1이 들어가고 2를 넘어가고 ___ 자리에 3, 4, 5, 7, 8, 9가 채워지고 6이 맨 오른쪽에 들어감 + +var pc = _.partial(console.log, _, 2, ___, 5, _, 7); +pc(1); +// 결과: 1 2 5 undefined 7 +// _ 자리에 1이 들어가고 2와 5사이는 유동적이므로 모이고 5가 들어간 후 _가 undefined로 대체 되고 7이 들어감 +pc(1, 3, 4); +// 결과: 1 2 3 5 4 7 +// _ 자리에 1이 들어가고 2와 5사이에 3이 들어가고 _ 를 4로 채운 후 7이 들어감 +// 왼쪽의 _ 들이 우선 순위가 제일 높고 ___ 보다 오른쪽의 _ 들이 우선순위가 높음 +pc(1, 3, 4, 6, 8); +// 결과: 1 2 3 4 6 5 8 7 +// _ 자리에 1이 들어가고 2와 5사이에 3, 4, 6이 들어가고 _ 를 8로 채운 후 7이 들어감 + +---------------------------------------------------------- + +// 문제가 있었던 m 함수 다시 적용해보기 +var add_all = _.partial(m, ___, add); + +add_all(1, 2, 3, 4); +// 10 +add_all(1, 2, 3, 4, 5); +// 15 + +var sub10 = _.partial(m, ___, 10, sub); + +sub10(50); +// 40 +sub10(50, 20); +// 20 +sub10(50, 20, 10); +// 10 + +``` +- _.partial을 이용하면 인자를 조합하기 위해 함수로 함수를 만드는 경우를 모두 대체 가능. +- 함수를 조립하고, 함수 합성 패턴(_.chain, _.compose, _.pipeline)등과도 잘 어울림. +- 함수에 인자를 미리 적용해 두는 기법은 비동기 상황에서도 효율적임. + +--- + +<참고자료> + +[책] [#함수형자바스크립트프로그래밍][함수형자바스크립트프로그래밍] - 유인동 지음 - +
+[사이트] + +--- + +##### 부분 적용 end + +[함수형자바스크립트프로그래밍]: https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=123715872 +[sangcho]: https://github.com/SangchoKim +[taeHyen]: https://github.com/rlaxogus0517 +[kangHyen]: https://github.com/bebekh1216 +[sumin]: https://github.com/ttumzzi