seoh님의 Elm Resources 글에서 [Functional Programming: Purely Functional Data Structures in Elm

]3 강의를 알게 되었다. 개요를 읽고 흥미가 생겨 강의 노트를 읽기 시작했고 나중에 쉽게 찾아보려고 짤막하게라도 정리하기로 했다. 강의에서 사용된 elm이 이미 구버전이라서 최신 버전과 다른 부분이 있어 그 부분은 별도로 적었다. 조금 지나면 내 노트도 금방 구버전이 될 것 같은 기분이지만.

강의 노트 정리 페이지 목록보기


Course Overview

이 코스는 다음 두가지 테마로 진행된다.

  • 효과적인 데이터 구조를 구현하고 분석, 특히 함수형 프로그래밍 언어의 맥락에서
  • 인터렉티브 프로그래밍을 위한 함수형 리액티브 프로그래밍(FRP) 배우기

여기서 사용한 언어는 ML의 방언인 Elm. ML은 1970년대 개발되어 많은 영향을 주고 있다.

  • 평가 전략 Evaluation Strategy: (이른 평가 문법 위에서 동작하는) 지연 평가
  • 타입 Types: 정적 타입, 자동 타입 인터페이스 등 쿨한 기능이 있어 하스켈과는 조금 다르다고.
  • 부작용 Side Effects: 함수형 프로그래밍에서는 가변변수 등 절차형에서 제공하는 기능을 사용할 수 있지만 작게, 지역적으로 사용하길 권장. ML은 타입 시스템 바깥에서 처리(?)하고 하스켈은 타입으로 기록하는 방식으로 차이가 있음.

Elm은 Standard ML, OCaml, F#에 비해 적은 기능만 추가돼 작은 방언이라 얘기한다.

이 코스도 처음으로 진행되고 Elm도 작은 언어에 작은 커뮤니티라 삽질을 각오하고 코스에 임할 것.

Elm 강의를 보고 있는데 멱집합을 생성하는 함수가 과제로 나왔다. 한참을 고민하다가 결국 검색을 해보게 되었다. 반성하는 차원에서 How is this powerset generating function working?를 짧게 번역했다. 수학공부 부지런히 해야겠다.


이 멱집합 생성 함수는 어떻게 동작하는거죠?

리스트를 넣어 멱집합을 생성하는 함수를 만들려고 하는데 다음과 같은 코드를 인터넷에서 찾게 되었다. 설명은 없었지만 테스트 해보니 정상적으로 동작했다. 이 함수가 어떻게 동작하는지 이해할 수가 없었다. 설명을 해주면 감사하겠다.

generateSubset [] = [[]]
generateSubset (x:xs) = let p = generateSubset xs in p ++ map (x:) p

Daniel Wagner의 답변

멱집합의 특성으로 쉽게 증명할 수 있다. P(A ∪ B) = {a ∪ b | a ∈ P(A) 또는 b ∈ P(B)}. 특히 집합 S와 그 집합에 포함된 원소인 s로 분해하고 S’의 모든 원소는 s가 아닐 때,

