5
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

JavaScriptでPython風の内包表記を作りたかった

Last updated at Posted at 2021-03-18

Pythonのリスト内包表記をJavaScriptでも使いたくなりました。

リスト内包表記を使うと、「数値のリストから整数だけ抽出して2乗する」といった処理が簡潔に書けます。

num_list = [1, 4.3, 8.71, 9, 11]
print([i**2 for i in num_list if isinstance(i, int)])
# 出力:[1, 81, 121]

便利です。
ということでJavaScriptでも使えるようにしようとしました。
こんな感じです↓

ac`i**2 for i of ${[1, 4.3, 8.71, 9, 11]} if Number.isInteger(i)`

名付けて「配列内包表記」。
見た目重視でテンプレートリテラル&タグ関数を採用しました。
こんな構文で使えます。

ac`式 for 変数名 of ${イテラブルオブジェクト} if 条件式`

if以下は省略可です。
ぱっと見すごくいい感じに見えませんか?

実際はほぼ実用に堪えないんですけどね。
というのも、筆者には構文解析の知識がないので、どうしても凡庸な文字列処理でゴリ押すことになります。
不正な構文を検知したりといった機能もありません。
さらに、「おまけ」の項で述べますが、次のような呼び出しをすると動かなくなります。

ac`"gift for you" for i of ${[1,2]}`

そんな稚拙な内包表記モドキでも見るだけ見てやるよ!という優しい方は続きを読んでいただけたら嬉しいです。

タグ関数の振る舞い

先ほど述べたように、今回作る内包表記モドキはタグ関数として実装することにしました。
タグ関数といっても、実体は普通の関数です。
普通の関数なのですが、タグ関数として実行する場合は呼び出し方と渡される引数がやや特殊になります。

タグ関数は以下のようにテンプレートリテラルの前に置いて使われます。組み込みのものだと、String.raw()がタグ関数として機能します。

String.raw`C:\foo\bar\baz`

タグ関数に渡される引数は少し変わった形式になります。
うまく言語化できないのですが、次のコードでなんとなく伝わるでしょうか。伝わってくれ。

myFunc`aaa${111}bbb${222}ccc`
// 上のタグ関数としての呼び出しは、下のような引数で呼び出すのと基本的に等価
myFunc(["aaa", "bbb", "ccc"], 111, 222)

ですので、自分でタグ関数を作りたいときは、下段のような形式で引数が渡される1ことを想定して関数を作っていけばOKです。

概形をつくる

次のコードが実行できるようになることを目標に概形を作ります。

ac`i + j for i, j of ${[[1, 2], [2, 4], [3, 6]]} if (i + j) % 2`

上の呼び出しを普通の形式に置き換えると、

ac(["i + j for i, j of ", " if (i + j) % 2"], [[1, 2], [2, 4], [3, 6]])

こうなります。

よって、外側は次のような感じでいいでしょう。

function ac(strs, iter) { 
}

引数から 変数名 条件式を取り出す

では、引数strsからユーザーが指定した 変数名 条件式を取り出します。

まずは変数名から。
変数名strs[0]から抽出します。forとof、及びその前後の空白を区切りとしてsplitしてしまえばいいでしょう。

const [expr, vars] = strs[0].split(/ +for +| +of +/);

上で示した目標コードで言えば、exprには"i + j"varsには"i, j"が入ります。
変数名["i", "j"]のように一つずつ分けておきたいですね。さらにsplitして配列にします。

const splitedVars = vars.split(/ *, */);

次にstrs[1]から条件式を取り出します。
strs[1].split(/ +if +/)[1]で取り出すことはできるのですが、Pythonのリスト内包表記のようにif部分を省略できるようにしたいです。
よって、ifが無い場合は"true"条件式とするようにします。

const condition = strs[1].includes("if") ? strs[1].split(/ +if +/)[1] : "true";

とりあえず前準備としてはこんなもんでしょう。

条件式を評価する関数とを評価する関数を作る

