Front-end/JavaScript

함수형 프로그래밍 - 기본적인 패턴

개발자 키우기 2023. 10. 16. 14:56

함수형 프로그래밍에서 자주 사용되는 기본적인 패턴들을 알아보자.

 

우선 초기값인 users 객체를 생성하겠다.

 

var users = [
  { id: 1, name: "ID", age: 36 },
  { id: 2, name: "BJ", age: 32 },
  { id: 3, name: "JM", age: 32 },
  { id: 4, name: "PJ", age: 27 },
  { id: 5, name: "HA", age: 25 },
  { id: 6, name: "JE", age: 26 },
  { id: 7, name: "JI", age: 31 },
  { id: 8, name: "MP", age: 23 },
];

Filtering

 

_filter 함수는 첫 번째 인자(list)로 값 또는 값을 리턴하는 함수를 받고 있고

 

두 번째 인자는 boolen값을 리턴하는 값이나 함수를 받고 있다.

 

new_list를 만들어 초기화 시키고 _each 함수를 사용해 반복문을 돌린다. ( _each에 대한 사항은 아래에서 설명 )

 

if문을 사용해서 predi에 들어온 함수가 필터링을 하여 참이면 true를 반환하여 new_list에 값을 저장한다.

 

_each의 반복문이 끝나면 최종 결과인 new_list를 반환하고 함수가 종료된다.

 

function _filter(list, predi) {
  var new_list = [];
  _each(list, function (val) {
    if (predi(val)) new_list.push(val);
  });
  return new_list;
}

console.log(
  _filter(users, function (user) {
  return user.age >= 30;
  })
);

 

출력 결과

 

[ { id: 1, name: 'ID', age: 36 }, { id: 2, name: 'BJ', age: 32 }, { id: 3, name: 'JM', age: 32 }, { id: 7, name: 'JI', age: 31 } ]

Mapping

 

_map 함수는 첫번째 인자(list)로 값 또는 값을 리턴하는 함수를 받고 있고, 두 번째 인자는 첫 번째 인자를 매핑할 함수를 받고 있다.

 

new_list를 만들어 초기화 시키고 _each 함수를 사용해 반복문을 돌린다. ( _each에 대한 사항은 아래에서 설명 )

 

list를 mapper에 정의한 매핑한 결과값을 new_list에 담고 _each 함수가 끝나면 new_list를 반환한다.

 

function _map(list, mapper) {
  var new_list = [];
  _each(list, function (val) {
  new_list.push(mapper(val));
  });
  return new_list;
}

console.log(
  _map(users, function (user) {
  return user.name;
  })
);

 

출력 결과

 

[ 'ID', 'BJ', 'JM', 'PJ', 'HA', 'JE', 'JI', 'MP' ]

Iterating

 

_each 함수는 첫번째 인자(list)로 값 또는 값을 리턴하는 함수를 받고 있고, 두 번째 인자는 반복해서 무엇을 수행할지 정의한 함수가 온다.

 

for문을 이용하여 첫번째 인자의 크기만큼 반복하고 두 번째 인자로 들어온 함수를 반복하여 수행한 뒤 list를 반환한다.

 

function _each(list, iter) {
  for (var i = 0; i < list.length; i++) {
    iter(list[i]);
  }
  return list;
}

Curring

 

_curryr함수는 인자로 본체 함수를 받고 함수를 리턴한다. 클로저로 인해 함수가 끝나더라도 자유 변수에 fn이 저장되어 사라지지 않는다.

 

이후 인자가 한번에 두 개가 들어오면 본체 함수가 실행되고 인자로 a, b가 순차적으로 들어간다.

 

하지만 인자가 따로 하나씩 들어오면 본체 함수가 실행될 때 인자로 b, a로 역순으로 들어가게 설계되어 있다.

 

sub에 _curryr 함수를 담으면서 fn함수를 정의하여 본체 함수를 담는다.

 

이후 sub에 한 번에 두 개의 인자를 넣거나 따로 두개의 인자를 담아서 원하는 순서대로 값을 변형시켜 적용시킬 수 있다.

 

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));
console.log(sub(10)(5));

 

