読者です 読者をやめる 読者になる 読者になる

CoffeeScriptでUnderscore.jsと戯れる

CoffeeScriptとUnderscore.jsの組み合わせで関数型言語のようなリスト主体のプログラミングをすることができます。せっかくなので_.mixin_オブジェクトを拡張してみます。

keysとvaluesからObjectを作成する_.pairs

Underscore.jsには、Objectから(1)キーだけを配列として取り出す_.keysと、(2)値だけを配列として取り出す_.valuesという関数があります。が、逆に、keysとvaluesからObjectを作成するような関数がありませんv1.4.xで_.objectとして追加されたようです。

ということで、やってみます。

_ = require "underscore"
log = () -> console.log a for a in arguments

_.mixin
	pair: (k, v) -> r = {}; r[k] = v; r
	pair2: (xs) -> _.pair xs[0], xs[1]
	extend2: (o1, o2) -> _(o1).extend o2
	pairs: (ks, vs) -> _(_(_.zip ks, vs).map(_.pair2)).reduce _.extend2, {}

# 普通に書いた場合
pairs = (ks, vs) ->
	ret = {}
	ret[k] = vs[i] for k, i in ks
	ret

obj = a: 1, b: 2, c: 3, d: 4, e: 5
ks = _(obj).keys()
vs = _(obj).values()
r1 = _.pairs ks, vs
r2 = pairs ks, vs

log ks, vs, r1, r2

_.extend2を作成している理由は、reduce*1の仕様に起因します。直接_.extendを渡すと、第4引数に処理対象の配列自身が入るためです。

_ = require "underscore"
t = [{a: 1}, {b: 2}, {c: 3}]
# lは常にt。_.extendでは数値は無視される(なのでiは無視される)が、
_(t).reduce ((m, v, i, l) -> console.log m, v, i, l; _.extend m, v), {}
# 配列に対しては有効
console.log _.extend {}, t
# { '0': { a: 1 }, '1': { b: 2 }, '2': { c: 3 } }

話は戻って、pairsの部分ですが、_(_(_.zip ks, vs).map(_.pair2)).reduce _.extend2, {}というのは、如何せんスマートじゃないので直します。Haskellとかでは合成関数で書くと良い感じになるので、Underscore.jsの_.composeを使ってみます。

_ = require "underscore"
log = () -> console.log a for a in arguments

_.mixin
	pair: (k, v) -> r = {}; r[k] = v; r
	pair2: (xs) -> _.pair xs[0], xs[1]
	extend2: (o1, o2) -> _(o1).extend o2
	#pairs: (ks, vs) -> _(_(_.zip ks, vs).map(_.pair2)).reduce _.extend2, {}
	pairs: (ks, vs) ->
		(_.compose ((v) -> _(v).reduce _.extend2, {})
			, ((v) -> _(v).map(_.pair2))
			, (_.zip)
		) ks, vs

obj = a: 1, b: 2, c: 3, d: 4, e: 5
ks = _(obj).keys()
vs = _(obj).values()
r = _.pairs ks, vs
log r

微妙。。。引数の順番や、関数がカリー化されていない都合上、無名関数を使わざるをえないのと*2_.composeとCoffeeScriptの相性が微妙です。CoffeeScriptは関数の引数に無名関数が出てくると、途端に書きづらくなるのがネックですね。

さて、実はUnderscore.jsには_.chainというのがあるので、配列を連続して処理したい時はコレを使うのがよさそうです。

_ = require "underscore"
log = () -> console.log a for a in arguments

_.mixin
	pair: (k, v) -> r = {}; r[k] = v; r
	pair2: (xs) -> _.pair xs[0], xs[1]
	extend2: (o1, o2) -> _(o1).extend o2
	#pairs: (ks, vs) -> _(_(_.zip ks, vs).map(_.pair2)).reduce _.extend2, {}
	pairs: (ks, vs) ->
		_.chain(_.zip ks, vs).map(_.pair2).reduce(_.extend2, {}).value()
	