先ほど 変数名 条件式を取り出しましたが、これらはいずれも文字列です。
ただの文字列なので、そのままでは碌に評価することもできません。
一般に、文字列をJavaScriptコードとして評価するのにはeval()やFunctionコンストラクタが使われます。

今回は、Functionコンストラクタを使って、条件式を評価する関数judgeを評価する関数getValueを作ります。

Functionコンストラクタを使うのは初めてだったので、MDNで構文を調べました。引用します。

構文
new Function([arg1 [, arg2 [, ...argN]] ,] functionBody)
引数
arg1, arg2, ... argN
仮引数の名前として関数で用いるための名前。各々は、妥当な JavaScript の識別子と一致する文字列か、カンマで区切られた文字列などのリストでなければなりません。例えば、 "x", "theValue", "x,theValue" などです。
functionBody
関数定義を形成する JavaScript の文を含む文字列。

わかりやすい使用例も載っていました。

// 二つの引数を取り、その合計を返す関数を生成します
const adder = new Function('a', 'b', 'return a + b');

// 関数を呼び出します
adder(2, 6);
// 8

なるほど、Functionコンストラクタ完全に理解した。

早速、条件式を評価する関数judgeから作ります。

const judge = new Function(...splitedVars, `return ${condition};`);

ちょっとわかりにくいでしょうか。
上で示した目標のコードで言えば、ちょうど以下のような感じで実行されると考えてください。

const judge = new Function("i", "j", "return (i + j) % 2;");

同じ要領で、を評価する関数getValueも作ります。

const getValue = new Function(...splitedVars, `return ${expr};`);

さて、あとは空の配列を用意してループ内でpushして最後にreturnでいけそうです。

const result = [];
// 色々と処理してresultにpushしていく
// …
// 最後にreturn
return result;

では、中の具体的な処理を書きます。

returnする配列を作る処理を書く

for-of文で引数のiterをループして、先ほどの関数getValueで求めた値を配列resultにpushしていけばいいでしょう。
ただし、pushするかどうかの条件判定を関数judgeを用いて行うようにしておきます。

for (const elem of iter) {
	if (judge(...elem)) {
		result.push(getValue(...elem));
	}
}

概形が完成

function ac(strs, iter) {
	const [expr, vars] = strs[0].split(/ +for +| +of +/);
	const splitedVars = vars.split(/ *, */);
	const condition = strs[1].includes("if") ? strs[1].split(/ +if +/)[1] : "true";
	const judge = new Function(...splitedVars, `return ${condition};`);
	const getValue = new Function(...splitedVars, `return ${expr};`);
	const result = [];
	for (const elem of iter) {
		if (judge(...elem)) {
			result.push(getValue(...elem));
		}
	}	
	return result;
}

これで目標のコード↓が動きます。

ac`i + j for i, j of ${[[1, 2], [2, 4], [3, 6]]} if (i + j) % 2`

だがちょっと待って欲しい。
今のままでは、次のような至極シンプルなコードが動きません。

ac`i * 2 for i of ${[1, 2, 3]}`

これは、for-ofループ内で、イテラブルから取り出した要素をスプレッド構文で展開しようとしているのが原因です。
1や2などの数値はスプレッド構文で展開できないのでエラーになります。
もちろん数値だけでなく、文字列や真偽値でも同様です。

完成形

変数名が一つしか指定されていないときはスプレッド構文を使わないように条件分岐しましょう。
配列splitedVarsのlengthで判定します。

function ac(strs, iter) {
	const [expr, vars] = strs[0].split(/ +for +| +of +/);
	const splitedVars = vars.split(/ *, */);
	const condition = strs[1].includes("if") ? strs[1].split(/ +if +/)[1] : "true";
	const result = [];
	if (splitedVars.length === 1) {
		const judge = new Function(splitedVars[0], `return ${condition};`);
		const getValue = new Function(splitedVars[0], `return ${expr};`);
		for (const elem of iter) {
			if (judge(elem)) {
				result.push(getValue(elem));
			}
		}
	} else {
		const judge = new Function(...splitedVars, `return ${condition};`);
		const getValue = new Function(...splitedVars, `return ${expr};`);
		for (const elem of iter) {
			if (judge(...elem)) {
				result.push(getValue(...elem));
			}
		}
	}
	return result;
}

