본문 바로가기
책정리/자바스크립트

함수형 자바스크립트 - 5장 함수로 함수 만들기

by 난파선 2017. 1. 23.

함수로 함수 만들기

 

 

'왜 함수를 만들며 어떻게 만들 것인가?

5.1 함수 조립의 핵심

  1. 대상이 존재하는 지 확인한다.
  2. 네이티브 버전이 있는 지 확인하여 있다면 그것을 사용한다.
  3. 네이티브 버전이 없다면 필요한 동작을 수행할 태스크를 구현한다.
function invoker (NAME, METHOD) {
  return function(target /* args ... */) {
    if (!existy(target)) fail("Must provide a target");

    var targetMethod = target[NAME];
    var args = _.rest(arguments);

    return doWhen((existy(targetMethod) && METHOD === targetMethod), function() {
      return targetMethod.apply(target, args);
    });
  };
};
------------------------------------------------------------------------------------
function dispatch(/* funs */) {
  var funs = _.toArray(arguments);
  var size = funs.length;

  return function(target /*, args */) {
    var ret = undefined;
    var args = _.rest(arguments);

    for (var funIndex = 0; funIndex < size; funIndex++) {
      var fun = funs[funIndex];
      ret = fun.apply(fun, construct(target, args));    // 메서드 실행을 다른 함수에 위임한다.

      if (existy(ret)) return ret;    // 결과값이 있을 경우 반환한다.
    }

    return ret;
  };
}

var str = dispatch(invoker('toString', Array.prototype.toString),
invoker('toString', String.prototype.toString));

str("a");
//=> "a"

str(_.range(10));
//=> "0,1,2,3,4,5,6,7,8,9"


function stringReverse(s) {
  if (!_.isString(s)) return undefined;    // 대상이 있는 지 확인
  return s.split('').reverse().join("");    // 필요한 동작을 구현
}

stringReverse("abc");
//=> "cba"

stringReverse(1);
//=> undefined

var rev = dispatch(invoker('reverse', Array.prototype.reverse),
stringReverse);

rev([1,2,3]);  // Array.prototype.reverse
//=> [3, 2, 1]

rev("abc");  // stringReverse
//=> "cba"
  • switch 문을 사용하는 함수를 개선해보자
  • 함수의 기능을 확장하려면 switch문을 고쳐야 한다.
  • 함수형 프로그래밍에서는 dispatch함수를 래핑하면 된다.
function performCommandHardcoded(command) {  // 파라미터에 따라 실행이 달라지는 함수
  var result;

  switch (command.type) //수정이 발생 시 case문이 추가되는 소스 수정이 필요하다.
  {
  case 'notify':
    result = notify(command.message);
    break;
  case 'join':
    result = changeView(command.target);
    break;
  default:
    alert(command.type);
  }

  return result;
}

function isa(type, action) {
  return function(obj) {
    if (type === obj.type)
      return action(obj);
  }
}

var performCommand = dispatch(
  isa('notify', function(obj) { return notify(obj.message) }),
  isa('join',   function(obj) { return changeView(obj.target) }),
  function(obj) { alert(obj.type) });

var performAdminCommand = dispatch( // 기능추가시 추가되는 기능 dispatch함수로 래핑한다.
  isa('kill', function(obj) { return shutdown(obj.hostname) }),
  performCommand);

5.1.1 변이는 저수준 동작이다.

  • 명령행보다는 함수형으로 코드를 구현하는 것이 가장 이상적이지만 때로는 속도나 편의를 고려해서 라이브러리 기능을 명령행으로 구현한다.
  • 함수 내부에서 변수 변이가 발생했고 외부로 영향을 주지 않는 다면 아무도 개의치 않는 다.
  • 때로는 변이가 필요하다. 하지만 변이는 항상 멀리해야 할 저수준 동작으로 간주한다.
  • 애플리케이션을 만들 때는 실행 파라미터가 무언인지 먼저 확인해 봄으로써 어떤 구현 기법이 가장 적절할지 결정하는 것이 바람직하다.

