c4se記:さっちゃんですよ☆

.。oO(さっちゃんですよヾ(〃l _ l)ノ゙☆)

.。oO(此のblogは、主に音樂考察Programming に分類されますよ。ヾ(〃l _ l)ノ゙♬♪♡)

音樂は SoundCloud に公開中です。

考察は現在は主に Scrapbox で公表中です。

Programming は GitHub で開發中です。

コメントでJavaScriptに型注釈を書き実行時に自動で型検査をする

此の記事はNode.js Advent Calendar 2014の18日目です。昨日はSails.js と Vue.js で API ファースト + リアルタイムな Web アプリケーションをさくっと作るでした。明日は id:slowquery さんです。

io.jsとは何故ですか。

Proxyが使へればなんでもできさうな気がしますね。

さて、あまりnode.jsといふよりはJavaScriptの話となります。
JavaScriptの型検査といへば、TypeScriptやFlowやAtScriptやES6など事前に型検査を行なふことになってゐます。jsのままやらうとしてもEsprimaなどで構文解析し、依存のグラフを作ってゆかねばなりません。事前の型検査を實裝[実装]する大変ですが、實行[実行]時の検査は、値が手元にあるので簡単です。そこでもしmacroが簡単にJavaScriptで使へるのだとしたら (假定[仮定])、實行時の型検査を實裝するのは簡単です (結論)。

函數[関数]のtoStringで関数のソースコードが實行時に讀め[読メ]ますので、これを使うてmacroを書くことができます (事實[事実])。故に (略)。以前にAngularJSのやうなinjectionの雑な實裝をやったことがありました。
cf. JavaScriptで引数名から自動でDIする http://c4se.hatenablog.com/entry/2014/03/29/074201

コメントを型アノテーションの代はりにする

ところでこのtoStringですが、函數内のコメントも消されずに讀むことができます。そこでJSDocを無視して、函數に型アノテーションを書いてみます。

function f(n/*:number*/)/*:string*/ {
  return '' + n;
}

console.log(f.toString());
// => function f(n/*:number*/)/*:string*/ {
//      return '' + n;
//    }

toStringを使ひ、函數を型検査つきの函數に変換するものを書きます。checkType()と云ふ型検査する函數が有るものとします。