これで完成です!
先ほど動かなかったこれ↓も動きます。

ac`i * 2 for i of ${[1, 2, 3]}`

やったね。

おまけ

上でとりあえず完成させた配列内包表記ですが、色々と弱点や問題があります。
ここからは、それらを緩和しようとジタバタしてみます(解決するとは言ってない)。
また、オブジェクト内包表記やジェネレータ式なども作ったので最後の部分で紹介しています。

① ローカル関数が使えん

グローバルから参照できない関数が使えません。

function main() {
	function add(a, b) {
		return a + b;
	}
	const it = [[1, 1], [2, 4] ,[3, 9], [4, 16]];
	console.log(ac`add(i, j) for i, j of ${it}`);
}

main();  // ReferenceError: add is not defined

仮にmainの定義の中でacを宣言したとしても同様です。
配列内包表記は内部でFunctionコンストラクタを使いますが、Functionコンストラクタで作られた関数はグローバルスコープで実行されるからです。

どうしましょう。使いたいローカル関数/オブジェクトの参照をacに渡す必要がありそうです。
また、Functionコンストラクタでは文字列をベースに関数を作るので、関数/オブジェクト名も文字列として取得しておきたいです。

あれこれ考えた結果、名前の文字列と参照を同時に渡すにはES6で導入されたプロパティ定義のショートハンドがいいんじゃないかという結論に達しました。

const a = function() {console.log("a");};
const b = function() {console.log("b");}; 
const obj = {a, b};  // {a: a, b: b}のショートハンド
console.log(Object.keys(obj));  // Array [ "a", "b" ]
console.log(Object.values(obj));  // Array [ function a(), function b() ]

特にObject.keys()で名前が取得できるのが強いです。

というわけで、以下のような構文でローカルオブジェクトを使えるようにしました。

ac`式 for 変数名 of ${イテラブルオブジェクト} if 条件式 locals ${{ローカルオブジェクト}}`

実装は以下。

function ac(strs, iter, locals = {}) {
	const [expr, vars] = strs[0].split(/ +for +| +of +/);
	const splitedVars = vars.split(/ *, */);
	const condition = strs[1].includes("if") ? strs[1].split(/ +if +| +locals +/)[1] : "true";
	const localsName = Object.keys(locals);
	const localsRef = Object.values(locals);
	const result = [];
	if (splitedVars.length === 1) {
		const judge = new Function(...localsName, splitedVars[0], `return ${condition};`);
		const getValue = new Function(...localsName, splitedVars[0], `return ${expr};`);
		for (const elem of iter) {
			if (judge(...localsRef, elem)) {
				result.push(getValue(...localsRef, elem));
			}
		}
	} else {
		const judge = new Function(...localsName, ...splitedVars, `return ${condition};`);
		const getValue = new Function(...localsName, ...splitedVars, `return ${expr};`);
		for (const elem of iter) {
			if (judge(...localsRef, ...elem)) {
				result.push(getValue(...localsRef, ...elem));
			}
		}
	}
	return result;
}

使用例です。

function main() {
	const myObj = {
		add(a, b) {
			return a + b;
		}
	}
	function isOdd(x) {
		return x % 2 === 1;
	}
	const it = [[1, 1], [2, 4] ,[3, 9], [4, 16]];
	console.log(ac`myObj.add(i, j) for i, j of ${it} if isOdd(i) locals ${{myObj, isOdd}}`);
}

main()

…もう普通にfilter()map()併用するほうが楽なのでは…?

② "gift for you"←区切りのforと区別できん

冒頭でも述べたアレです。