obj = a: 1, b: 2, c: 3, d: 4, e: 5
ks = _(obj).keys()
vs = _(obj).values()
r = _.pairs ks, vs
log r

だいぶ見やすくなったのではないでしょうか。

配列から、2n番目をキー、2n+1番目を値とするオブジェクトを作成する_.pairNext

やってみます。

_ = require "underscore"
log = () -> console.log a for a in arguments

_.mixin
	pair: (k, v) -> r = {}; r[k] = v; r
	pair2: (xs) -> _.pair xs[0], xs[1]
	extend2: (o1, o2) -> _(o1).extend o2
	pairs: (ks, vs) ->
		_.chain(_.zip ks, vs).map(_.pair2).reduce(_.extend2, {}).value()
	pairNext: (xs) ->
		_ys = _.chain(_.zip _.range(xs.length), xs)
		[ks, vs] = _([0, 1]).map \
			(i) -> _ys.filter((xs) -> xs[0] % 2 is i).map(_.last).value()
		_.pairs ks, vs

obj = a: 1, b: 2, c: 3, d: 4, e: 5
ks = _(obj).keys()
vs = _(obj).values()
xs = _(_.zip ks, vs).flatten()
r = _.pairNext xs
log xs, r

Underscore.jsとCoffeeScriptと組み合わせれば、実にキモくスマートに書けます。

クラスをbindでスマートにする

以下の様なシチュエーションを考えます。

class Hoge
	fn1: (key, x) -> console.log key, x
	fn2: (key, x) -> console.log key, x
	fn3: (key, x) -> console.log key, x
	fn4: (key, x) -> console.log key, x
	bind: (key) ->
		return
			fn1: _.bind @fn1, @, key
			fn2: _.bind @fn2, @, key
			fn3: _.bind @fn3, @, key
			fn4: _.bind @fn4, @, key

hoge = new Hoge
foo = hoge.bind "foo"
foo.fn1 1
foo.fn2 2
foo.fn3 3
foo.fn4 4

Hoge.bindでkeyを固定化することができます。fnXが増えた時に、個別に_.bindを書くのは嫌なので、exportしたい関数名を配列でまとめます。先ほどの_.pairNextを使ってみます。

_ = require "underscore"
log = () -> console.log a for a in arguments

_.mixin
	pair: (k, v) -> r = {}; r[k] = v; r
	pair2: (xs) -> _.pair xs[0], xs[1]
	extend2: (o1, o2) -> _(o1).extend o2
	pairs: (ks, vs) ->
		_.chain(_.zip ks, vs).map(_.pair2).reduce(_.extend2, {}).value()
	pairNext: (xs) ->
		_ys = _.chain(_.zip _.range(xs.length), xs)
		[ks, vs] = _([0, 1]).map \
			(i) -> _ys.filter((xs) -> xs[0] % 2 is i).map(_.last).value()
		_.pairs ks, vs

class Hoge
	fn1: (key, x) -> console.log key, x
	fn2: (key, x) -> console.log key, x
	fn3: (key, x) -> console.log key, x
	fn4: (key, x) -> console.log key, x
	bind: (key) ->
		_.chain(["fn1", "fn2", "fn3", "fn4"]).map((v) => [v, _(@[v]).bind(@, key)]).flatten().pairNext().value()

hoge = new Hoge
foo = hoge.bind "foo"
foo.fn1 1; foo.fn2 2; foo.fn3 3; foo.fn4 4

ん〜キモい。誰得感満載ですね。普通に書くのが一番ですね。

class Hoge
	fn1: (key, x) -> console.log key, x
	fn2: (key, x) -> console.log key, x
	fn3: (key, x) -> console.log key, x
	fn4: (key, x) -> console.log key, x
	bind: (key) ->
		ret = {}
		ret[v] = _(@[v]).bind @, key for v in ["fn1", "fn2", "fn3", "fn4"]
		ret

Undersocre.jsは調子に乗って使うと、かえって冗長になるのが玉に瑕です。超注意。

*1:ECMA

*2:bindでもいいですが、無名関数使ったほうが素直だと思う。

広告を非表示にする