5.2 커링

  • 각각의 논리적 인자에 대응하는 새로운 함수를 반환하는 함수를 커리함수라고 한다.
  • 각각의 논리 파라미터를 이용해서 점차 세부적으로 설정된 함수를 반환한다.
function rightAwayInvoker() { //일반적인 함수, 함수를 반환하지 않는 다.
  var args = _.toArray(arguments);
  var method = args.shift();
  var target = args.shift();

  return method.apply(target, args);
}

rightAwayInvoker(Array.prototype.reverse, [1,2,3])
//=> [3, 2, 1]

5.2.1 우향 커리, 좌향 커리

  • 좌향 커리 : 가장 오른쪽 인자에서 커리가 시작해서 왼쪽으로 이동한다.
function leftCurryDiv(n) {
  return function(d) {
    return n/d;
  };
}

function rightCurryDiv(d) {
  return function(n) {
    return n/d;
  };
}

5.2.2 자동 커링 파라미터

  • 함수를 인자로 받은 다음에 한 개의 파라미터를 갖는 고정된 함수를 반환하는 고차원 함수를 curry라고 부른다.
  • 함수를 직접 사용하면 되는 데 굳이 curry를 사용하는 가?
  • 자바스크립트에서는 인자의 개수와 부가적인 '특화'인자의 개수가 정해져 있을 때가 많다.
parseInt('11');
parseInt('11',2);
['11','11'].map(parseInt) // [11, NaN]  내부적으로 배열, 배열인덱스가 넘어간다.

function curry(fun) {
  return function(arg) {
    return fun(arg);
  };
}

['11','11'].map(curry(parseInt));

커링으로 새로운 함수 만들기

function curry2(fun) {
  return function(secondArg) {
    return function(firstArg) {
        return fun(firstArg, secondArg);
    };
  };
}

var plays = [{artist: "Burial", track: "Archangel"},
             {artist: "Ben Frost", track: "Stomp"},
             {artist: "Ben Frost", track: "Stomp"},
             {artist: "Burial", track: "Archangel"},
             {artist: "Emeralds", track: "Snores"},
             {artist: "Burial", track: "Archangel"}];

_.countBy(plays, function(song) {
  return [song.artist, song.track].join(" - ");
});

//=> {"Ben Frost - Stomp": 2,
//    "Burial - Archangel": 3,
//    "Emeralds - Snores": 1}

function songToString(song) {
  return [song.artist, song.track].join(" - ");
}

var songCount = curry2(_.countBy)(songToString);    //함수이름을 직관적으로 구현

songCount(plays);
//=> {"Ben Frost - Stomp": 2,
//    "Burial - Archangel": 3,
//    "Emeralds - Snores": 1}

세 개의 파라미터를 커링해서 HTML 16진 색상 생성기 구현하기

function curry3(fun) {
  return function(last) {
    return function(middle) {
      return function(first) {
        return fun(first, middle, last);
      };
    };
  };
};

var songsPlayed = curry3(_.uniq)(false)(songToString);

songsPlayed(plays);

//=> [{artist: "Burial", track: "Archangel"},
//    {artist: "Ben Frost", track: "Stomp"},
//    {artist: "Emeralds", track: "Snores"}]

function toHex(n) {
  var hex = n.toString(16);
  return (hex.length < 2) ? [0, hex].join(''): hex;
}

function rgbToHexString(r, g, b) {
  return ["#", toHex(r), toHex(g), toHex(b)].join('');
}

rgbToHexString(255, 255, 255);
//=> "#ffffff"

var blueGreenish = curry3(rgbToHexString)(255)(200);

blueGreenish(0);
//=> "#00c8ff"

5.2.3 커링을 이용한 플루언트 API

??