P(S) = P({s} ∪ S')
     = {a ∪ b | a ∈ P({s}), b ∈ P(S')}

여기서 P({s})는 직접 계산하기 좋을 정도로 충분히 작다. P({s}) = {{}, {s}}. 이 사실로 다음과 같이 유도한다.

P(S) = {a ∪ b | a ∈ {{}, {s}}, b ∈ P(S')}
     = {b | b ∈ P(S')} ∪ {{s} ∪ b | b ∈ P(S')}
     = P(S') ∪ {{s} ∪ b | b ∈ P(S')}
     = let p = P(S') in p ∪ {{s} ∪ b | b ∈ p}

이 방법은 멱집합이 빈 원소를 갖는 집합이기 때문에 사용 가능한 계산 방법이다. 먼저 원소를 고른 후, 나머지로 멱집합을 연산한다. 그 후 각각의 부분집합에 추가하거나 더하지 않는다. 질문한 함수는 이 부분을 코드로 전환한 것으로 리스트를 집합으로 사용했다.

-- P         ({s} ∪ S') = let p = P(S')             in p  ∪ {{s} ∪ b | b ∈ p}
generateSubset (x:xs)   = let p = generateSubset xs in p ++     map (x:) p

재귀를 위해 기본값을 넣어주는 일만 남았다. 멱집합의 정의에 따라 다음과 같이 추가한다.

-- P          ({}) = {{}}
generateSubset []  = [[]]

Backbone.js를 지금까지 사용해본 적이 없었는데 주말에 깜짝 방문한 jimkimau님과 함께 살펴보게 되었다. 처음 사용해보는데다 아직 이사온 곳에 인터넷이 아직 들어오지 않아 문서 없이 코드만 보고 살펴볼 수 있을지 걱정했다. 컴퓨터를 검색해보니 어디서 사용한지 모르겠지만 backbone.js 파일을 찾을 수 있었다. 게다가 un-zipped 버전의 backbone.js에는 주석으로 모든 함수에 대한 설명과 사용 방식이 상세하게 남겨져 있어 오프라인이 문제가 되지 않았다.

underscore를 개발한 개발자가 만들어서 그런지 underscore에 대한 강한 의존성을 가지고 있다. Underscore도 방대하고 유익한 함수가 많다는 사실을 익히 들어 알고 있었지만 실무에서 사용하고 있지 않아 익숙해질 기회가 별로 없었다. 이번에 backbone.js를 살펴보던 중 이 라이브러리를 사용하는 방식이 상당히 인상적이었다. underscore를 더 재미있게 활용하는데 도움이 될 것 같아 어떤 방식으로 사용하고 있는지 발췌해 포스트로 정리해봤다.

객체 확장하기: _.extend()

Backbone.js에서는 기본적으로 underscore에서 제공하는 _.extend(destination, *sources) 함수를 이용해 확장하는 방식을 채택하고 있다. 이 함수는 sorces로 전달된 모든 객체의 프로퍼티를 복사해 destination 객체에 넣고 그 객체를 반환한다. 만약 동일한 명칭의 프로퍼티가 있다면 sources로 제공된 순서에 따라 덮어쓰게 된다. Backbone.js 전반에 걸쳐 상속 등에 사용되고 있는 함수다.

_.extend(Colletion.prototype, Events, {
    ...
});

Backbone.js에서 제공하는 모든 모듈은 위와 같은 방식으로 Backbone.Events의 함수를 확장하고 있다. 이 Events가 모든 모듈에 포함되어 이벤트 핸들링을 쉽게 만들며 전역 객체인 Backbone도 이 Events를 상속해 전역적인 pubsub을 구현할 수 있도록 돕고 있다.

객체 생성하기: _.defaults()_.uniqueId()

_.defaults()은 객체의 기본값을 설정할 때 많이 사용되는 함수로 Backbone.js에서는 기본값에 사용자 설정을 채워넣을 때 사용하고 있다.

var setOptions = {add: true, remove: true, merge: true};
// ...
_.extend(Collection.prototype, Events, {
  set: function(models, options) {
    options = _.defaults({}, options, setOptions);
    // ...
  }
  // ...
};

앞서 _.extend()와 유사하게 느껴지지만 _.defaults()는 첫번째 인자로 들어온 객체에 프로퍼티가 존재하지 않거나 null인 경우 즉, obj[prop] == null이 참인 경우에 이후 인자의 프로퍼티를 덮어쓰게 된다.

또한 객체를 초기화 하는 과정에서 고유한 id가 필요한 경우가 있는데 backbone.js는 _.uniqueId() 함수를 사용해 객체의 cid를 설정하고 있다. 인자로 제공한 문자열은 prefix가 된다. 이 함수는 호출할 때마다 1씩 증가한다.

this.cid = _.uniqueId('view'); // view1

객체에서 필요한 값 얻기: _.pick(), _.result()

객체를 확장하기 위해서 _.extend() 함수를 사용하는 것을 앞에서 확인했다. 확장에서 모든 객체를 확장하는 것이 아니라 필요로 하는 메소드와 프로퍼티만 확장하고 싶을 수도 있다. 객체의 모든 값이 아니라 일부만 필요하다면 _.pick() 함수를 활용할 수 있다.

var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName', 'events'];
_.extend(this, _.pick(options, viewOptions));

위와 같이 배열로 입력해도 되고 필요로 하는 키 이름을 나열해도 반환한다.

var bibimbab = {gosari: 10, egg: 4, rice:3, pepper_paste:10, spinach:3, sesame_oil: 2};
_.pick(bibimbab, 'egg', 'rice', 'sesame_oil');
// Object {egg: 4, rice: 3, sesame_oil: 2}
_.pick(bibimbab, ['gosari', 'spinach']);
// Object {gosari: 10, spinach: 3}

함수가 1급 클래스인 JavaScript의 특성으로 객체 프로퍼티가 값인 경우도 있고 함수인 경우도 있다. 프로퍼티 값을 받는 것과 같이 함수의 경우에는 호출해서 그 반환 값을 바로 받도록 하고 싶다면 직접 _.isFunction() 함수로 검사해서 값을 만들어도 되지만 간편하게 _.result() 함수를 활용할 수 있다.

var attrs = _.extend({}, _.result(this, 'attributes'));

위와 같이 객체에서 사용할 어트리뷰트가 함수의 형태로 저장되어 있다면 함수를 실행해 그 결과를 반환해서 바로 사용할 수 있다.

underscore 메소드 재사용하기

Bacbone.js에서 제공하는 Backbone.Collection에서는 underscore의 다양한 함수를 체이닝으로 사용할 수 있도록 확장되어 있다. 문서에서는 collection에서 언더스코어의 90% 가량 함수를 지원한다고 기술하고 있다.

var methods = ['forEach', 'each', 'map', 'collect', 'reduce', ... ];

_.each(methods, function(method) {
  Collection.prototype[method] = function() {
    var args = slice.call(arguments);
    args.unshift(this.models);
    return _[method].apply(_, args);
  };
});

Backbone.Collection이 테이블이라면 각각의 데이터 행을 Backbone.Model로 제공한다. 위 코드에서 보면 Collection에 존재하는 model의 목록을 unshift()로 인자목록 앞에 넣어 underscore로 넘기는 방식으로 체이닝 메소드를 구현했다.

위와 같은 구현은 컬렉션 외에도 Backbone.Model에서도 유사하게 지원한다. 앞서 언급한 바와 같이 model은 단일 엔티티를 의미하고 있어 keys(), values()와 같은 메소드만 선택적으로 확장하고 있다.

앞서의 methods와 달리 attributeMethods의 경우는 다음과 같이 약간 다른 구현이 필요하다.

var attributeMethods = ['groupBy', 'countBy', 'sortBy', 'indexBy'];

_.each(attributeMethods, function(method) {
    Collection.prototype[method] = function(value, context) {
      var iterator = _.isFunction(value) ? value : function(model) {
        return model.get(value);
      };
      return _[method](this.models, iterator, context);
    };
  });

익명함수나 값을 기준으로 처리하는 attribute 함수는 위와 같은 방식으로, 함수인지 아닌지 판별 과정을 거쳐 함수인 경우 필터에 사용한다. 함수가 아닌 경우 해당 모델에서 값을 받아 사용할 수 있도록 처리되었다.


위에서 언급한 부분 외에도 흥미로운 부분이 많았다. _.once() 함수를 Backbone.Events에서 구현한 방식도 인상적이었고, 함수형 접근에 더 적절한 맥락을 갖도록 몇가지 함수를 제어 역전하는 부분도 살펴보기 좋다. 이벤트 트리거를 위해 내부적으로 사용하고 있는 함수인 triggerEvents()에서는 속도를 위해 switch를 쓰는 재미있는 구석도 있다.

코드를 읽는데도 주석도 잘 달려있고 각각의 함수 이름도 잘 지어진데다 잘게 잘 쪼개져 있어서 배우게 된 부분이 많았다. Backbone.js와 underscore를 관통하는 패러다임을 살펴볼 수 있었고 지금도 많은 개발자가 이 라이브러리를 사용할 만큼 매력적이란 사실을 알 수 있었다. lodash와 함께 나올 underdash도 기대가 된다.

얼마 전 제이펍 출판사 베타리더스 3기에 선정되었다. 선정 되자마자 <함수 프로그래밍 실천 기술>이란 제목의 책을 베타리딩하게 되었는데 함수형 프로그래밍에 대해 전반적인 내용과 세세한 개념을 Haskell로 설명하는 책이었다. 함수형 프로그래밍에 대한 책을 처음 읽어봐서 생소한 개념도 많았지만 다른 언어로의 비교 코드를 많이 제시하고 있어 전체적인 이해에 도움이 많이 되었다. 조만간 출간된다고 하니 관심이 있다면 제목을 적어두는 것도 좋겠다 🙂

함수형 언어를 얘기하면 모나드가 꼭 빠지지 않는다. 이 포스트는 Monads in JavaScript의 번역글이다. 이 글이 모나드에 대해 세세하게 모든 이야기를 다룬 것은 아니지만 모나드의 아이디어를 JavaScript로 구현해서 이 코드에 익숙하다면 좀 더 쉽게 접근할 수 있는 글이라 번역으로 옮겼다. 쉽게 이해하기 어렵지만 이해하면 정말 강력하다는 모나드를 이 글을 통해 조금이나마 쉽게 이해하는데 도움이 되었으면 좋겠다.


모나드는 순서가 있는 연산을 처리하는데 사용하는 디자인 패턴이다. 모나드는 순수 함수형 프로그래밍 언어에서 부작용을 관리하기 위해 광범위하게 사용되며 복합 체계 언어에서도 복잡도를 제어하기 위해 사용된다.

모나드는 타입으로 감싸 빈 값을 자동으로 전파하거나(Maybe 모나드) 또는 비동기 코드를 단순화(Continuation 모나드) 하는 등의 행동을 추가하는 역할을 한다.

모나드를 고려하고 있다면 코드의 구조가 다음 세가지 조건을 만족해야 한다.

  1. 타입 생성자 – 기초 타입을 위한 모나드화된 타입을 생성하는 기능. 예를 들면 기초 타입인 number를 위해 Maybe<number> 타입을 정의하는 것.
  2. unit 함수 – 기초 타입의 값을 감싸 모나드에 넣음. Maybe 모나드가 number 타입인 값 2를 감싸면 타입 Maybe<number>의 값 Maybe(2)가 됨.
  3. bind 함수 – 모나드 값으로 동작을 연결하는 함수.

다음의 TypeScript 코드가 이 함수의 일반적인 표현이다. M은 모나드가 될 타입으로 가정한다.

interface M<T> {

}

function unit<T>(value: T): M<T> {
    // ...
}

function bind<T, U>(instance: M<T>, transform: (value: T) => M<U>): M<U> {
    // ...
}

Note: 여기에서의 bind 함수는 Function.prototype.bind 함수와 다르다. 후자의 bind는 ES5부터 제공하는 네이티브 함수로 부분 적용한 함수를 만들거나 함수에서 this 값을 바꿔 실행할 때 사용하는 함수다.

JavaScript와 같은 객체지향 언어에서는 unit 함수는 생성자와 같이 표현될 수 있고 bind 함수는 인스턴스의 메소드와 같이 표현될 수 있다.

interface MStatic<T> {
    new(value: T): M<T>;
}

interface M<T> {
    bind<U>(transform: (value: T) => M<U>):M<U>;
}

또한 여기에서 다음 3가지 모나드 법칙을 준수해야 한다.

  1. bind(unit(x), f) ≡ f(x)
  2. bind(m, unit) ≡ m
  3. bind(bind(m, f), g) ≡ bind(m, x => bind(f(x), g))

먼저 앞 두가지 법칙은 unit이 중립적인 요소라는 뜻이다. 세번째 법칙은 bind는 결합이 가능해야 한다는 의미로 결합의 순서가 문제가 되서는 안된다는 의미다. 이 법칙은 덧셈에서 확인할 수 있는 법칙과 같다. 즉, (8 + 4) + 2의 결과는 8 + (4 + 2)와 같은 결과를 갖는다.

아래의 예제에서는 화살표 함수 문법을 사용하고 있다. Firefox (version 31)는 네이티브로 지원하고 있지만 Chrome (version 36)은 아직 지원하지 않는다.

Identity 모나드

identity 모나드는 가장 단순한 모나드로 값을 감싼다. Identity 생성자는 앞서 살펴본 unit과 같은 함수를 제공한다.

function Identity(value) {
  this.value = value;
}

Identity.prototype.bind = function(transform) {
  return transform(this.value);
};

Identity.prototype.toString = function() {
  return 'Identity(' + this.value + ')';
};

다음 코드는 덧셈을 Identity 모나드를 활용해 연산하는 예시다.

var result = new Identity(5).bind(value =>
                 new Identity(6).bind(value2 =>
                     new Identity(value + value2)));

Maybe 모나드

Maybe 모나드는 Identity 모나드와 유사하게 값을 저장할 수 있지만 어떤 값도 있지 않은 상태를 표현할 수 있다.

Just 생성자가 값을 감쌀 때 사용된다.

function Just(value) {
  this.value = value;
}

Just.prototype.bind = function(transform) {
  return transform(this.value);
};

Just.prototype.toString = function() {
  return 'Just(' + this.value + ')';
};

Nothing은 빈 값을 표현한다.

var Nothing = {
  bind: function() {
    return this;
  },
  toString: function() {
    return 'Nothing';
  }
};

기본적인 사용법은 identity 모나드와 유사하다.

var result = new Just(5).bind(value =>
                 new Just(6).bind(value2 =>
                      new Just(value + value2)));

Identity 모나드와 주된 차이점은 빈 값의 전파에 있다. 중간 단계에서 Nothing이 반환되면 연관된 모든 연산을 통과하고 Nothing을 결과로 반환하게 된다.

다음 코드에서는 alert가 실행되지 않게 된다. 그 전 단계에서 빈 값을 반환하기 때문이다.

var result = new Just(5).bind(value =>
                 Nothing.bind(value2 =>
                      new Just(value + alert(value2))));

이 동작은 수치 표현에서 나타나는 특별한 값인 NaN(not-a-number)과도 유사하다. 결과 중간에 NaN 값이 있다면 NaN은 전체 연산에 전파된다.

var result = 5 + 6 * NaN;

Maybe 모나드는 null 값에 의한 에러가 발생하는 것을 막아준다. 다음 코드는 로그인 사용자의 아바타를 가져오는 예시다.

function getUser() {
  return {
    getAvatar: function() {
      return null; // 아바타 없음
    }
  }
}

빈 값을 확인하지 않는 상태로 메소드를 연결해 호출하면 객체가 null을 반환할 때 TypeErrors가 발생할 수 있다.

try {
  var url = getUser().getAvatar().url;
  print(url); // 여기는 절대 실행되지 않음
} catch (e) {
  print('Error: ' + e);
}

대안적으로 null인지 확인할 수 있지만 이 방법은 코드를 장황하게 만든다. 코드는 틀리지 않지만 한 줄의 코드가 여러 줄로 나눠지게 된다.

var url;
var user = getUser();
if (user !== null) {
  var avatar = user.getAvatar();
  if (avatar !== null) {
    url = vatar.url;
  }
}

다른 방식으로 작성할 수 있을 것이다. 비어 있는 값을 만날 때 연산이 정지하도록 작성해보자.

function getUser() {
  return new Just({
    getAvatar: function() {
      return Nothing; // 아바타 없음
    }
  });
}

var url = getUser()
            .bind(user => user.getAvatar())
            .bind(avatar => avatar.url);

if(url instanceof Just) {
  print('URL has value: ' + url.value);
} else {
  print('URL is empty');
}

List 모나드

List 모나드는 값의 목록에서 지연된 연산이 가능함을 나타낸다.

이 모나드의 unit 함수는 하나의 값을 받고 그 값을 yield하는 generator를 반환한다. bind 함수는 transform 함수를 목록의 모든 요소에 적용하고 그 모든 요소를 yield 한다.

function* unit(value) {
  yield value;
}

function* bind(list, transform) {
  for (var item of list) {
    yield* transform(item);
  }
}

배열과 generator는 이터레이션이 가능하며 그 반복에서 bind 함수가 동작하게 된다. 다음 예제는 지연을 통해 각각 요소의 합을 만드는 목록을 어떻게 작성하는지 보여준다.

var result = bind([0, 1, 2], function (element) {
  return bind([0, 1, 2], function* (element2) {
    yield element + element2;
  });
});

for (var item of result) {
  print(item);
}

다음 글은 다른 어플리케이션에서 JavaScript의 generator를 어떻게 활용하는지 보여준다.

Continuation 모나드

Continuation 모나드는 비동기 일감에서 사용한다. ES6에서는 다행히 직접 구현할 필요가 없다. Promise 객체가 이 모나드의 구현이기 때문이다.

  1. Promise.resolve(value) 값을 감싸고 pormise를 반환. (unit 함수의 역할)
  2. Promise.prototype.then(onFullfill: value => Promise) 함수를 인자로 받아 값을 다른 promise로 전달하고 promise를 반환. (bind 함수의 역할)

다음 코드에서는 Unit 함수로 Promise.resolve(value)를 활용했고, Bind 함수로 Promise.prototype.then을 활용했다.

var result = Promise.resolve(5).then(function(value) {
  return Promise.resolve(6).then(function(value2) {
      return value + value2;
  });
});

result.then(function(value) {
    print(value);
});

Promise는 기본적인 continuation 모나드에 여러가지 확장을 제공한다. 만약 then이 promise 객체가 아닌 간단한 값을 반환하면 이 값을 Promise 처리가 완료된 값과 같이 감싸 모나드 내에서 사용할 수 있다.

두번째 차이점은 에러 전파에 대해 거짓말을 한다는 점이다. Continuation 모나드는 연산 사이에서 하나의 값만 전달할 수 있다. 반면 Promise는 구별되는 두 값을 전달하는데 하나는 성공 값이고 다른 하나는 에러를 위해 사용한다. (Either 모나드와 유사하다.) 에러는 then 메소드의 두번째 콜백으로 포착할 수 있으며 또는 이를 위해 제공되는 특별한 메소드 .catch를 사용할 수 있다.

Promise 사용과 관련된 기사는 다음과 같다:

Do 표기법

Haskell은 모나드화 된 코드를 작업하는데 도움을 주기 위해 편리 문법(syntax sugar)인 do 표기법을 제공하고 있다. do 키워드와 함께 시작된 구획은 bind 함수를 호출하는 것으로 번역이 된다.

ES6 generator는 do 표기법을 JavaScript에서 간단하고 동기적으로 보이는 코드로 작성할 수 있게 만든다.

전 예제에서는 maybe 모나드가 다음과 같이 직접 bind를 호출했었다.

var result = new Just(5).bind(value =>
                 new Just(6).bind(value2 =>
                     new Just(value + value2)));

다음은 같은 코드지만 generator를 활용했다. 각각의 호출은 yield로 모나드에서 값을 받는다.

var result = doM(function*() {
  var value = yield new Just(5);
  var value2 = yield new Just(6);
  return new Just(value + value2);
});

이 작은 순서를 generator로 감싸고 그 뒤에 bind를 값과 함께 호출해 yield로 넘겨준다.

function doM(gen) {
  function step(value) {
    var result = gen.next(value);
    if (result.done) {
      return result.value;
    }
    return result.value.bind(step);
  }
  return step();
}

이 방식은 다른 Continuation 모나드와 같은 다른 모나드에서도 사용할 수 있다.

Promise.prototype.bind = Promise.prototype.then;

var result = doM(function*() {
  var value = yield Promise.resolve(5);
  var value2 = yield Promise.resolve(11);
  return value + value2;
}());

result.then(print);

다른 모나드와 같은 방식으로 동작하도록 thenbind로 별칭을 붙였다.

promise에서 generator를 사용하는 예는 Easy asynchrony with ES6를 참고하자.

연결된 호출 Chained calls

다른 방식으로 모나드화 된 코드를 쉽게 만드는 방법은 Proxy를 활용하는 것이다.

다음 함수는 모나드 인스턴스를 감싸 proxy 객체를 반환한다. 이 객체는 값이 있는지 없는지 확인되지 않은 프로퍼티라도 안전하게 접근할 수 있게 만들고 모나드 내에 있는 값을 함수에서 활용할 수 있게 돕는다.

function wrap(target, unit) {
  target = unit(target);
  function fix(object, property) {
    var value = object[property];
    if (typeof value === 'function') {
      return value.bind(object);
    }
    return value;
  }
  function continueWith(transform) {
    return wrap(target.bind(transform), unit);
  }
  return new Proxy(function() {}, {
    get: function(_, property) {
      if(property in target) {
        return fix(target, property);
      }
      return continueWith(value => fix(value, property));
    },
    apply: function(_, thisArg, args) {
      return continueWith(value => value.apply(thisArg, args));
    }
  });
}

이 래퍼는 빈 객체를 참조할 가능성이 있는 경우에 안전하게 접근하는 방법을 제공한다. 이 방식은 실존적 연산자(?.) 구현 방식과 동일하다.

function getUser() {
  return new Just({
    getAvatar: function() {
      return Nothing; // 아바타 없음
    }
  });
}

var unit = value => {
  // 값이 있다면 Maybe 모나드를 반환
  if (value === Nothing || value instanceof Just) {
    return value;
  }
  // 없다면 Just를 감싸서 반환
  return new Just(value);
}

var user wrap(getUser(), unit);

print(user.getAvatar().url);

아바타는 존재하지 않지만 url을 호출하는 것은 여전히 가능하며 빈 값을 반환 받을 수 있다.

동일한 래퍼를 continuation 모나드에서 일반적인 함수를 실행할 때에도 활용할 수 있다. 다음 코드는 특정 아바타를 가지고 있는 친구가 몇명이나 있는지 반환한다. 예제는 보이기엔 모든 데이터를 메모리에 올려두고 사용하는 것 같지만 실제로는 비동기적을 데이터를 가져온다.

Promise.prototype.bind = Promise.prototype.then;

function User(avatarUrl) {
  this.avatarUrl = avatarUrl;
  this.getFriends = function() {
    return Promise.resolve([
      new User('url1'),
      new User('url2'),
      new User('url11'),
    ]);
  }
}

var user = wrap(new User('url'), Prommise.resolve);

var avatarUrls = user.getFriends().map(u => u.avatarUrl);

var length = avatarUrls.filter(url => url.contains('1')).length;

length.then(print);

여기서 모든 프로퍼티의 접근과 함수의 호출은 간단하게 값을 반환하는 것이 아니라 모나드 안으로 진입해 Promise를 실행해 결과를 얻게 된다.

ES6의 Proxies에 대한 자세한 내용은 Array slices를 참고하자.


원본 포스트 https://curiosity-driven.org/monads-in-javascript (CC BY 3.0)

요즘 함수형 프로그래밍에 대한 관심이 많아져 여러가지 글을 찾아 읽고 있다. JavaScript에서도 충분히 활용 가능한데다 JS의 내부를 더 깊게 생각해볼 수 있고 다른 각도로 문제를 사고해보는데 도움이 되는 것 같아 한동안은 이와 관련된 포스트를 번역하려고 한다.

커링(currying)은 함수형 프로그래밍 기법 중 하나로 함수를 재사용하는데 유용하게 쓰일 수 있는 기법이다. 커링이 어떤 기법인지, 어떤 방식으로 JavaScript에서 구현되고 사용할 수 있는지에 대한 글이 있어 번역했다. 특히 이 포스트는 함수를 작성하고 실행하는 과정을 하나씩 살펴볼 수 있어 좋았다.

원본은 Kevin Ennis의 Currying in JavaScript에서 확인할 수 있다.


나는 최근 함수형 프로그래밍에 대해 생각을 많이 하게 되었다. 그러던 중 curry 함수를 작성하는 과정을 공유하면 재미있을 것 같다는 생각이 들었다.

처음 듣는 사람을 위해 간단히 설명하면, 커링은 함수 하나가 n개의 인자를 받는 과정을 n개의 함수로 각각의 인자를 받도록 하는 것이다. 부분적으로 적용된 함수를 체인으로 계속 생성해 결과적으로 값을 처리하도록 하는 것이 그 본질이다.

어떤 의미인지 다음 코드를 보자:

function volume( l, w, h ) {
  return l * w * h;
}

var curried = curry( volume );

curried( 1 )( 2 )( 3 ); // 6

면책 조항

이 포스트는 기본적으로 클로저와 Function#apply()와 같은 고차함수에 익숙한 것을 가정하고 작성했다. 이런 개념에 익숙하지 않다면 더 읽기 전에 다시 복습하자.

curry 함수 작성하기

앞서 코드에서 볼 수 있듯 curry는 인자로 함수를 기대하기 때문에 다음과 같이 작성한다.

function curry( fn ) {

}

다음으로 얼마나 많은 인자가 함수에서 필요로 할지 알아야 한다. (인자의 갯수 arity 라고 부른다.) 인자의 갯수를 알기 전까지 몇 번이나 새로운 함수를 반환하고, 어느 순간에 함수 대신 값을 반환하게 될지 알 수가 없다.

함수에서 몇개의 인자를 기대하는지 length 프로퍼티를 통해 확인할 수 있다.

function curry( fn ) {
  var arity = fn.length;
}

이제 여기서부터 약간 복잡해진다.

기본적으로는, 매번 curry된 함수를 호출할 때마다 새로운 인자를 배열에 넣어 클로저 내에 저장해야 한다. 그 배열에 있는 인자의 수는 원래 함수에서 기대했던 인자의 수와 동일해야 하며, 그 이후 호출 가능해야 한다. 다를 때엔 새로운 함수로 반환한다.

이런 작업을 하기 위해 (1) 인자 목록을 가질 수 있는 클로저가 필요하고 (2) 전체 인자의 수를 확인할 수 있는 함수와 함께, 부분적으로 적용된 함수를 반환 또는 모든 인자가 적용된 원래의 함수에서 반환되는 값을 반환해야 한다.

여기서는 resolver라 불리는 함수를 즉시 실행한다.

function curry( fn ) {
  var arity = fn.length;

  return (function resolver() {

  }());
}

이제 resolver 함수와 함께 해야 할 첫번째 일은 지금까지 입력 받은 모든 인자를 복사하는 것이다. Array#slice 메소드를 이용, arguments의 사본을 memory라는 변수에 저장한다.

function curry( fn ) {
  var arity = fn.length;

  return (function resolver() {
    var memory = Array.prototype.slice.call( arguments );
  }());
}

다음으로 resolver가 함수를 반환하게 만들어야 한다. 함수 외부에서 curry된 함수를 호출하게 될 때 접근할 수 있게 되는 부분이다.

function curry( fn ) {
  var arity = fn.length;

  return (function resolver() {
    var memory = Array.prototype.slice.call( arguments );
    return function() {

    };
  }());
}

이 내부 함수가 실제로 호출이 될 때마다 인자를 받아야 한다. 또한 이 추가되는 인자를 memory에 저장해야 한다. 그러므로 먼저 slice()를 호출해 memory의 복사본을 만들자.

function curry( fn ) {
  var arity = fn.length;

  return (function resolver() {
    var memory = Array.prototype.slice.call( arguments );
    return function() {
      var local = memory.slice();
    };
  }());
}

이제 새로운 인자를 Array#push로 추가한다.

function curry( fn ) {
  var arity = fn.length;

  return (function resolver() {
    var memory = Array.prototype.slice.call( arguments );
    return function() {
      var local = memory.slice();
      Array.prototype.push.apply( local, arguments );
    };
  }());
}

좋다. 이제까지 받은 모든 인자를 새로운 배열에 포함하고 있으며 부분적으로 적용된 함수를 연결(chain)하고 있다.

마지막으로 할 일은 지금까지 받은 인자의 갯수를 실제로 curry된 함수의 인자 수와 맞는지 비교해야 한다. 길이가 맞다면 원래의 함수를 호출하고 그렇지 않다면 resolver가 또 다른 함수를 반환해 인자 수에 맞게 더 입력 받아 memory에 저장할 수 있어야 한다.

function curry( fn ) {
  var arity = fn.length;

  return (function resolver() {
    var memory = Array.prototype.slice.call( arguments );
    return function() {
      var local = memory.slice();
      Array.prototype.push.apply( local, arguments );
      next = local.length >= arity ? fn : resolver;
      return next.apply( null, local );
    };
  }());
}

지금까지 작성한 내용을 앞서 보여줬던 예제와 함께 순서대로 살펴보자.

function volume( l, w, h ) {
  return l * w * h;
}

var curried = curry( volume );

curriedvolume 함수를 앞서 작성한 curry 함수에 넣은 결과가 된다.

여기서 무슨 일이 일어났는지 다시 살펴보면:

  1. volume의 인자 수 즉, 3을 arity에 저장했다.
  2. resolver를 인자 없이 바로 실행했고 그 결과 memory 배열은 비어 있다.
  3. resolver는 익명 함수를 반환했다.

여기까지 이해가 된다면 curry된 함수를 호출하고 길이를 넣어보자.

function volume( l, w, h ) {
  return l * w * h;
}

var curried = curry( volume );
var length = curried( 2 );

여기서 진행된 내용을 살펴보면 다음과 같다:

  1. 여기서 실제로 호출한 것은 resolver에 의해 반환된 익명 함수다.
  2. memory(아직은 비어 있음)를 local에 복사한다.
  3. 인자 (2)를 local 배열에 추가한다.
  4. local의 길이가 volume의 인자 갯수보다 적으므로, 지금까지의 인자 목록과 함께 resolver를 다시 호출한다. 새로운 memory 배열과 함께 새로 생성된 클로저는 첫번째 인자로 2를 포함한다.
  5. 마지막으로, resolver는 클로저 바깥에서 새로운 memory 배열에 접근할 수 있도록 새로운 함수를 반환한다.

이 과정으로 내부에 있던 익명 함수를 다시 반환한다. 하지만 이번에는 memory 배열은 비어 있지 않다. 앞서 입력한, 첫번째 인자인 (인자 2)가 내부에 있다.

앞서 만든 length 함수를 다시 호출한다.

function volume( l, w, h ) {
  return l * w * h;
}

var curried = curry( volume );
var length = curried( 2 );
var lengthAndWidth = length( 3 );
  1. 여기서 호출한 것은 resolver에 의해 반환된 익명 함수다.
  2. resolver는 앞에서 입력한 인자를 포함하고 있다. 즉 배열 2 를 복사해 local에 저장한다.
  3. 새로운 인자인 3local 배열에 저장한다.
  4. 아직도 local의 길이가 volume의 인자 갯수보다 적으므로, 지금까지의 인자 목록과 함께 resolver를 다시 호출한다. 그리고 이전과 동일하게 새로운 함수를 반환한다.

이제 lengthAndWidth 함수를 호출해 값을 얻을 차례다.

function volume( l, w, h ) {
  return l * w * h;
}

var curried = curry( volume );
var length = curried( 2 );
var lengthAndWidth = length( 3 );

console.log( lengthAndWidth( 4 ) ); // 24

여기서의 순서는 이전과 약간 다르다.

  1. 다시 여기서 호출한 함수는 resolver에서 반환된 익명 함수다.
  2. resolver는 앞에서 입력한 인자를 포함한다. 배열 [ 2, 3 ]를 복사해 local에 저장한다.
  3. 새로운 인자인 4local 배열에 저장한다.
  4. 이제 local의 길이가 volume의 인자 갯수와 동일하게 3을 반환한다. 그래서 새로운 함수를 반환하는 대신 지금까지 입력 받아서 저장해둔 모든 인자와 함께 volume 함수를 호출해 결과를 반환 받는다. 그 결과로 24 라는 값을 받게 된다.

정리

아직 이 커링 기법을 필수적으로 적용해야만 하는 경우를 명확하게 찾지는 못했다. 하지만 이런 방식으로 함수를 작성하는 것은 함수형 프로그래밍에 대한 이해를 향상할 수 있는 좋은 방법이고 클로저와 1급 클래스 함수와 같은 개념을 강화하는데 도움을 준다.

현재 Project Decibel에서 구인중이다. 보스턴 지역에서 이런 JavaScript 일을 하고 싶다면 이메일을 부탁한다.

그리고 이 포스트가 좋다면 내 트위터를 구독하라. 다음 한 달 중 하루는 글을 쓰기 위해 노력할 예정이다.


새로운 개념을 배워가는 과정에서 JavaScript의 새 면모를 배우게 되는 것 같아 요즘 재미있게 읽게 되는 글이 많아지고 있다. 지금 회사에서 JS를 front-end에서 제한적으로 사용하고 있는 수준이라서 아쉽다는 생각이 들 때도 많지만 이런 포스트를 통해 매일 퍼즐을 풀어가는 기분이라 아직도 배워야 할 부분이 많구나 생각하게 된다.

벌써 2015년도 반절이 지났다. 여전히 어느 것 하나 깊게 알고 있는 것이 없는 기분이라 아쉬운데 남은 한 해는 겉 알고 있는 부분을 깊이있게 접근할 수 있는 끈기를 챙길 수 있었으면 좋겠다.

이상한모임 슬랙 #dev-frontend 채널에서 Lodash에 대해 이야기하다 지연 평가(Lazy Evaluation)를 지원한다는 이야기를 듣고 검색하게 되었다. 검색 결과로 찾은, Filip Zawada의 How to Speed Up Lo-Dash ×100? Introducing Lazy Evaluation 포스트를 번역한 글이다.

지연평가는 필요할 때만 수행하는 평가 방식으로 함수형 프로그래밍에서는 널리 사용되는 방법이다. 이 글은 lodash 뿐만 아니라 지연평가가 어떤 방식으로 접근하고 동작하는가에 대해 쉽게 설명하고 있다.


Lo-Dash와 같은 라이브러리는 더이상 빨라질 수 없을 정도로 충분히 빠르다고 항상 생각했다. Lo-Dash는 자바스크립트를 짜내다시피 해 다양한 기술을 완벽하게 잘 섞었다. 이 라이브러리는 JavaScript의 가장 빠른 문장, 적응형 알고리즘을 위해 사용하며 때로는 부수적으로 발생하는 예기치 못한 재귀를 피하기 위해 성능을 측정할 때에도 사용한다.

지연 평가 Lazy Evaluation

하지만 내가 잘못 생각했다. Lodash는 획기적으로 빨라지는 것이 가능했다. 이 일에 필요한 것은 미세한 최적화에 대한 생각을 멈추고 올바른 알고리즘을 사용하고 있는지 살피는 것으로 시작해야 한다. 예를 들면, 일반적인 반복문에서 반복에 걸리는 단위 시간을 최적화하려 한다:

var len = getLength();
for(var i = 0; i < len; i++) {
    operation(); // <- 10ms - 어떻게 9ms로 만들 수 있을까?!
}

이런 경우는 대부분 어렵고 매우 제한적이다. 때로는 getLength()를 최적화 하는 것이 더 의미있다. 이 함수가 반환하는 값이 작을수록, 10ms 주기는 짧아진다.

다음 코드는 간략하게 작성한 Lodash에서 지연평가를 하는 방식이다. 이 방법은 주기의 횟수를 줄이는 것이지 주기에 걸리는 시간을 줄이는 것이 아니다. 다음 예를 고려해보자:

function priceLt(x) {
   return function(item) { return item.price < x; };
}
var gems = [
   { name: 'Sunstone', price: 4 }, { name: 'Amethyst', price: 15 },
   { name: 'Prehnite', price: 20}, { name: 'Sugilite', price: 7  },
   { name: 'Diopside', price: 3 }, { name: 'Feldspar', price: 13 },
   { name: 'Dioptase', price: 2 }, { name: 'Sapphire', price: 20 }
];

var chosen = _(gems).filter(priceLt(10)).take(3).value();

$10보다 작은 가격의 보석 3개를 고르려고 한다. 일반적인 Lodash 접근인 엄격한 평가에서는 8개의 보석을 모두 걸러낸 후 앞 3개를 골라낸다:

Lodash naïve approach

충분히 쿨하지 않다. 이 방식은 8개의 모든 요소를 처리하지만 사실 필요로 하는 것은 그 중 5개 뿐이다. 지연 평가 알고리즘에서는 이 방식과 대조적으로, 배열에서 적은 숫자의 요소를 가져와 올바른 결과를 얻는다. 다음을 살펴보자:

Lo-Dash regular approach

이 방식으로 쉽게 37.5% 성능 향상을 만들었다. 이는 단순한 예시이며 사실 1000+배 성능 향상이 있는 예도 들 수 있다. 다음을 보자:

var phoneNumbers = [5554445555, 1424445656, 5554443333, … ×99,999];

// "55"가 포함된 전화번호 100개를 획득
function contains55(str) {
    return str.contains("55");
};

var r = _(phoneNumbers).map(String).filter(contains55).take(100);

이 예제에서 99,999개의 요소를 검사하게 되는데, 이 모두를 다 실행하지 않고 예를 들어 1,000개의 요소만 검사해도 결과를 얻을 수 있게 된다. 벤치마크에서 이 엄청난 성능 향상을 확인할 수 있다:

benchmark

파이프라이닝

지연 평가에 또 다른 잇점이 있는데 “파이프라이닝” 이라고 부른다. 이 아이디어는 체인으로 실행되는 동안 값이 전달되기 위해 배열이 생성되는 경우를 회피한다는 점이다. 모든 동작은 하나의 요소에 한번에 실행되야 한다. 다음 코드를 보면:

var result = _(source).map(func1).map(func2).map(func3).value();

간단하게 Lo-Dash가 어떻게 해석하는지 작성하면 다음과 같다. (엄격한 평가)

var result = [], temp1 = [], temp2 = [], temp3 = [];

for(var i = 0; i < source.length; i++) {
   temp1[i] = func1(source[i]);
}

for(i = 0; i < source.length; i++) {
   temp2[i] = func2(temp1[i]);
}

for(i = 0; i < source.length; i++) {
   temp3[i] = func3(temp2[i]);
}
result = temp3;

반면 지연 평가에서는 다음과 같이 실행된다:

var result = [];
for(var i = 0; i < source.length; i++) {
   result[i] = func3(func2(func1(source[i])));
}

임시 배열이 존재하지 않는다는 점은 극적인 성능 향상을 가져온다. 특히 배열이 크고 메모리 접근이 비싼 경우에서는 말이다.

유예 실행 Deferred execution

지연 평가가 가져온 또 다른 잇점은 유예 실행이다. 체인을 만들게 되면 언제나 .value()를 명시적으로나 암시적으로 호출하기 전까지는 연산되지 않는다. 이 접근은 쿼리를 먼저 준비하게 하고 나중에 실행하게 해 가장 최신의 데이터를 얻게 된다.

var wallet = _(assets).filter(ownedBy('me'))
                      .pluck('value')
                      .reduce(sum);

$json.get("/new/assets").success(function(data) {
    assets.push.apply(assets, data); // update assets
    wallet.value(); // returns most up-to-date value
});

이와 같은 방식은 몇가지 경우에서 또한 속도 향상을 가져온다. 실행 속도가 중요한 경우에 복잡한 쿼리를 일찍 만든 후 나중에 실행할 수 있게 된다.

정리

지연 평가는 새로운 아이디어가 아니다. 이미 LINQ, Lazy.js 등 여러 뛰어난 라이브러리에서 사용하고 있다. 내가 믿기에 Lo-Dash가 만든 큰 차이는 Underscore API를 그대로 유지하면서도 새롭고 강력한 내부의 엔진을 얻게 되었다는 사실이다. 새로운 라이브러리를 배울 필요도, 작성한 코드를 크게 변경할 필요도 없이 라이브러리를 업그레이드하면 된다.

Lo-Dash를 사용하지 않더라도 이 글이 영감을 줬기를 바란다. 자신의 어플리케이션에서 병목을 찾아 jsperf.com의 try/fail 스타일의 최적화는 그만 할 때도 되었다. 대신 나가서 커피를 마시며 알고리즘에 대해 생각해야 할 때다. 창의성이 중요하지만 알고리즘 개론와 같은 좋은 책으로 배경지식을 다지는 것도 좋다. 행운을 빈다!


번역에 피드백 주신 Heejoon Lee님 감사드립니다.

이상한모임 슬랙 #dev-frontend 채널에서 함수가 1급 시민이라는 얘기가 나온 적이 있었다. Wikipedia를 읽다가 Partial Application에 대한 이야기가 있어 검색하던 중 John Resig이 작성한 Partial Application in JavaScript를 읽게 되었다. 2008년 글이라 요즘 코드와는 조금 다른 부분이 있지만 개념을 잡기에는 충분히 도움이 되는 것 같아 번역했다.


면밀하게 보면, 부분만 사용한 함수는 함수가 실행되기 전에 미리 인자를 지정할 수 있는, 흥미로운 기법이다. 이와 같은 효과로 부분만 반영된 함수는 호출할 수 있는 새로운 함수를 반환한다. 다음 예제를 통해 이해할 수 있다:

String.prototype.csv = String.prototype.split.partial(/,\s*/);
var results = "John, Resig, Boston".csv();
alert( (results[1] == "Resig") + " The text values were split properly." );

위에서는 일반적으로 사용하는 String의 .split() 메소드에 인자로 미리 정규표현식을 저장하는 경우다. 그 결과로 만들어진 새로운 함수 .csv()를 쉼표로 분리된 값을 배열로 변환하는데 사용할 수 있다. 함수 인자를 앞에서부터 필요한 만큼 채우고 새로운 함수를 리턴하는 방식을, 일반적으로 커링(currying)이라 부른다. 간단하게 커링은 어떻게 구현되는지 다음 프로토타입 라이브러리에서 확인할 수 있다:

Function.prototype.curry = function() {
  var fn = this, args = Array.prototype.slice.call(arguments);
  return function() {
    return fn.apply(this, args.concat(
      Array.prototype.slice.call(arguments)));
  };
};

상태를 기억하기 위해 클로저(closure)를 사용한 좋은 케이스다. 이 경우에 미리 입력한 인수(args)를 저장하기 위해서 새로 만들어지는 함수에 전달되었다. 새로운 함수는 인수가 미리 입력되게 되고 새로운 인수도 하나로 합쳐져(concat) 전달된다. 그 결과, 이 메소드는 인수를 미리 입력할 수 있게 되고 활용 가능한 새 함수를 반환하게 된다.

이제 이 스타일의 부분 어플리케이션은 완전 유용하지만 더 좋게 만들 수 있다. 만약 주어진 함수에서 단순히 앞에서부터 인수를 입력할 것이 아니라 비어있는 모든 인수를 채우기 원한다면 어떻게 해야할까. 다음과 같은 형태의 부분 어플리케이션 구현은 다른 언어에도 존재하지만 JS에서는 Oliver Steele가 Function.js 라이브러리에서 시연했다. 다음 구현을 살펴보자:

Function.prototype.partial = function (){
  var fn = this, args = Array.prototype.slice.call(arguments);
  return function(){
    var arg = 0;
    for ( var i = 0; i < args.length && arg < arguments.length; i++)
      if ( args[i] === undefined )
        args[i] = arguments[arg++];
    return fn.apply(this, args);
  }
}

이 구현은 근본적으로 curry() 메소드와 비슷하지만 중요한 차이점이 존재한다. 특히 이 함수가 호출될 때, 미리 입력하고 싶지 않은 인수에 대해 undefined를 입력하는 것으로 나중에 입력하도록 만들 수 있다. 이 방식의 구현은 인수를 병합하는데 더 편리하게 활용할 수 있게 돕는다. 인수를 배정하는 과정에서 비어있는 곳에 적절한 간격으로 처리해 나중에 실행할 때 조각을 맞출 수 있게 만든다.

위에서 문자열 분리 함수를 생성하는데 사용한 예에도 있지만 다른 방식에서 어떻게 새 함수 기능을 활용할 수 있는지 확인하자. 함수를 간단하게 지연해서 실행하도록 하는 함수를 생성할 수 있다.

var delay = setTimeout.partial(undefined, 10);
delay(function(){
  alert( "A call to this function will be temporarily delayed." );
});

delay라는 이름의 새로운 함수를 만들었다. 언제든 함수를 인자로 넣으면 10ms 후에 비동기적으로 실행하게 된다.

이벤트를 연결(binding) 하기 위한, 간단한 함수를 만들 수 있다:

var bindClick = document.body.addEventListener
                  .partial('click', undefined, false);

bindClick(function() {
  alert( "Click event bound via curried function." );
});

이 기법은 라이브러리에서 이벤트를 연결하기 위해 사용하는, 간단한 헬퍼 메소드로 사용할 수 있다. 이 결과로 단순한 API를 제공해 최종 사용자가 불필요한 인수로 인해 번거롭게 되는 경우를 줄이고 단일 함수를 호출하는 횟수를 줄일 수 있다.

클로저를 사용하면 결과적으로 코드에서의 복잡도를 쉽고 간단하게 줄일 수 있어서 JavaScript 함수형 프로그래밍의 강력함을 확인하게 된다.

요즘 출퇴근 하는 시간에는 눈도 쉴 겸 팟캐스트를 자주 듣는다. 그 중 Hanselminutes을 애청하고 있는데 Scott Hanselman이 여러 분야 사람들을 인터뷰하는 방식으로 진행되는 팟캐스트다. 이 팟캐스트에서 진행한 Getting started making NodeBots and Wearables 에피소드에서 NodeBots 프로젝트Johnny-Five.io에 대해 알게 되어 살펴보게 되었다.

NodeBots

NodeBots 프로젝트는 말 그대로 JavaScript를 이용해 로봇공학을 배우는 프로젝트로 세계 각지에서 진행되고 있다고 한다. JS를 사용할 수 있는 이점을 살려 쉽고 재미있는 과정을 제공하고 있는데 2015년 7월 25일은 국제 NodeBots의 날로 각 지역별로 프로그램이 진행된다.(멜번에서도!) 아쉽게도 한국에는 아직 오거나이저가 없는 것 같다.

Johnny-Five는

Johnny-Five는 JavaScript 로봇공학 프로그래밍 프레임워크로, 이전 포스트인 ino toolkit으로 Arduino 맛보기에서 C 문법 스타일의 sketch를 사용한 반면 이 프레임워크로 JavaScript를 이용해 제어할 수 있다. 그리고 REPL을 제공하고 있어서 실시간으로 데이터를 확인하거나 nodejs의 다양한 라이브러리도 활용할 수 있다. NodeBots 세션에서는 손쉽게 웹API로 만들어 브라우저를 통해 제어하는 등 이전 환경에서는 만들기 까다로웠던 부분을 재미있게 풀어가는데 활용하고 있다. 게다가 이 프레임워크는 아두이노에만 국한된 것이 아니라 다양한 개발 보드를 지원하고 있는 것도 장점이다.

그 사이 주문한 서보 모터는 도착했는데 서보 실드나 브래드보드가 도착하지 않아서 여전히 LED 깜빡이는 수준이라 아쉽다. 🙁 이 포스트에서는 Raspberry Pi에 Arduino Uno를 연결해서 진행했다.

요구 환경

OSX에서는 Node.js, Xcode, node-gyp가 필요하고 Windows에서는 Node.js, VS Express, Python 2.7, node-gyp가 필요하다.

$ npm install --global node-gyp

요구 사항은 Getting Started 페이지에서 확인할 수 있다.

Firmata 설치하기

Arduino에서 Johnny-Five를 사용하기 위해서는 Firmata를 먼저 설치해야 한다. Firmata는 마이크로 컨트롤러를 소프트웨어로 조작하기 위한 프로토콜인데 펌웨어 형태로 제공되고 있어 arduino에 설치하기만 하면 된다.

Arduino IDE를 사용하고 있다면 아두이노를 연결한 후, File > Examples > Firmata > StandardFirmata 순으로 선택한 후 Upload 버튼을 클릭하면 된다고 한다.

CLI 환경에서 작업하고 있는 경우에는 Firmata 코드를 받아 ino로 빌드 후 업로드할 수 있다. 여기서는 v2.4.3 이지만 Firmata github에서 최신인지 확인하자.

$ wget https://github.com/firmata/arduino/releases/download/v2.4.3/Arduino-1.6.x-Firmata-2.4.3.zip
$ unzip Arduino-1.6.x-Firmata-2.4.3.zip
$ cd ./Firmata/

# StandardFirmata.ino를 복사해서 빌드에 포함시킴
$ cp ./examples/StandardFirmata/StandardFirmata.ino ./src

이 상황에서 바로 빌드하면 에러가 난다. StandardFirmata.ino를 에디터로 열어 다음 코드를 찾는다.

#include <Firmata.h>

그리고 다음처럼 Firmata.h 파일을 폴더 내에서 찾도록 수정한다.

#include "./Firmata.h"

모든 준비가 끝났다. USB로 연결한 후, ino로 빌드와 업로드를 진행한다.

$ ino build
$ ino upload

Firmware를 생성하고 업로드하는 과정을 화면에서 바로 확인할 수 있다. 이제 Johnny-five를 시작하기 위한 준비가 끝났다.

Johnny-five로 LED 깜빡이 만들기

앞서 과정은 좀 복잡했지만 johnny-five를 사용하는건 정말 간단하다. 먼저 nodejs가 설치되어 있어야 한다. 프로젝트를 만들고 johnny-five를 npm으로 설치한다.

$ mkdir helloBlinkWorld
$ cd helloBlinkWorld
$ npm init # 프로젝트 정보를 입력
$ npm install --save johnny-five

설치가 모두 완료되면 blink.js를 생성해 다음 JavaScript 코드를 입력한다.

var five = require("johnny-five"),
    board = new five.Board();

board.on("ready", function () {

  // 13은 보드에 설치된 LED 핀 번호
  var led = new five.Led(13);

  // 500ms으로 깜빡임
  led.blink(500);

});

정말 js다운 코드다. 위 파일을 저장하고 node로 실행하면 보드와 연동되는 것을 확인할 수 있다. (아쉽게도 동영상은 만들지 않았다 🙂 더 재미있는 예제를 기약하며)

$ node blink.js

JavaScript가 다양한 영역에서 사용되고 있다는 사실은 여전히 신기하다. 이 프레임워크도 상당히 세세하게 많이 구현되어 있어서 단순히 JS 로보틱스 입문 이상으로도 충분히 활용할 수 있겠다는 인상을 받았다. 조만간 Tessel 2도 나올 예정인데 이 기기의 js 사랑도 이 라이브러리와 견줄만 할 정도라 많이 기대된다.

지난번 구입한 Raspberry Pi에 이어 이번엔 Arduino가 도착했다. 첫인상으로 비교했을 땐 Raspberry Pi는 똑똑하고 Arduino는 우직한 기분이 든다. 🙂 Arduino는 모든 정보가 오픈소스로 공개되어 있어서 훨씬 다양한 종류의 보드가 존재한다. Arduino가 적혀 있는 공식 보드도 있지만 자체 브랜드가 적혀있거나 아예 아무 내용도 적혀있지 않은, 저렴한 보드도 있다.

Arduino의 모습

ebay를 통해서 5달러로 ATmega328P가 탑재된 아두이노 호환 보드를 구입했다. 이외에도 소켓 브레드보드, ESP8266 WIFI 무선 송수신 모듈, Servo를 위한 모듈을 구입했는데 오늘 보드가 먼저 도착했다. 저항도 구입해야 하는데… 낱개로는 저렴하지만 결국 비싼 취미가 되고 있는 느낌이다.

별도 모듈 없이 보드 자체만 가지고서는 보드에 있는 LED를 껐다 켰다 하는 것이 할 수 있는 전부다. 기본적으로 보드 메모리에 설치되어 나오는 것도 이 LED를 껐다 켰다 하는 _blink_인데 전원을 넣으면 LED가 깜빡이는 것을 확인할 수 있다. 다음 사진에서 빨간 LED는 전원이고 초록색은 조작할 수 있는 LED다.

이 포스트에서는 Raspberry Pi에 Arduino를 USB로 연결해 진행했다. 여기서 사용한 ino 툴킷은 아쉽게도 맥과 리눅스 환경에서만 구동 가능하다. 만약 윈도우 환경이라면 Arduino IDE를 사용하자.

먼저 Raspberry Pi와 Arduino가 서로 통신할 수 있도록 드라이버를 설치한다. sudo를 쓰지 않아도 되는 환경이라면 사용하지 않아도 무관하다.

$ sudo apt-get install arduino

GUI 환경에서는 Arduino IDE를 설치하면 되지만 콘솔에서 작업하고 싶다면 Python으로 작성된 ino 툴킷을 사용하면 된다. 이 라이브러리는 pip 또는 easy_install로 설치할 수 있다.

$ sudo pip install ino

pip가 없다면 다음과 같이 easy_install을 사용할 수 있다. 물론 리포지터리를 받아 setup.py를 실행해도 된다.

$ sudo apt-get install python-setuptools
$ easy_install ino

Arduino의 HelloWorld인 blink 프로젝트를 만들어보자. blink 프로젝트를 템플릿으로 사용해 프로젝트를 초기화한다.

$ mkdir helloWorld
$ cd helloWorld
$ ino init --template blink

lib 디렉토리와 src 디렉토리, 그리고 src/sketch.ino 가 생성된 것을 확인할 수 있다. sketch.ino를 열어보면 blink 템플릿 내용을 확인할 수 있다.

#define LED_PIN 13

void setup()
{
    pinMode(LED_PIN, OUTPUT);
}

void loop()
{
    digitalWrite(LED_PIN, HIGH);
    delay(100);
    digitalWrite(LED_PIN, LOW);
    delay(900);
}

이제 이 코드를 빌드해서 arudino에 올려보자. ino buildino upload로 간단하게 빌드, 업로드 할 수 있다.

$ ino build

위 명령어를 입력하면 빌드 과정을 보여준다. firmware.hex가 변환되고 업로드 할 준비가 완료된다. 이제 업로드를 진행한다.

$ ino upload

업로드가 진행되고 arduino에 있는 LED가 위에서 입력한, 새로운 패턴으로 깜박이게 된다.


아직 다양한 모듈이 없어서 맛보기만 했지만 LED 깜빡이는 것만 봐도 신기하다. 조만간 다른 모듈이 오면 더 재미있는 Thing을 만들 생각에 기대된다.

Client-side에서 패키지 관리를 위해서 bower를 자주 사용하는 편인데 bower는 module loader가 아니라 정말 말 그대로 패키지만 받아서 bower_components 에 넣어주는 정도의 역할만 하기 때문에 부수적인 작업이 많이 필요한 편이다. jspm은 module loader인 SystemJS를 기반으로 모듈을 불러온다. Traceur이나 Babel도 쉽게 적용할 수 있어 ES6 기준으로 개발하는데 편리하다.

jspm-cli를 먼저 설치한다.

$ npm install --global jspm/jspm-cli

프로젝트 폴더에서 jspm init로 프로젝트를 초기화 한다. 초기화 과정에서 프롬프트로 기본적인 사항들을 입력 받는데 ES6를 위해 어떤 transpiler를 사용할지 정할 수 있다. 여기서 설정한 모든 내용은 package.json에 저장되며 SystemJS를 위한 config.js 파일도 자동으로 생성된다.

$ jspm init

jspm으로 npm과 github에 있는 라이브러리를 쉽게 설치할 수 있다. (jspm install registry:package@version) 또한 alias로 등록되어 있는 라이브러리는 registry를 입력하지 않고도 설치할 수 있다. 그 목록은 registry 리포지터리에서 확인할 수 있다.

$ jspm install github:github/fetch
$ jspm install fetch

위에는 github의 fetch 구현을 사용했지만 whatwg-fetch를 사용할 수도 있다. 패키지를 사용할 때 패키지명을 다음과 같이 맵핑해서 사용할 수 있다. 다시 말하면 fetch에 맵핑하면 import "whatwg-fetch" 대신 import "fetch"로 불러오는 것이 가능하다.

$ jspm install fetch=npm:whatwg-fetch

다음 코드대로 index.html을 작성한다.

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>Hello World</title>
    <script src="jspm_packages/system.js"></script>
    <script src="config.js"></script>
    <script>
        System.import('app/main'); // 진입점이 되는 js 경로
    </script>
</head>
<body></body>
</html>

app/main.js를 작성한다. github에서 haruair의 리포지터리 갯수를 구해 console.log로 출력한다.

import 'fetch'

fetch('https://api.github.com/users/haruair/repos')
  .then(response => response.json())
  .then(repos => console.log(repos.length))

이제 결과를 확인해보자. php -S localhost:8000 또는 python -m SimpleHTTPServer 8000으로 간단하게 서버를 띄워 console을 확인한다.

jspm의 장점은 js 뿐만 아니라 css와 같은 파일도 import 할 수 있어 컴포넌트 형태로 개발하기 쉽다. Webpack에서도 각 확장별로 loader를 지정해주면 알아서 처리해주지만 jspm은 설정 파일을 조작할 필요 없이 단순히 jspm의 systemjs/plugin-css만 설치하면 되는 편리함이 있다.

$ jspm install css

app/style.css를 간단하게 추가해서 확인해보자.

body {
  background: #0dd0dd;
}

방금 작성한 css 파일을 js에서 import 하는 것이 가능하다. 다음 코드를 app/main.js에 추가하면 페이지에서 css를 불러오는 것을 확인할 수 있다.

import './style.css!'

css 외에도 less도 가능하다. sass는 아직 systemJS을 지원하는 플러그인이 없는 것으로 보인다.

패키지 묶기 (bundle)

jspm은 bundle도 지원한다. 간단하게 jspm bundle 명령어로 묶을 수 있고 app/main과 같이 진입점을 사용하고 있는 경우 jspm bundle-sfx app/main -o <outfile> 식으로 묶을 수 있다.

Jack Franklin의 London React Meetup, ES6 Modules & React with SystemJS에서는 uglifyjs와 html-dist 패키지를 함께 활용해 다음과 같은 Makefile을 만들어 make build로 묶을 수 있도록 만들었다.

먼저 uglifyjs와 html-dist를 npm으로 설치해야 한다.

$ npm install --save-dev uglifyjs html-dist

다음 내용으로 Makefile을 만든다. 2행 이후의 내용은 모두 스페이스가 아닌 탭으로 시작해야 한다.

build:
    -rm -r dist/
    mkdir dist
    jspm bundle-sfx app/main -o dist/app.js
    ./node_modules/.bin/uglifyjs dist/app.js -o dist/app.min.js
    ./node_modules/.bin/html-dist index.html --remove-all --minify --insert app.min.js -o dist/index.html

이제 make build로 dist 폴더 내에 minify된 index.html과 번들된 app.js, app.map, 그리고 uglifyjs로 변환된 app.min.js를 확인할 수 있다.


jspm의 아쉬운 점은 아직 브라우저에서만 명확하게 동작한다는 점이다. registry에 등록되지 않은 패키지도 npm이나 github에서 직접 받을 수 있도록 지원하고 있지만 브라우저에서 제대로 지원하지 않는 라이브러리의 경우 사용할 수 없다. 또한 isomorphic한 방식으로 접근할 때 nodejs가 jspm으로 설치한 라이브러리를 불러오지 못하기 때문에 pre-rendering을 하려 한다면 중복되는 패키지를 다시 받아야 하는 불편함이 있다. 하지만 jspm은 무엇보다 설정을 배우는 것에 시간을 많이 쓰지 않고서도 ES6를 바로 사용할 수 있다는 장점이 있다. 또한 cli도 상당히 단순해서 다른 도구과 달리 바로 써먹을 수 있는 즐거움이 있다. 이 jspm이 서버측, 클라이언트측 모두 사용할 수 있도록 나온다면 말 그대로 강력함을 뽐내지 않을까 상상해본다. 🙂

웹사이트 설정

웹페이지 색상을 선택하세요

Darkreader 플러그인으로 선택한 색상이 제대로 표시되지 않을 수 있습니다.