ac`"gift for you" for i of ${[1,2]}`

変数名を抽出するときに、文字列"gift for you"" for "の部分でもsplitしてしまうのでうまく動かなくなります。

これは…どうにもなりませんでした。
構文解析ができればいいんですが、そんなスキルはないし、ちょっとした遊びのつもりなのに大仰すぎます。

そうです、そもそもかっこつけてタグ関数なんて使おうとしたのが良くないのです。
普通の関数で実装すればこんな問題おさらばです。

ac(, 変数名, イテラブルオブジェクト, 条件式 , {ローカルオブジェクト})

ちょっと引数の順序が気持ち悪い気はしますが、この形式で十分じゃありませんか?

こんな感じの実装にすればいいです。

function ac(expr, vars, iter, condition = "true", locals = {}) {
	const splitedVars = vars.split(/ *, */);
	const localsName = Object.keys(locals);
	const localsRef = Object.values(locals);
	const result = [];
	if (splitedVars.length === 1) {
		const judge = new Function(...localsName, splitedVars[0], `return ${condition};`);
		const getValue = new Function(...localsName, splitedVars[0], `return ${expr};`);
		for (const elem of iter) {
			if (judge(...localsRef, elem)) {
				result.push(getValue(...localsRef, elem));
			}
		}
	} else {
		const judge = new Function(...localsName, ...splitedVars, `return ${condition};`);
		const getValue = new Function(...localsName, ...splitedVars, `return ${expr};`);
		for (const elem of iter) {
			if (judge( ...localsRef, ...elem)) {
				result.push(getValue(...localsRef, ...elem));
			}
		}
	}
	return result;
}

さっき動かなかったアレも実行できます。

ac("'gift for you'", "i" , [1,2])

…やっぱり普通にfilter()map()併用するほうが楽なのでは…?

③ その他の○○内包表記 & ジェネレータ式

オブジェクト内包表記

function oc(strs, iter, locals = {}) {
	const [expr, vars] = strs[0].split(/ +for +| +of +/);
	const splitedVars = vars.split(/ *, */);
	const condition = strs[1].includes("if") ? strs[1].split(/ +if +| +locals +/)[1] : "true";
	const localsName = Object.keys(locals);
	const localsRef = Object.values(locals);
	const result = {};
	if (splitedVars.length === 1) {
		const judge = new Function(...localsName, splitedVars[0], `return ${condition};`);
		const getEntry = new Function(...localsName, splitedVars[0], `return ${expr};`);
		for (const elem of iter) {
			if (judge(...localsRef, elem)) {
				const [key, value] = getEntry(...localsRef, elem);
				result[key] = value;
			}
		}
	} else {
		const judge = new Function(...localsName, ...splitedVars, `return ${condition};`);
		const getEntry = new Function(...localsName, ...splitedVars, `return ${expr};`);
		for (const elem of iter) {
			if (judge(...localsRef, ...elem)) {
				const [key, value] = getEntry(...localsRef, ...elem);
				result[key] = value;
			}
		}
	}
	return result;
}

構文

oc`[キー, 値] for 変数名 of ${イテラブルオブジェクト} if 条件式 locals ${{ローカルオブジェクト}}`;

使用例

console.log(oc`[j, i] for i, j of ${[[1, "a"], [2, "b"]]}`);
// Object { a: 1, b: 2 }

ジェネレータ式

function ge(strs, iter, locals = {}) {
	const [expr, vars] = strs[0].split(/ +for +| +of +/);
	const splitedVars = vars.split(/ *, */);
	const condition = strs[1].includes("if") ? strs[1].split(/ +if +| +locals +/)[1] : "true";
	const localsName = Object.keys(locals);
	const localsRef = Object.values(locals);
	let result;
	if (splitedVars.length === 1) {
		const judge = new Function(...localsName, splitedVars[0], `return ${condition};`);
		const getValue = new Function(...localsName, splitedVars[0], `return ${expr};`);
		result = function*() {
			for (const elem of iter) {
				if (judge(...localsRef, elem)) {
					yield getValue(...localsRef, elem);
				}
			}
		}
	} else {
		const judge = new Function(...localsName, ...splitedVars, `return ${condition};`);
		const getValue = new Function(...localsName, ...splitedVars, `return ${expr};`);
		result = function*() {
			for (const elem of iter) {
				if (judge(...localsRef, ...elem)) {
					yield getValue(...localsRef, ...elem);
				}
			}
		}
	}
	return result();
}