function typed(func/*:function*/)/*:function*/ {
  var params, returnType;

  params = func.toString().
    replace(/\r?\n/g, ' ').
    match(/function[^\(]*\(([^\)]*)\)/)[1].
    split(',').
    map(function (param) {
      var m;

      m = param.trim().match(/\/\*:([\w?]+)\*\/$/);
      return m ? m[1] : void 0;
    });
  returnType = func.toString().match(/function[^\(]*\([^\)]*\)([^{]*){/)[1].
    trim().
    match(/\/\*:([\w?]+)\*\//);
  returnType = returnType ? returnType[1] : void 0;
  return function () {
    var result, i = 0, iz = 0;

    for (i = 0, iz = arguments.length; i < iz; ++i) {
      checkType(arguments[i], params[i]);
    }
    result = func.apply(null, arguments);
    checkType(result, returnType);
    return result;
  };
}

function f(n/*:number*/)/*:string*/ {
  return '' + n;
}
f = typed(f);
f(1);   // => '1'
f('1'); // => TypeError

checkTypeの實裝は以下のものです。nullableや配列型にも対応させました。上記のtypedの實裝があまりに適当であるにもかかはらずです。

var assert = require('assert');

// https://plus.google.com/u/0/+InoueSachirou/posts/cGtJLikXC4P
// https://gist.github.com/SirAnthony/e1c7b9522e7ab02c6679
// http://jsperf.com/typeof-v-s-instanceof
var typeCheckers = {
  string     : function (obj) { return 'string'   === typeof obj || obj instanceof String; },
  'String'   : function (obj) { return 'string'   === typeof obj || obj instanceof String; },
  number     : function (obj) { return 'number'   === typeof obj || obj instanceof Number; },
  'Number'   : function (obj) { return 'number'   === typeof obj || obj instanceof Number; },
  'boolean'  : function (obj) { return 'boolean'  === typeof obj || obj instanceof Boolean; },
  'Boolean'  : function (obj) { return 'boolean'  === typeof obj || obj instanceof Boolean; },
  'function' : function (obj) { return 'function' === typeof obj; },
  object     : function (obj) { return 'object'   === typeof obj; },
  array      : Array.isArray,
};

function checkType(obj/*:object?*/, type/*:string?*/)/*:undefined*/ {
  var constructor, i = 0, iz = 0;

  if (!type) { return; }
  if (type[type.length - 1] === '?') {
    if (null === obj || void 0 === obj) { return; }
    type = type.slice(0, type.length - 1);
  } else if (null === obj || void 0 === obj) {
    throw new TypeError(obj+' is not a '+type);
  }
  if (type.endsWith('[]')) {
    if (!Array.isArray(obj)) {
      throw new TypeError(obj+' is not a '+type);
    }
    type = type.slice(0, type.length - 2);
    for (i = 0, iz = obj.length; i < iz; ++i) {
      checkType(obj[i], type);
    }
    return;
  }
  if (typeCheckers[type]) {
    if (typeCheckers[type](obj)) { return; }
    throw new TypeError(obj+' is not a '+type);
  }
  while (obj) {
    if (obj.constructor.name === type) { return; }
    obj = obj.__proto__;
  }
  throw new TypeError(obj+' is not a '+type);
}
// {{{!test
(function () {
checkType('A',            'string');
checkType('',             'string');
checkType(String(''),     'string');
checkType(new String(''), 'string');
checkType('A',            'String');
checkType('',             'String');
checkType(String(''),     'String');
checkType(new String(''), 'String');

checkType(1,             'number');
checkType(0,             'number');
checkType(Number(0),     'number');
checkType(new Number(0), 'number');
checkType(NaN,           'number');
checkType(Infinity,      'number');
checkType(1,             'Number');
checkType(0,             'Number');
checkType(Number(0),     'Number');
checkType(new Number(0), 'Number');
checkType(NaN,           'Number');
checkType(Infinity,      'Number');

checkType(true,               'boolean');
checkType(false,              'boolean');
checkType(Boolean(false),     'boolean');
checkType(new Boolean(false), 'boolean');
checkType(true,               'Boolean');
checkType(false,              'Boolean');
checkType(Boolean(false),     'Boolean');
checkType(new Boolean(false), 'Boolean');

checkType(function () {},   'function');
checkType(Function(''),     'function');
checkType(new Function(''), 'function');
checkType(function () {},   'Function');
checkType(Function(''),     'Function');
checkType(new Function(''), 'Function');

checkType({},           'object');
checkType(Object(),     'object');
checkType(new Object(), 'object');
checkType({},           'Object');
checkType(Object(),     'Object');
checkType(new Object(), 'Object');

checkType([],          'array');
checkType(Array(),     'array');
checkType(new Array(), 'array');
checkType([],          'Array');
checkType(Array(),     'Array');
checkType(new Array(), 'Array');

checkType(/r/,            'RegExp');
checkType(RegExp(''),     'RegExp');
checkType(new RegExp(''), 'RegExp');

function Parent() { }
function Child() { Parent.call(this); }
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;
checkType(new Parent, 'Parent');
checkType(new Child,  'Child');
checkType(new Child,  'Parent');

assert.throws(function () { checkType(0, 'string'); }, TypeError);
assert.throws(function () { checkType(new Parent, 'Child'); }, TypeError);

assert.throws(function () { checkType(null, 'number'); }, TypeError);
checkType(null,   'number?');
checkType(void 0, 'number?');
checkType(0,      'number?');

checkType([0], 'number[]');
checkType([],  'number[]');
assert.throws(function () { checkType(0,       'number[]'); }, TypeError);
assert.throws(function () { checkType([0, ''], 'number[]'); }, TypeError);
}());
// }}}!test

次のやうに、引數[引数]と返り値を検査できます。

var f = typed(function (a/*:string*/)/*:number*/ {
  return a;
});
try { f('str'); } catch (err) { console.error(err); }
try { f(42); } catch (err) { console.error(err); }


var g = typed(function (a/*:number*/, b/*:number?*/)/*:number*/ {
  b = b || 0;
  return a + b;
});
console.log(g(5, 6));
console.log(g(5));
try { g(5, '') } catch (err) { console.error(err); }


function A() {
  this.a = 42;
}
A.prototype.toString = function () {
  return 'A{a='+this.a+'}';
};
var h = typed(function (a/*:A*/)/*:A*/ {
  return a;
});
console.log(h(new A()));
try { h(42);   } catch (err) { console.error(err); }
try { h(null); } catch (err) { console.error(err); }
try { h();     } catch (err) { console.error(err); }

これを纏めると、以下のものになります。

JS コメントで型注釈を書き実行時に自動で型検査 http://jsdo.it/ne_sachirou/2Z6h
https://gist.github.com/ne-sachirou/562a7bd59772147a83df

以上。