LoginSignup
0

posted at

[ClearScript] F# Interactive から JavaScript を扱う

はじめに

最近、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 でも動作確認済み)
環境は下の画像のとおりです。
ClearScript0.png
ClearScript1.png

F# と JavaScript の接続

素材

次の JavaScript を F# と接続することにします。

JavaScript(金種別枚数計算)
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 から簡単にパッケージをロードできます。

F# Interactive
#r "nuget: Microsoft.ClearScript.linux-x64"
let engine = new Microsoft.ClearScript.V8.V8ScriptEngine();;

ClearScript2.png
ClearScript は、プラットフォーム別にパッケージが用意されているので、ここでは
Microsoft.ClearScript.linux-x64
を選択しています。
(詳しくは、https://github.com/microsoft/ClearScript#composite-packages を参照のこと。)

これで、F# から使える JavaScript エンジン(engine)が用意できました。

グローバル変数の追加とスクリプトの実行

先に示したスクリプトではkingakuが未定義でしたが、スクリプトエンジンのGlobalプロパティからグローバル変数として追加できます。

あとは、スクリプトを丸ごと文字列にしてExecuteメソッドに渡せば実行されます。

F# Interactive
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;;

ClearScript3.png

実行後の変数や定数の取得

グローバル変数はGlobalプロパティで値を取得できます。
まずは、Globalプロパティでアクセスできるものを見てみます。

F# Interactive
engine.Global.PropertyNames;;

ClearScript4.png
kingakumaisuuが見えますが、kinsyuは見えません。
JavaScript でconstlet宣言したものはブロックスコープなのでGlobalプロパティから見えません。
そのため、maisuuvar宣言しています。

スクリプト実行後のmaisuuの値を取得する方法をGlobalプロパティを含めて3つ示します。

F# Interactive
engine.Global[ "maisuu" ];;

(engine.Script :?> Microsoft.ClearScript.ScriptObject)[ "maisuu" ];;

"maisuu" |> engine.Evaluate;;

ClearScript5.png
すべて、同じ結果を返しています。

2番目のScriptプロパティはGlobalプロパティに似た働きをしますが、
Microsoft.ClearScript.ScriptObject
にダウンキャストする必要があります。

3番目のEvaluateメソッドは、式を評価した結果を返すので、スクリプトのデバッグなどにも使えます。
また、constletで宣言された値も取得可能です。

取得した値を F# で使う

取得した値は、fsi 上ではまだobj型ですので、値が文字列リテラルや数値リテラルならばstring, intなど .NET 上の適切な型にダウンキャストする必要があります。

さて、先ほど取得したmaisuuは整数の配列です。
JavaScript のArrayオブジェクトは、System.Collections.IEnumerableにキャストできます。
さらにSeq.cast<'T>すれば F# のシーケンスになります。

maisuuは、以下の手順でSystem.Array<int>に変換できます。

F# Interactive
engine.Global[ "maisuu" ] :?> System.Collections.IEnumerable
|> Seq.cast<int>
|> Seq.toArray;;

ClearScript6.png
「56,789円」に必要な1万円札から1円玉までの枚数が配列になっています。

型付き配列(TypedArray)の場合

JavaScript の型付き配列(TypedArray)は
Microsoft.ClearScript.JavaScript.ITypedArray<'T>
にダウンキャストします。

ITypedArrayToArrayメソッドを持つので、直接System.Arrayに変換できます。

maisuuを JavaScript のInt32Arrayに変換して確認してみます。

F# Interactive
"Int32Array.from(maisuu)" |> engine.Evaluate :?> Microsoft.ClearScript.JavaScript.ITypedArray<int>;;
it.ToArray();;

ClearScript7.png

JavaScript の関数を利用する

functionの扱い

ここまで、値をやりとりしてみましたが、このままでは実用になりません。
同じスクリプトをExecuteメソッドに渡せば、constの再宣言エラーを返されます。

ということで、JavaScript 側で関数(kinsyuhyou)を定義し、Invokeメソッドで呼び出すことにします。

F# Interactive
"""
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;;

Screenshot 2022-09-18 13.11.13.png
JavaScript を関数として呼び出す手段があれば、あとは F# で処理できます。

複数の金額を金種別の2次元配列にする例です。

F# Interactive
[| "34,567"; "88,888"; "54,321" |]
|> Array.map (fun kingaku ->
    engine.Invoke("kinsyuhyou", kingaku) :?> System.Collections.IEnumerable
    |> Seq.cast<int>)
|> array2D;;

Screenshot 2022-09-18 13.48.42.png

FSharp.Interop.Dynamic を使うとカッコよくなる?

これで、JavaScript の関数を利用できますが、引っかかっていた点があります。
公式の ClearScript Examples (C#)にfunctionの呼び出し例が示されています。

C#(ClearScript Examples の一部)
// 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 を利用してみます。

F# Interactive
#r "nuget: FSharp.Interop.Dynamic"
open FSharp.Interop.Dynamic;;

ClearScript9.png
FSharp.Interop.Dynamic で定義された?演算子によって、C# に近い記述が可能になります。

Invokeメソッドの使用例2つを書き換えてみました。

F# Interactive
"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;;

Screenshot 2022-09-18 13.53.40.png
「金額」だけをパイプラインに渡せるだけでなく、型推論が効いてSystem.Collections.IEnumerableへのダウンキャストなしに動作します。

つまり、もっとカッコよく(F# らしく、関数合成で)書き換えられるということ。

F# Interactive
"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;;

Screenshot 2022-09-18 13.57.43.png
実務で使うかは別にして:rolling_eyes:、芸の幅が少し広がりました。
VBScriptもサポートしているので、暇なときに遊んでみようと思います。

おまけ

C# で動作確認した際のシェルスクリプトと実行画面を載せておきます。
スクリプト下方に "Program.cs"(ClearScript Examples の一部コピー)を書いています。

注:同名のプロジェクトがあっても上書きします。

Qiita_ClearScript_Example.sh
#!/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

Screenshot 2022-09-18 16.21.51.png
今日は日曜日でした。

資料

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
What you can do with signing up
0