함수형 프로그래밍 – JavaScript

이 글은 내가 이해하는 자바스크립트 함수형 프로그래밍 방식에 관한 설명이다. 위키에서 함수형 프로그래밍을 이렇게 설명한다.

함수형 프로그래밍은 자료 처리를 수학적 함수의 계산으로 취급하고 상태와 가변 데이터를 멀리하는 프로그래밍 패러다임의 하나이다.”

자바스크립트를 사용하여 이러한 함수형 프로그래밍을 할 수 있다. 함수가 변수처럼 다루어지는 특성이 있어서 가능하다. 아래 배열을 사용하여 어떻게 for, if 문을 제거하고 중간 변수 없는 프로그래밍을 할 수 있는지 예를 들겠다.

['apple', 'banana', 'melon'];

1. 기존의 함수

과일 이름 목록을 입력으로하고 같은 내용의 목록을 출력을 한다고 가정하면 for 키워드를 사용하면 간단하다. f를 입력으로 g를 출력으로 사용한다.

var f = ['apple', 'banana', 'melon'];
var g = [];
for (var i = 0; i < f.length; i++) {
  g.push(f[i])
}
console.log(g);
// [ 'apple', 'banana', 'melon' ]

과일 이름의 첫 자를 대문자로 변경하기 위해 코드를 조금 변경하겠다.

var f = ['apple', 'banana', 'melon'];
var g = [];
for (var i = 0; i < f.length; i++) {
  g.push(f[i][0].toUpperCase() + f[i].substr(1))
}
console.log(g);
// [ 'Apple', 'Banana', 'Melon' ]

이 기능을 하나의 함수로 만들겠다.

function capitalize(f) {
  var g = [];
  for (var i = 0; i < f.length; i++) {
    g.push(f[i][0].toUpperCase() + f[i].substr(1))
  }
  return g;
}
capitalize(['apple', 'banana', 'melon']);
// [ 'Apple', 'Banana', 'Melon' ]

2. for 키워드를 사용하는 함수

여기까지는 기존의 함수 사용법과 같다. capitalize 함수 내부의 루프와 문자열 변경 기능을 각각 함수로 분리한다.

function map(f, callback) {
  var g = [];
  for (var i = 0; i < f.length; i++) {
    g.push(callback(f[i]))
  }
  return g;
}
function capitalize(word) {
  return word[0].toUpperCase() + word.substr(1);
}
map(['apple', 'banana', 'melon'], capitalize);
// [ 'Apple', 'Banana', 'Melon' ]

마지막 한 줄에는 루프가 보이지 않는다. 루프가 사라진 코드는 간결하고 이해하기 쉽다. 그리고 중간 변수가 없다. 함수는 입력받은 데이터를 처리하여 복사본을 돌려준다. 전달 받은 변수나 전역의 변수를 변경하지 않는다. 코드에는 입력 데이터와 함수만 보인다. 하나의 함수가 map과 capitalize과 분리되어 전체 코드는 늘었지만 아래과 같이 응용이 가능하다.

map(['apple', 'banana', 'melon'], console.log)
// apple
// banana
// melon
map(map(['apple', 'banana', 'melon'], capitalize), console.log);
// Apple
// Banana
// Melon

함수를 호출하고 다시 그 결과를 저장한 이후에 다른 함수의 입력으로 사용하는 방식이 아니고, 함수를 호출할 때 처리 함수를 인자로 넘겨주는 방식을 사용한다. Bash의 파이프 혹은 jQuery의 Chaining과 비슷하다. 데이터를 계속 넘겨받으면서 처리한다.

함수형 프로그래밍의 영문 위키 를 보면 함수형 프로그래밍의 개념 중에 아래 2가지가 설명되는데

  • First-class and higher-order functions
  • Pure functions

첫 번째 개념은 자바스크립트 언어가 지원하며 두 번째 개념은 map과 capitalize가 전달받은 인자에 대해서만 처리함으로 일부 만족한다고 볼 수 있다. underscorelodash 같은 자바스크립트 라이브러리는 map 같은 부류의 여러 다른 함수들 제공한다. map 함수 대신에 underscore를 사용한 예는 다음과 같다. Node.js에서 테스트 할 수 있다.