출력 결과

 

5
-5

Safe Accessor 1

 

_get 함수는 _curryr 함수와 자주 사용되는데 obj와 key 값을 정방향과 반대방향으로 유연성 있게 사용하기 위해서이다.

 

또한 값이 null일 때 오류가 발생하는 것을 방지하기 위해서 null일 경우 undefined를 반환하고

 

null이 아닌 경우 값을 반환하도록 설계되어 있다.

 

var _get = _curryr(function (obj, key) {
  return obj == null ? undefined : obj[key];
});

console.log(_get(users[0], "name"));
console.log(_get(users[10], "name"));

console.log(_get("name")(users[0]));
console.log(_get("name")(users[10]));

 

출력 결과

 

ID
undefined
ID
undefined

Reducing

 

add함수는 아래의 _reduce에 활용하기 위하여 만든 a + b의 간단한 함수이다.

 

slice는 배열 작업을 수행할 수 있는 내장함수인 Arrays.prototype.slice를 사용하여 배열의 일부 요소를 추출하여 

 

새로운 배열을 반환하는 역할을 수행한다.

 

_rest함수는 첫 번째 인자(list)로 값 또는 값을 리턴하는 함수를 받고 있고, 두 번째 인자는 숫자를 받고 있다.

 

Arrays.prototype.slice의 call 함수를 사용해서 list의 값을 두번째 인자로 받은 숫자만큼 데이터를 제외하거나

 

두번째 인자값이 없을 경우 한 개의 데이터만 제외하고 배열을 복사하여 리턴하는 함수이다.

 

마지막 _reduce 함수는 첫 번째 인자(list)로 값 또는 값을 리턴하는 함수를 받고, 두번째 인자는 반복할 함수를

 

받고 있으며, 마지막 인자로 첫번째 인자를 가지고 반복하는 함수의 결괏값을 누적하여 저장할 데이터의 초기값을 받고 있다.

 

함수 안의 내용을 보면 _reduce의 인자로 값이 두 개(list, iter)만 왔을 때 memo(초기값)에 list의 첫 번째 데이터를 저장하고

 

_rest(list)를 수행하여 슬라이스를 통해 첫번째 데이터를 제외한 새로운 배열을 list에 저장한다.

 

이후 _each반복문을 통해 _reduce의 두 번째 인자로 받은 함수를 수행하면서 memo에 저장하고 그 결과를 반환한다.

 

function add(a, b) {
  return a + b;
}

var slice = Array.prototype.slice;
function _rest(list, num) {
  return slice.call(list, num || 1);
}

function _reduce(list, iter, memo) {
  if (arguments.length == 2) {  
    memo = list[0];
    list = _rest(list);
}
  _each(list, function (val) {
    memo = iter(memo, val);
  });
  return memo;
}

console.log(
  _reduce([1,2,3], add, 0)
);
console.log(
  _reduce([1,2,3], add)
);

 

출력 결과

 

6
6

Pipe ( 반환값 o )

 

_pipe 함수는 인자값들은 fns에 저장하여 또 다른 함수를 리턴한다. 그 함수에 인자값을 넣으면 _reduce 함수가 실행된다.

 

f1에 _pipe함수와 인자 3개를 넣으면 f1에 인자를 넣기 전까지 f1은 인자 하나를 대기하는 함수가 된다.

 

f1에 인자값을 넣으면 _reduce가 실행되고 _reduce의 첫 번째 인자로 함수 리스트가 전달되고 두 번째 인자로 iter에 해당하는

 

함수가 들어가서 arg는 memo의 초기값으로 들어가고 fn은 val로 최초에 받은 인자값들의 함수가 들어가서

 

함수가 인자값을 가지로 계산을 수행한 결과를 arg(memo)에 저장을 해나가고 그 결과를 반납한다.

 

function _pipe() {
  var fns = arguments;
  return function (arg) {
    return _reduce(fns, function (arg, fn) {
      return fn(arg);
    }, arg );
  };
}

var f1 = _pipe(
  function (a) {
    return a + 1;
  },
  function (a) {
    return a * 2;
  },
  function (a) {
    return a * a;
  }
);

