此の記事は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
以上。