함수형 프로그래밍(FP) - 함수형 프로그래밍으로 개발

Yuno·2023년 5월 19일
2
post-thumbnail

머릿말

이전 포스팅에서 FP로 개발한 코드가 어떻게 다른가에 대해 살펴보았습니다.
예제가 궁금하다면 이전포스팅을 참고해주세요

이번 포스팅에선, 함수형 프로그래밍으로 개발한 함수를 직접 만들어보고 쓰임에 대해 알아보겠습니다.

함수 개발 규칙

FP 시리즈의 가장 첫 시간에, 객체지향과의 가장 큰 차이에 대해 얘기했습니다.

// 객체지향
arr.map(v => v*v)

// 함수형
map(arr, v=> v*v)

1번은 Array 객체의 메소드인 map을 활용한 객체지향 코드이고,
2번은 map 함수를 이용한 함수형 프로그래밍 코드입니다.

함수형 프로그래밍의 함수는 데이터를 받아 처리하도록 작성하고,
그 함수에 맞는 데이터를 넣어주면서 프로그래밍 합니다.

더 간단하게 함수가 먼저나오면 함수형, 객체가 먼저나오면 객체지향이 되겠네요.

함수형 함수를 개발하는 이유

원하는 시점에서 함수를 호출해 실행할 수 있기 때문에,
자주 사용하는 동작( 어디서 반복하고, 어디에 할당하고 등 )을 함수로 그런 로직에 신경을 쓰지않고
과제, 요구사항에 더 집중하도록 도와줍니다.

each

자 이제 첫 함수를 직접 개발해봅니다.
each는 데이터를 반복시키는 함수입니다.

Array 메소드에 익숙하다면, Array.prototype.forEach를 생각하면 됩니다.
함수로써 직접 구현은 어렵지 않습니다.

const each = (data, iter) => {
  for (let i = 0; i<data.length; i++) {
  	iter(data, i);
  }
}


