1. 前置き
私の職場ではExcelが活躍しており、私も作業自動化のためVBAを書くことがありますが、VBAを使うことをつらく感じることも少なくありません。ほかの言語を使って書けばいい話ではありますが、Excelを使うほうが有利な場面も多く、なんとか少しでもVBAを楽に便利に使いたい思いがあります。
今回は、JavaScriptを使った高階関数の利用をテーマに書いてみます。
私の作業環境は次のようになっています。
Windows XP Professional + Excel 2003- Windows 8.1 Pro + Excel 2013
- Windows 8.1 Pro + Excel 2010
- Windows 8.1 + Excel 2007
- Windows 10 Home + Excel 2016
Officeはいずれも32ビット版です。
2. 高階関数とは
高階関数の説明は、JavaScript 高階関数を説明するよがわかりやすいです。
高階関数を一言で表すと、「関数を引数、戻り値として扱う関数」のことです。
馴染みがないと雰囲気や使いどころがつかみづらいかもしれませんが、「関数を変数に代入して保管しておき、自由なタイミングで実行できる」といえば、なんとなく便利そうに聞こえるのではないでしょうか。
3. 使い方
COMのひとつであるScriptControlを利用すると、JavaScriptの関数オブジェクトを変数に保管したり、任意のタイミングで実行したり、ということができます。
次は、引数を2倍して返す関数を生成し、func
変数に代入してから呼出す例です。
' 関数オブジェクトを利用する例
Public Sub FunctionObjectTest()
Dim js As Object
Dim func As Object
On Error GoTo Finally
' JavaScriptを実行する準備
Set js = CreateObject("ScriptControl")
' 言語をJavaScriptに設定
js.Language = "JScript"
' SrciptControl.Evalを使って関数オブジェクトを生成
Set func = js.Eval("(function () { return function (__arg) { return __arg * 2; }; })();")
' 中身のコードの確認
Debug.Print func ' => function (__arg) { return __arg * 2; };
' 関数オブジェクトの呼出し
Debug.Print func(2) ' => 4
Finally:
Set js = Nothing
Set func = Nothing
End Sub
順に解説します。
3.1. ScriptControl の準備
次の部分で、JavaScriptのコードを解釈・実行するコントロールであるScriptControlを使えるようにしています。
' JavaScriptを実行する準備
Dim js As Object
Set js = CreateObject("ScriptControl")
js
変数はObject
型で宣言していますが、「参照設定」から「Microsoft Script Control <バージョン>」にチェックを入れれば、CreateObject
は使わずに
Dim js As ScriptControl
Set js = New ScriptControl
と書くこともできます。
続いて、ScriptControl.Language
で言語を指定します。
' 言語をJavaScriptに設定
js.Language = "JScript"
通常はJScript
(MicrosoftによるJavaScriptの実装)とVBScript
が指定できますが、ここでは関数オブジェクトをあつかえるJScript
を指定しています。
3.2. 関数オブジェクトの生成
ScriptControlの準備が整ったので、関数を生成して変数にしまいます。JavaScriptのコードを実行するにはScriptControl.Eval
を利用します。
' SrciptControl.Evalを使って関数オブジェクトを生成
Set func = js.Eval("(function () { return function (__arg) { return __arg * 2; }; })();")
Eval
は、引数のコードを実行した結果を戻り値として返します。
' JavaScriptの配列オブジェクトを受取る
Set js_array = js.Eval("[];")
' 数値(値型)を受取る
sum = js.Eval("10 * 5;") ' => 50
しかし、関数オブジェクトも同じ感覚で受取ろうとすると「オブジェクトが必要です。」と怒られるため、今回の例では即時関数を使って明示的に関数を戻り値としています。即時関数は、関数を定義した瞬間に実行する記法です。
' このコードだとEmptyを返してエラーになる
Set func = js.Eval("function (__arg) { return __arg * 2; };")
' 関数オブジェクトを戻り値とする即時関数を実行すると…
Set func = js.Eval("(function () { return function (__arg) { return __arg * 2; }; })();")
' 受取れたぜ!
Debug.Print func ' => function (__arg) { return __arg * 2; };
2017.11.26 追記
コメントにて、よりわかりやすい関数オブジェクトの受取り方を教えていただきました。ありがとうございます。Set func = js.Eval("function f(__arg) { return __arg * 2; }; f;")
ちなみに、ここではScriptControl.Eval
を使いましたが、ScriptControl.Run
というメソッドもあるようです。こちらで紹介されています。
3.3. 関数の呼出し・実行
関数を変数として保持できたら、あとは好きなタイミングで呼出すだけです。呼出しの方法はいくつかありますが、func(arg1, arg2, ..)
というように()
を付して呼ぶのが一番楽でしょう。
Debug.Print func(2) ' => 4
4. 役立つ場面
結局どういう場面でこれが役に立つのかというと、例えば次のように、似て非なる内容のコードが量産されるときです。
次の3つの関数は、引数として与えられたa
とb
に対し、それぞれ加算、減算、乗算をおこなったうえで2倍していますが、中身のコードの違いはそれぞれの+
、-
、*
の箇所のみです。
Public Function CalcSumAndTwice(ByVal a, ByVal b)
CalcSumAndTwice = (a + b) * 2
End Function
Public Function CalcDiffAndTwice(ByVal a, ByVal b)
CalcDiffAndTwice = (a - b) * 2
End Function
Public Function CalcProductAndTwice(ByVal a, ByVal b)
CalcProductAndTwice = (a * b) * 2
End Function
これらのコードは単純なので、本来であればわざわざ関数にする必要はないかもしれませんが、もう少し内容が複雑であったり、あるいは何度も繰返しおこなうような処理であれば、やはり関数として独立させることになります。
こうしてくくり出したこれらのプロシージャが互いに似通った内容である場合、コードが重複し冗長さを生出します。
ここで高階関数を使うと、例えば次のように書けます。
Public Sub Main()
Dim calc_sum_func As Object
Dim calc_diff_func As Object
Dim calc_product_func As Object
calc_sum_func = js.Eval("function () { return function (_a, _b) { return _a + _b; } };")
calc_diff_func = js.Eval("function () { return function (_a, _b) { return _a - _b; } };")
calc_product_func = js.Eval("function () { return function (_a, _b) { return _a * _b; } };")
Debug.Print Twice(5, 2, calc_sum_func) ' => 14
Debug.Print Twice(5, 2, calc_diff_func) ' => 6
Debug.Print Twice(5, 2, calc_product_func) ' => 20
Debug.Print Twice(4, 1, calc_sum_func) ' => 10
Debug.Print Twice(4, 1, calc_diff_func) ' => 6
Debug.Print Twice(4, 1, calc_product_func) ' => 8
End Sub
' 関数オブジェクトを実行し、その結果を2倍して返す
Public Function Twice(ByVal a, ByVal b, ByRef func)
Twice = func(a, b) * 2
End Function
コードの共通部分はFunctionプロシージャTwice
として残し、異なる部分(加・減・乗算箇所)のみ関数オブジェクトとしてくくり出しています。Twice
に渡す引数func
を都度別のものに置換えれば、割と柔軟に処理を記述することができるようになります。
難点は、デバッグがしづらいなど保守性が下がったり、Eval
を使っているため脆弱性云々につながりうるという点です。あとは検証していませんが、実行速度は遅いかもしれません。
このほかの使いみちとしては、RubyのArray
におけるeach
やmap
、inject
をJavaScript側で実装し、呼出したりできるかな、と思っています(むしろこっちが本領発揮な気も…)。
5. その他雑感
本当はクロージャも使いたいと思っていたのですが、実行時のスコープ・環境をJavaScript側に渡す術がないため、おそらく無理でしょうね。最初はFunction.prototype.apply()
やFunction.prototype.call()
の第一引数でなんとかthis
として持ってこれないかな~など考えていましたが…。
ここまで来ると、もはや「すべてのコードをJavaScriptで書いて、結果だけVBAで受取れば?」という気もします。これについてももう少し検討したいと思っています。