var _ = require('underscore');
function capitalize(word, index) {
  return index + ": " + word[0].toUpperCase() + word.substr(1);
}
_.map(['apple', 'banana', 'melon'], capitalize);
// [ '0: Apple', '1: Banana', '2: Melon' ]

underscore의 _.map 함수와 위에서 설명한 map 함수는 모두 내부에 for 문을 사용한다. 함수형 프로그램에서 for 같은 루프가 필요할 때 map 함수를 사용하면 된다.

3. 함수를 반환하는 함수

일반적으로 함수는 값을 반환하지만 자바스크립트의 함수는 함수를 반환할 수 있다. 함수를 반환하는 방식을 사용하여 같은 기능을 만들었다. 단, 위의 소스와 다른점이 있는데 _.map 함수는 전달된 함수를 호출할 때 두 번째 인자로 리스트의 인덱스를 전달하는데 이 인덱스를 capitalize 함수에서 두 번째 인자로 사용한 것이다.

var _ = require('underscore');
function capitalize(word, index) {
  return index + ": " + word[0].toUpperCase() + word.substr(1);
}
function getListFunction(changeCallback) {
  return function (list) {
    return _.map(list, changeCallback);
  };
}
var upperCaseFirstChar = getListFunction(capitalize);
upperCaseFirstChar(['apple', 'banana', 'melon']);
// [ '0: Apple', '1: Banana', '2: Melon' ]

이전 코드와 같은 결과를 보여주지만 다른 점이 있다. getListFunction이라는 함수가 추가되었다. 이 함수는 함수를 돌려준다. map에 전달될 changeCallback 함수는 getListFunction의 인자로 전달된다. 이때 반환된 함수는 다음과 같아서 list라는 인자를 전달받는 함수이다.

function (list) {
  return _.map(list, changeCallback);
}

changeCallback은 capitalize가 전달되었으므로 아래처럼 보아도 된다.

function (list) {
  return _.map(list, capitalize);
}

이 함수가 upperCaseFirstChar에 할당되었으므로 아래처럼 보아도 된다.

function upperCaseFirstChar(list) {
  return _.map(list, capitalize);
}

동적으로 함수를 생성하고 이 함수를 이름 있는 변수에 할당할 수 있다. 자바스크립트의 함수는 실행할 수 있는 객체와 같아서 객체를 언제든 만들 수 있듯이 함수도 그렇다. getListFunction 함수를 사용하면 다음과 같은 작은 변화만으로 과일 이름 전체를 대문자로 만드는 함수를 만들 수 있다.

var upperCase = getListFunction(function (word) {
  return word.toUpperCase();
});
upperCase(['apple', 'banana', 'melon']);
// [ 'APPLE', 'BANANA', 'MELON' ]

4. if 키워드를 사용하는 함수

underscore를 사용하지 않는 버전을 다시 준비했다. 주어진 소문자 과일들을 모두 대문자로 변경한다. map 함수에 전달된 두 번째 인자는 함수다. 이 함수는 이름이 없다. 코드는 더욱 단순해졌다.

function map(f, callback) {
  var g = [];
  for (var i = 0; i < f.length; i++) {
    g.push(callback(f[i]))
  }
  return g;
}
map(['apple', 'banana', 'melon'], function (word) {
    return word.toUpperCase();
});
// [ 'APPLE', 'BANANA', 'MELON' ]

과일 이름의 길이를 5자일 경우에만 대문자로 변경하려면 map 함수 내부에서 if 키워드를 사용하여 처리하는 것이 가장 간단하다. 그러나 이렇게 처리할 경우 map 함수에 이런 제한 사항이 들어있는지 함수를 호출하는 코드에서 모르게 된다. 그래서 길이를 제한하는 별도의 함수를 만들어 사용하겠다. filter 함수는 입력을 배열로 받아서 각 항목의 길이가 5인 항목만 배열로 만들어서 돌려준다.