// 사용 예제
each([1,2,3], v=> console.log(v*2)

메서드가 this를 사용하는 대신, 함수가 특정 데이터를 받아 처리하는 식으로 변경된것을 제외하면,
늘 사용하던 forEach 메소드와 똑같이 사용할 수 있게 되었습니다.

다형성

그럼 forEach보다 뭐가 좋을까요?

위에 구현한 each함수를 보면 인자 명을 array가 아닌 data로 작성하였습니다.
(혹은, src도 좋음)
이것에는 이유가 있습니다. array 뿐아니라 다양한 데이터를 받을 수 있기 때문인데요

직접 만든 each함수는 배열만을 순회하는게 아니라, key값이 있는 모든 데이터를 순회할겁니다.
만약 key가 없다면 빈배열로 처리하여 에러가 발생하지 않도록 합니다.

데이터의 key값을 구하기 위한 keys함수를 만듭니다.
key는 object 타입에 대하여 존재합니다. (array역시 특정 key를 가진 object입니다)

const isObj = (data) => typeof data === 'object';
const keys = (data) => isObj(data)? Object.keys(data): [];

이를통해 keys는 어떤 데이터가 와도 배열임을 보장하게 되었습니다.
each는 이제 key배열을 반복하여 값을 얻고 함수를 호출하면 됩니다.

const each = (data, iter) => {
  const ks = keys(data);
  
  for (let i = 0; i < ks.length; i++) {
    const key = keys[i]
  	iter(data[key], key);
  }
}

이렇게 하면 어떤 데이터 - 원시값, 배열, 객체 등 -가 인자로 넘어와도 처리할 수 있습니다.

{name: 'yuno', age: 26}

이런 객체의 each를 돌려도 yuno, 26 이라는 value가 나올겁니다.
모든 데이터에대해 처리할 수 있도록 다형성을 가지게 되었습니다.

함수형 함수는 이처럼, 다형성을 가지도록 만들고 여러 데이터에 대해 중복되는 로직을 감추고 요구사항을 구현하는 로직에 집중하게 합니다.

map, filter

map, filter 역시 배열을 사용하면서 가장 많이 사용하는 메서드중 하나입니다.
each와 마찬가지로 기존 메서드와 똑같이 동작하지만, 다형성을 가지도록 합니다.

추가로, 작은 단위의 함수를 조합하여 앞으로 구현할 여러 기능과 또 다른 함수를 만들 수 있습니다.

const map = (data, iter) => {
	const result = [];
  	each(data, (v) => result.push(iter(v))); 
  	return result;
}

const filte = (data, predi) => {
  	const result = [];
  	each(data, v => if(predi(v)) result.push(v);
  	return result;
}

미리 만든 each함수를 활용했기에, 3줄로 구현할 수 있었습니다.

또, 반복에 대한 부분은 each 함수가 담당하기 때문에
map과 filter에서는 반복에 대해 신경쓰지않고, 구현하는 로직에 대해 신경쓸 수 있었습니다.

reduce

reduce는 집계함수입니다. 특정 데이터를 순회하면서, 원하는 집계값을 만들어 낼 때 사용합니다.
순회하려는 데이터를 접어서 하나의 원하는 데이터로 만들기 위한 함수라고 생각하면 됩니다.

const reduce = (data, iter, init) => {
  each((v) => (init = iter(init, v)), data);
  return init;
})

init으로 시작하여, 역시 each를 통해 data를 순회하고,
iter 함수에서 반환된 값을 init에 누적시켜서 반환합니다.

이렇게 사용할 수 있습니다.

console.log(reduce([1,2,3], (a,b) => a+b, 0); // 6

each를 사용하기에 reduce또한 다형성을 가질 수 있습니다.
이처럼 만들어 놓은 each가 keys 다형성과 반복에 대해 처리하므로 조합하면서 유용하게 사용되고 있습니다.

curring

커링은 함수형 프로그래밍의 성질과 잘 맞으며, 함수형 프로그램이에서 자주 등장하는 개념입니다.

커링 작업을 하는 curry 함수는 함수에 약간의 변화를 가한 결과를 내는 함수입니다.
이는, 함수를 인자로 받고, 함수를 리턴한다는 뜻입니다.

코드가 헷갈릴 수 있으나, 인자로 함수를 받고 그 함수에 변화를 가한 함수를 리턴한다는걸 생각하고 본다면 이해할 수 있을겁니다.

const curry = (f) => {
	const cnt = f.length;
  	return (...a1) = a1.length >= f.length? f(...a1): (...a2) => f(...a1, ...a2)
}

인자를 늘어뜨려 함수에 넣어줘야해서 스프레드가 많이나오지만 간단한 함수입니다.

  • curry함수는 어떤 함수를 인자로 받습니다.
  • cnt는 인자로 받은 함수의 args(인자)의 개수입니다.
  • 그리고, 받을 인자들을 a1에 담는 함수를 반환합니다.
  • 함수가 반환되고, 그 함수가 함수 f의 인자를 충족하는 개수의 인자를
    • 받으면: 즉시 f 함수를 호출합니다.
    • 못받으면: 다음 인자를 받을 함수를 다시 반환합니다.

즉, 변형된 함수를 리턴하는데 이 함수는 받는 인자의 개수에 따라 함수를 호출할지 미룰지 결정하게 할 수 있습니다.

curry는 이러한 기능을 추가하는 wrapper 함수로 써 동작할 수 있습니다.

간단한 예제를 알아봅니다.

// 평범한 add 함수에 curry를 씌웁니다.
const add = curry((a,b) => a+b);

add(1,2) // 3

const add5 = add(5) // fn
add5(10) // 15

커링된 add 함수에 인자를 2개를 넣는지, 1개를 넣는지에 따라 반환하는 값이 달라집니다.
비교하며 감이 오나요?

추가로, 먼저 받은 부족한 인자를 함수의 뒤쪽에 넣는 curryr도 만들어볼 수 있습니다.

const curryr = (f) => {
	const cnt = f.length;
  	return (...a1) = a1.length >= f.length? f(...a1): (...a2) => f(...a2, ...a1)
}

반환하는 함수가 호출될 때, 먼저받은 인자를 뒤에 넣도록 수정하였습니다.

pipeline

여러 함수와 curry를 이용하면 굉장히 유용하고 아름다운 pipeline을 사용할 수 있습니다.
앞서 소개한 map, filter, reduce 함수에 curryr를 감싸 봅시다.

const map = curryr((data, iter) => {
	const result = [];
  	each(data, (v) => result.push(iter(v))); 
  	return result;
})

const filter = curryr(
	// filter fn 내용...
)

const reduce = curryr(
	// reduce fn 내용...
)

함수들을 인자로하여, 그 함수들을 순차적으로 실행해주는 함수를 반환하는 pipe입니다.

const pipe =
  (...fs) =>
  (data) =>
    reduce(fs, (v, f) => f(v), data);

어려운 사람들을 위한

  • 함수들을 인자로 받아 함수를 리턴합니다.
  • 리턴하는 함수는 데이터를 인자로 받은 함수에 순차 실행해주는 함수입니다.
    • 리듀스에서 f(v)값을 계속 반환하여, f(v)를 다음 함수의 인자로 넣어줍니다.

pipe를 즉시 실행하는 go입니다.

const go = (data, ...fs) => pipe(...fs)(data);

예제를 작성하며 파이프라인을 어떻게 하는지 살펴봅니다.

// users를 필터, 변형, 집계하여 출력합니다. 
go(
  users,
  filter((u) => u.age > 35),
  map(u => u.age),
  reduce(add),
  console.log
);

// name을 key로 만들고, 스토리지에서 가져와 파싱합니다.
const v = go(
  name,
  nameToKey,
  window.sessionStorage.getItem,
  JSON.parse
);

filter, map, reduce에 인자를 넣지만 1개로는 실행되지 않고, iter함수를 클로저가 유지하며
함수가 반환됩니다.

go 파이프라인 함수를 통해 데이터와 함수의 나열로 로직에 따른 순차적인 개발이 가능해 졌습니다.

유용한 함수

인자에 따라서 결과값을 내는 map, filter, reduce와 같은 함수를 만들었는데,
이외에도 위 기본함수를 기반으로 하는 여러 함수가 존재한다.

  • map을 변형한 수집: pluck 등
  • filter를 변형한 거르기: reject, where 등
  • reduce를 변형한 집계: group_by, min 등
  • find를 변형한 탐색: find_index, some 등

유용한 함수 몇가지를 소개하겠습니다.

group_by

export const group_by = curryr((data, iter) =>
  reduce(
    (grouped, v) => {
      const key = iter(v);
      return { ...grouped, [key]: [...(grouped[key] ?? []), v] };
    },
    data,
    {}
  )
);

iter 함수의 반환값을 기준으로 데이터를 집계해주는 함수입니다.
다음과 같이 호출합니다.

export const users = [
  { id: 1, name: "YH", age: 26, work: 2 },
  { id: 2, name: "SS", age: 26, work: 1 },
  { id: 5, name: "WS", age: 34, work: 1 },
  { id: 6, name: "HA", age: 15, work: 1 },
  { id: 9, name: "BABY", age: 15, work: 0 },
];

group_by(users, user => user.age);

age를 기준으로 집계하며 이렇게 결과가 나옵니다.

age인 15, 26 ,34를 키값으로 해당하는 value가 담긴 배열이 집계되어 나옵니다.

where

const _where = curryr(( data, condition) =>
  filter(data, (v) => every((c, key) => v[key] === c, condition))
);

필터를 변형한 where 함수는 condition에 원하는 조건의 객체를 담아,
적합한 데이터만 남기는 함수입니다.

다음처럼 실행합니다.

where({ age: 26, work: 1 }, users2)

이처럼 결과가 나옵니다.

활용 가능성

함수형 함수를 직접 개발하며 사용해본 것 처럼, 일급함수 순수함수와 같은 특징을 활용해
함수를 가지고 있다가 원하는 시점에, 원하는 곳에서 함수를 호출합니다.

덕분에, 파이프라인과 같은 유용한 개발이 가능해집니다.

원하는 시점에 호출할 수 있다는 장점을 가지고, 비동기 함수의 동기적 처리와,
지연평가, 동시성 같은 로직에 유용하게 사용할 수 있습니다.

마치며

함수형 함수를 직접 구현하고, 어떻게 로직을 작성하며 어떤 강점을가지는지 알아보았습니다.
앞으로, 실전 예시와 함께 함수형 함수로 어떻게 개발할 수 있는지에 대해 알아보도록 하겠습니다.

profile
web frontend developer

0개의 댓글