はじめに
最近、Google Apps Script や Office JavaScript API を使うことが多く、JavaScript と関わる機会が増えてきました。
普段は PowerShell, fsi(F# Interactive), IronPython をよく使いますが、作成した JavaScript の再利用を視野に、Microsoft.ClearScript を利用して F# Interactive から JavaScript を動かしてみました。
【ClearScript について】
・ClearScript(公式)
・ClearScript の概要(MSDN Magazine Issues 2014)
動作確認環境
記載した内容は
・ ASUS Chromebook CX1(CX1500CKA-EJ0015) 上の Linux 開発環境(Debian 11)
・ .NET SDK 6.0.401
にて動作確認を行っています。(Windows 10 でも動作確認済み)
環境は下の画像のとおりです。
F# と JavaScript の接続
素材
次の JavaScript を F# と接続することにします。
const kinsyu = [10000, 5000, 1000, 500, 100, 50, 10, 5, 1];
var maisuu = [];
kinsyu.reduce((x, y) => {
maisuu.push(Math.floor(x / y));
return x % y;
}, parseInt(kingaku.replace(/,/g, '')));
これは、過去の投稿で Excel のカスタム関数用に書いた JavaScript を一部改編したものです。
"12,345"
のように、カンマ付き文字列で与えられた「金額(kingaku
)」から、各金種の「枚数の配列(maisuu
)」を作ります。(2000円札を除いています)
ただし、kingaku
の値はスクリプト上にないので、F# 側から設定するものとします。
以下、F# Interactive での実行結果も添えながら進めます。
Microsoft.ClearScript の準備
fsi は、 nuget から簡単にパッケージをロードできます。
#r "nuget: Microsoft.ClearScript.linux-x64"
let engine = new Microsoft.ClearScript.V8.V8ScriptEngine();;
ClearScript は、プラットフォーム別にパッケージが用意されているので、ここでは
Microsoft.ClearScript.linux-x64
を選択しています。
(詳しくは、https://github.com/microsoft/ClearScript#composite-packages を参照のこと。)
これで、F# から使える JavaScript エンジン(engine
)が用意できました。
グローバル変数の追加とスクリプトの実行
先に示したスクリプトではkingaku
が未定義でしたが、スクリプトエンジンのGlobal
プロパティからグローバル変数として追加できます。
あとは、スクリプトを丸ごと文字列にしてExecute
メソッドに渡せば実行されます。
engine.Global[ "kingaku" ] <- "56,789";;
"""
const kinsyu = [10000, 5000, 1000, 500, 100, 50, 10, 5, 1];
var maisuu = [];
kinsyu.reduce((x, y) => {
maisuu.push(Math.floor(x / y));
return x % y;
}, parseInt(kingaku.replace(/,/g, '')));
"""
|> engine.Execute;;
実行後の変数や定数の取得
グローバル変数はGlobal
プロパティで値を取得できます。
まずは、Global
プロパティでアクセスできるものを見てみます。
engine.Global.PropertyNames;;
kingaku
とmaisuu
が見えますが、kinsyu
は見えません。
JavaScript でconst
やlet
宣言したものはブロックスコープなのでGlobal
プロパティから見えません。
そのため、maisuu
をvar
宣言しています。
スクリプト実行後のmaisuu
の値を取得する方法をGlobal
プロパティを含めて3つ示します。
engine.Global[ "maisuu" ];;
(engine.Script :?> Microsoft.ClearScript.ScriptObject)[ "maisuu" ];;
"maisuu" |> engine.Evaluate;;
2番目のScript
プロパティはGlobal
プロパティに似た働きをしますが、
Microsoft.ClearScript.ScriptObject
にダウンキャストする必要があります。
3番目のEvaluate
メソッドは、式を評価した結果を返すので、スクリプトのデバッグなどにも使えます。
また、const
,let
で宣言された値も取得可能です。
取得した値を F# で使う
取得した値は、fsi 上ではまだobj
型ですので、値が文字列リテラルや数値リテラルならばstring
, int
など .NET 上の適切な型にダウンキャストする必要があります。
さて、先ほど取得したmaisuu
は整数の配列です。
JavaScript のArray
オブジェクトは、System.Collections.IEnumerable
にキャストできます。
さらにSeq.cast<'T>
すれば F# のシーケンスになります。
maisuu
は、以下の手順でSystem.Array<int>
に変換できます。
engine.Global[ "maisuu" ] :?> System.Collections.IEnumerable
|> Seq.cast<int>
|> Seq.toArray;;
「56,789円」に必要な1万円札から1円玉までの枚数が配列になっています。
型付き配列(TypedArray)の場合
JavaScript の型付き配列(TypedArray
)は
Microsoft.ClearScript.JavaScript.ITypedArray<'T>
にダウンキャストします。
ITypedArray
はToArray
メソッドを持つので、直接System.Array
に変換できます。
maisuu
を JavaScript のInt32Array
に変換して確認してみます。
"Int32Array.from(maisuu)" |> engine.Evaluate :?> Microsoft.ClearScript.JavaScript.ITypedArray<int>;;
it.ToArray();;
JavaScript の関数を利用する
function
の扱い
ここまで、値をやりとりしてみましたが、このままでは実用になりません。
同じスクリプトをExecute
メソッドに渡せば、const
の再宣言エラーを返されます。
ということで、JavaScript 側で関数(kinsyuhyou
)を定義し、Invoke
メソッドで呼び出すことにします。
"""
function kinsyuhyou(kingaku) {
const kinsyu = [10000, 5000, 1000, 500, 100, 50, 10, 5, 1];
const maisuu = [];
kinsyu.reduce((x, y) => {
maisuu.push(Math.floor(x / y));
return x % y;
}, parseInt(kingaku.replace(/,/g, '')));
return maisuu;
};
"""
|> engine.Execute;;
engine.Invoke("kinsyuhyou", "34,567") :?> System.Collections.IEnumerable
|> Seq.cast<int>
|> Seq.toArray;;
JavaScript を関数として呼び出す手段があれば、あとは F# で処理できます。
複数の金額を金種別の2次元配列にする例です。
[| "34,567"; "88,888"; "54,321" |]
|> Array.map (fun kingaku ->
engine.Invoke("kinsyuhyou", kingaku) :?> System.Collections.IEnumerable
|> Seq.cast<int>)
|> array2D;;
FSharp.Interop.Dynamic を使うとカッコよくなる?
これで、JavaScript の関数を利用できますが、引っかかっていた点があります。
公式の ClearScript Examples (C#)にfunction
の呼び出し例が示されています。
// call a script function
engine.Execute("function print(x) { Console.WriteLine(x); }");
engine.Script.print(DateTime.Now.DayOfWeek);
サンプルの内容はさておき、C# ならengine.Script.関数名(引数)
で呼び出すことができます。
(末尾の「おまけ」で確認済み)
残念ながら、F# で同様の記述ができません。
そこで、FSharp.Interop.Dynamic を利用してみます。
#r "nuget: FSharp.Interop.Dynamic"
open FSharp.Interop.Dynamic;;
FSharp.Interop.Dynamic で定義された?
演算子によって、C# に近い記述が可能になります。
Invoke
メソッドの使用例2つを書き換えてみました。
"34,567"
|> engine.Script?kinsyuhyou
|> Seq.cast<int>
|> Seq.toArray;;
[| "34,567"; "88,888"; "54,321" |]
|> Array.map (fun kingaku ->
engine.Script?kinsyuhyou(kingaku) |> Seq.cast<int>)
|> array2D;;
「金額」だけをパイプラインに渡せるだけでなく、型推論が効いてSystem.Collections.IEnumerable
へのダウンキャストなしに動作します。
つまり、もっとカッコよく(F# らしく、関数合成で)書き換えられるということ。
"34,567"
|> (engine.Script?kinsyuhyou >> Seq.cast<int> >> Seq.toArray);;
[| "34,567"; "88,888"; "54,321" |]
|> Array.map (engine.Script?kinsyuhyou >> Seq.cast<int>)
|> array2D;;
実務で使うかは別にして、芸の幅が少し広がりました。
VBScriptもサポートしているので、暇なときに遊んでみようと思います。
おまけ
C# で動作確認した際のシェルスクリプトと実行画面を載せておきます。
スクリプト下方に "Program.cs"(ClearScript Examples の一部コピー)を書いています。
注:同名のプロジェクトがあっても上書きします。
#!/bin/bash
project='jsFunctionExample'
package='Microsoft.ClearScript.linux-x64'
#
printf '\033[33m%s\033[m\n' "プロジェクト ${project} を作成します..."
dotnet new console -o $project
cd $project/
#
printf '\033[33m%s\033[m\n' "パッケージ ${package} を追加します..."
dotnet add package $package
#
printf '\033[33m%s\033[m\n' 'Program.cs を上書きします...'
cat << EOS > Program.cs
using System;
using Microsoft.ClearScript.V8;
using (var engine = new V8ScriptEngine())
{
// expose a host type
engine.AddHostType("Console", typeof(Console));
// call a script function
engine.Execute("function print(x) { Console.WriteLine(x); }");
engine.Script.print(DateTime.Now.DayOfWeek);
}
EOS
#
printf '\033[33m%s\033[m\n' '実行します...'
dotnet run
資料