構文

ge`式 for 変数名 of ${イテラブルオブジェクト} if 条件式 locals ${{ローカルオブジェクト}}`;

使用例

for (const i of ge`i + 2 for i of ${[1, 2, 3]}`) {
  console.log(i);
}
// 3
// 4
// 5

Map内包表記

function mc(strs, iter, locals = {}) {
	const [expr, vars] = strs[0].split(/ +for +| +of +/);
	const splitedVars = vars.split(/ *, */);
	const condition = strs[1].includes("if") ? strs[1].split(/ +if +| +locals +/)[1] : "true";
	const localsName = Object.keys(locals);
	const localsRef = Object.values(locals);
	const result = new Map();
	if (splitedVars.length === 1) {
		const judge = new Function(...localsName, splitedVars[0], `return ${condition};`);
		const getEntry = new Function(...localsName, splitedVars[0], `return ${expr};`);
		for (const elem of iter) {
			if (judge(...localsRef, elem)) {
				result.set(...getEntry(...localsRef, elem));
			}
		}
	} else {
		const judge = new Function(...localsName, ...splitedVars, `return ${condition};`);
		const getEntry = new Function(...localsName, ...splitedVars, `return ${expr};`);
		for (const elem of iter) {
			if (judge(...localsRef, ...elem)) {
				result.set(...getEntry(...localsRef, ...elem));
			}
		}
	}
	return result;
}

構文

mc`[キー, 値] for 変数名 of ${イテラブルオブジェクト} if 条件式 locals ${{ローカルオブジェクト}}`;

使用例

mc`[i, (j+2)**2] for i, j of ${[["a", 1], ["b", 2]]}`;
// Map { a → 9, b → 16 }

なお、const result = new Map();const result = new WeakMap();にするとWeakMap内包表記になります。

Set内包表記

function sc(strs, iter, locals = {}) {
	const [expr, vars] = strs[0].split(/ +for +| +of +/);
	const splitedVars = vars.split(/ *, */);
	const condition = strs[1].includes("if") ? strs[1].split(/ +if +| +locals +/)[1] : "true";
	const localsName = Object.keys(locals);
	const localsRef = Object.values(locals);
	const result = new Set();
	if (splitedVars.length === 1) {
		const judge = new Function(...localsName, splitedVars[0], `return ${condition};`);
		const getValue = new Function(...localsName, splitedVars[0], `return ${expr};`);
		for (const elem of iter) {
			if (judge(...localsRef, elem)) {
				result.add(getValue(...localsRef, elem));
			}
		}
	} else {
		const judge = new Function(...localsName, ...splitedVars, `return ${condition};`);
		const getValue = new Function(...localsName, ...splitedVars, `return ${expr};`);
		for (const elem of iter) {
			if (judge(...localsRef, ...elem)) {
				result.add(getValue(...localsRef, ...elem));
			}
		}
	}
	return result;
}

構文

sc`式 for 変数名 of ${イテラブルオブジェクト} if 条件式 locals ${{ローカルオブジェクト}}`;

使用例

sc`(i+2)**2 for i of ${[1, 2, 3]}`
// Set(3) [ 9, 16, 25 ]

なお、const result = new Set();const result = new WeakSet();にするとWeakSet内包表記になります。

  1. タグ関数の第一引数に渡される配列にはrawというプロパティが追加されているので、厳密にいえば等価ではありません。ただ、今回はrawプロパティを使わないのでスルーで。

5
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?