JavaScript
Excel
VBA
ExcelVBA

[Excel/VBA] VBAでJavaScriptの関数オブジェクト・高階関数を使う

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つの関数は、引数として与えられたabに対し、それぞれ加算、減算、乗算をおこなったうえで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におけるeachmapinjectをJavaScript側で実装し、呼出したりできるかな、と思っています(むしろこっちが本領発揮な気も…)。

5. その他雑感

本当はクロージャも使いたいと思っていたのですが、実行時のスコープ・環境をJavaScript側に渡す術がないため、おそらく無理でしょうね。最初はFunction.prototype.apply()Function.prototype.call()の第一引数でなんとかthisとして持ってこれないかな~など考えていましたが…。

ここまで来ると、もはや「すべてのコードをJavaScriptで書いて、結果だけVBAで受取れば?」という気もします。これについてももう少し検討したいと思っています。

6. 参考記事