5.2.4 자바스크립트에서 커링의 단점

  • 함수의 인자가 다양하므로 커링을 적용했을 때 혼란스러울 때가 많다.

5.3 부분 적용

  • 부분적으로 실행을 마친 다음에 나머지 인자와 함께 즉시 실행한 상태가 되는 함수
function divPart(n) {
  return function(d) {
    return n / d;
  };
}

var over10Part = divPart(10);
over10Part(2);
//=> 5
  • 커리 함수는 사실상 한 개의 인자만 남겨 둔 부분 적용 함수와 같다. 그러나 부분 적용에서는 한 번에 한 개의 인자를 처리하지 않으며 한 번에 필요한 만큼의 인자를 적용한 다음에 이후 실행 시 나머지 인자를 활용할 수 있는 상태로 저장된다.

5.3.1 한두 개의 알려진 인자를 부분 적용

function partial1(fun, arg1) {
  return function(/* args */) {
    var args = construct(arg1/*한개의 인자를 부분적용*/, arguments);
    return fun.apply(fun, args);
  };
}

var over10Part1 = partial1(div, 10);
over10Part1(5) //=>2

function partial2(fun, arg1, arg2) {    // 두개의 인자를 부분 적용하는 함수
  return function(/* args */) {
    var args = cat([arg1, arg2], arguments);
    return fun.apply(fun, args);
  };
}

var div10By2 = partial2(div, 10, 2)

div10By2()
//=> 5

5.3.2 임의의 수의 인자를 부분 적용

function partial(fun /*, pargs */) {
  var pargs = _.rest(arguments);

  return function(/* arguments */) {
    var args = cat(pargs, _.toArray(arguments));
    return fun.apply(fun, args);
  };
}

5.3.3 부분 적용 사례: 선행조건

  • 선행조건 : 호출하는 함수에서 보장하는 조건
  • 후행조건 : 선행조건이 지켜졌다는 가정 하에 함수 호출 결과를 보장하는 조건
  • 선행조건과 후행조건의 관계를 '함수가 처리할 수 있는 데이터를 제공했을 때 함수는 특정기준을 만족하는 결과를 반환할 것이다.'
var zero = validator("cannot be zero", function(n) { return 0 === n
});
var number = validator("arg must be a number", _.isNumber);

function sqr(n) { // 선행 조건 추가 시 if문을 추가해야 한다.
  if (!number(n)) throw new Error(number.message);
  if (zero(n))    throw new Error(zero.message);

  return n * n;
}

function condition1(/* validators */) {
  var validators = _.toArray(arguments);

  return function(fun, arg) {
    var errors = mapcat(function(isValid) {
      return isValid(arg) ? [] : [isValid.message];
    }, validators);

    if (!_.isEmpty(errors))
      throw new Error(errors.join(", "));

    return fun(arg);
  };
}

var sqrPre = condition1(
  validator("arg must not be zero", complement(zero)),
  validator("arg must be a number", _.isNumber));

5.4 함수의 끝을 서로 연결하는 함수 조립 방법

  • 한 쪽으로 데이터를 넣으면 반대편으로 완전히 새로운 데이터가 나올 수 있도록 함수들이 파이프라인을 이루고 있다면 가장 이상적인 함수형 프로그램이라고 할 수 있다.
  • 함수형 조립은 여러 함수가 수행하는 데이터 변형을 모아서 데이터 체인을 이용, 새로운 함수를 만든 다.
  • 아무 것도 변경하지 않은 상태에서 새로운 값을 얻을 수 있다.
!_.isString(name)
/**
_.isString과 연산자 ! 사이에서 파이프 라인이 만들어 진다.
_.isString은 객체를 인자로 받아 불린 값을 반환 한다.
!는 불린 값을 평가해서 반전한 값을 반환한다.
*/

var isntString = _.compose(function(x) { return !x }, _.isString);

isntString([]);
//=> true


댓글