console.log(f1(1));

 

출력 결과

 

16

go ( 반환값 x )

 

_go함수는 _pipe와 다르게 초기부터 초기값인 arg과 함수들인 arguments를 받는다.

 

fns에 함수들을 저장하고 _pipe.apply를 통하여 실행시키는데 첫번째 인자로 null값을 주었는데 

 

그 이유는 apply의 첫번째 인수는 함수 내부에서 this가 가리킬 객체를 받고 두번째 인수로 함수에 전달할 인수를 배열로 받기 때문이다.

 

파이프에서는 인자가 최종적으로 두 가지가 들어와야 실행하기 때문에 다른 인자로 초기값인 arg를 넘겨준다.

 

이후 위에서 설명한 것과 같이 _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
);

Safe Accessor 2

 

_map과 _filter를 _curryr를 활영하여 인자를 한 번에 두 개를 전달하면 정방향으로 한 개씩 따로 전달하면 역방향으로 동작하도록 설정.

 

이후 이전에는 obj가 null인지 확인하여 null이면 undefined를 반환하게 하였던 것처럼 _get에 "length" 인자를 전달한다.

 

이미 _curryr이 적용된 _get("length")을 사용하여 첫 번째 인자인 key값을 넘겨주고 _each함수 안의 for문에 _length는

 

아직 함수 상태이기 때문에 list인자를 받아 최종적으로 list.length가 완성하여 길이를 리턴하던지 undefined를 반환하게 된다.

 

따라서 아래의 출력 두 개 모두 에러가 뜨지 않고 undefinded를 반환하게 되어 안전한 함수를 완성하게 되었다.

 

var _map = _curryr(_map),
      _filter = _curryr(_filter);
 
var _length = _get("length");

function _each(list, iter) {
  for (var i = 0, len = _length; i < len; i++) {
    iter(list[i]);
  }
  return list;
}

_each(null, console.log);

console.log(
  _map(null, function (val) {
    return val;
  })
);

 

출력 결과

 

[]
[]

Safe Accessor 3

 

이번에는 자주 사용되는 Object.keys 내장 함수를 안전한 함수로 전환해 보겠다.

 

4번째 코드와 같이 Object.keys를 실행하면 오류를 발생시킨다.

 

console.log(Object.keys({ name: "ID", age: 33 }));
console.log(Object.keys([1, 2, 3, 4]));
console.log(Object.keys(10));
//console.log(Object.keys(null));

 

출력 결과

 

[ 'name', 'age' ]
[ '0', '1', '2', '3' ]
[]

 

해결 방안

 

_get과 유사하게 만들면 되는데 _is_object함수는 받은 인자의 타입이 object 인지 또는 !! 불리언 값으로 강제로 형변환하는 기법을

 

사용하여 obj가 객체인 경우 ture를 반환하고 그 외의 경우에는 false를 반환하는 함수이다.

 

_keys함수는 받은 인자의 타입이 object인지 _is_object함수를 사용하여 ture면 object의 키 값을 반환하고

 

아니면 빈 배열을 반환한다. 따라서 마지막 코드에 에러가 발생하지 않고 빈 배열을 반환하게 된다.

 

function _is_object(obj) {
  return typeof obj == "object" && !!obj;
}

function _keys(obj) {
  return _is_object(obj) ? Object.keys(obj) : [];
}

console.log(_keys({ name: "ID", age: 33 }));
console.log(_keys([1, 2, 3, 4]));
console.log(_keys(10));
console.log(_keys(null));

 

출력 결과

 

[ 'name', 'age' ]
[ '0', '1', '2', '3' ]
[]
[]

 

 

_keys를 _each함수에 적용하여 객체의 키 값이 정리되지 않았을때 Object.keys를 사용하여 값을 가져오는 함수를 만들어보자.

 

function _each(list, iter) {
  var keys = _keys(list);
  for (var i = 0, len = keys.length; i < len; i++) {
    iter(list[keys[i]]);
  }
  return list;
}

_each(
   {
    13: "ID",
    19: "HD",
    29: "YD",
  },
  function (name) {
    console.log(name);
 });

 

출력 결과

 

ID
HD
YD