function filter(f) {
  var g = [];
  map(f, function (word) {
      if(word.length === 5) {
        g.push(word);
      }
  });
  return g;
}
map(filter(['apple', 'banana', 'melon']), function (word) {
    return word.toUpperCase();
});
// [ 'APPLE', 'MELON' ]

추가된 filter 함수는 map 함수 전달 전에 호출되어 문자열 길이 5자 제한 사항을 처리한다. filter의 기능을 범용으로 사용하기 위해 filter 함수에 처리 함수를 전달하는 방법을 사용하겠다.

function filter(f, callback) {
  var g = [];
  map(f, function (word) {
      if(callback(word)) {
        g.push(word);
      }
  });
  return g;
}
map(filter(['apple', 'banana', 'melon'], function (word) {
      return word.length === 5;
  }), function (word) {
    return word.toUpperCase();
});

filter 함수는 리스트에 전달 받은 비교 함수를 사용하여 리스트를 선택한 후에 돌려준다. underscore 라이브러리에도 비슷한 _.filter 함수가 있음은 물론이고 if 키워드를 사용한다. 선택이 필요할 때 if 키워드 대신 filter 함수를 사용할 수 있다.

5. 장점

함수형 프로그래밍에서 중요한 것은 함수이고 내장된 함수이건 새롭게 만든 함수이건 문제 처리의 기반이 된다. 원하는 기능을 함수 단위로 나누고 함수들을 이용해서 문제를 해결하는 방법이 자바스크립트로 가능하다. 스스로에게 질문을 던져본다. 아래 질문에 얼마나 명확한 코드를 작성할 수 있는가?

문자열을 항목으로 가지는 배열이 있다. 이 배열에서 5 글자의 항목만을 선택하여 모두 대문자로 변경하라.

map(filter(['apple', 'banana', 'melon'], function (word) {
      return word.length === 5;
  }), function (word) {
    return word.toUpperCase();
});

이런 방식은 임시 변수도 없고 코드가 짧고 이해하기 쉽다. 더구나 map, filter 함수는 범용으로 사용할 수 있는 장점이 있고 아래와 같은 예외도 문제 없으며

map(filter([], function (word) {
      return word.length === 5;
  }), function (word) {
    return word.toUpperCase();
});
map([], function (word) {
    return word.toUpperCase();
});

아래와 같이 map과 filter 함수를 바꾸어 사용해도 문제 없다.

filter(map(['apple', 'banana', 'melon'], function (word) {
      return word.toUpperCase();
  }), function (word) {
    return word.length === 5;
});

마무리

유닉스 철학에 다음과 같은 내용이 나온다.

각 프로그램이 하나의 일을 잘 할 수 있게 만들 것. 새로운 일을 하려면, 새로운 기능들을 추가하기 위해 오래된 프로그램을 복잡하게 만들지 말고 새로 만들 것.
모든 프로그램 출력이 아직 잘 알려지지 않은 프로그램이라고 할지라도 다른 프로그램에 대한 입력이 될 수 있게 할 것. 무관한 정보로 출력을 채우지 말 것. 까다롭게 새로로 구분되거나 바이너리로 된 입력 형식은 피할 것. 대화식 입력을 고집하지 말 것.

위 내용에서 프로그램이라는 단어를 함수로 변경하면 함수형 프로그래밍에서 사용하는 map, filter 같은 함수들이 이러한 조건을 만족한다고 생각한다. 함수를 서로 조합할 수 있는 논리적인 코드 블럭으로 사용하는 것은 매력적이다. github.com/afrontend 한 그릇, 웹 데이터를 읽고 보기 좋게 보여주는 기술

함수형 프로그래밍 – JavaScript
자바스크립트의 클로저

답글 남기기

아래 항목을 채우거나 오른쪽 아이콘 중 하나를 클릭하여 로그 인 하세요:

WordPress.com 로고

WordPress.com의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

Google photo

Google의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

Twitter 사진

Twitter의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

Facebook 사진

Facebook의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

%s에 연결하는 중

This site uses Akismet to reduce spam. Learn how your comment data is processed.