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

## はじめに
最近、Google Apps Script や Office JavaScript API を使うことが多く、JavaScript と関わる機会が増えてきました。

普段は PowerShell, fsi(F# Interactive), IronPython をよく使いますが、作成した JavaScript の再利用を視野に、**Microsoft.ClearScript** を利用して F# Interactive から JavaScript を動かしてみました。

【ClearScript について】
　・[ClearScript](https://microsoft.github.io/ClearScript/)（公式）
　・[ClearScript の概要](https://docs.microsoft.com/ja-jp/archive/msdn-magazine/2014/september/cutting-edge-a-look-at-clearscript)（MSDN Magazine Issues 2014）
## 動作確認環境
記載した内容は
　・ ASUS Chromebook CX1(CX1500CKA-EJ0015) 上の Linux 開発環境(Debian 11)
　・ .NET SDK 6.0.401
にて動作確認を行っています。（Windows 10 でも動作確認済み）
環境は下の画像のとおりです。
![ClearScript0.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209885/f3fc16e0-5ad9-dcb0-2ccd-eb524f482e65.png)
![ClearScript1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209885/9982a584-91ae-4274-2356-f6eac2393bf5.png)
## F# と JavaScript の接続
#### 素材
次の JavaScript を F# と接続することにします。
```JavaScript: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, '')));
```
これは、[過去の投稿](https://qiita.com/Mt_SQ/items/6ad193a3a1cba9434407#2-%E9%87%91%E9%A1%8D%E3%81%8B%E3%82%89%E9%87%91%E7%A8%AE%E5%88%A5%E3%81%AE%E6%9E%9A%E6%95%B0%E3%82%92%E8%BF%94%E3%81%99%E9%96%A2%E6%95%B0)で Excel のカスタム関数用に書いた JavaScript を一部改編したものです。

`"12,345"`のように、カンマ付き文字列で与えられた「金額(`kingaku`)」から、各金種の「枚数の配列(`maisuu`)」を作ります。（2000円札を除いています）
**ただし、`kingaku`の値はスクリプト上にないので、F# 側から設定するものとします。**

以下、F# Interactive での実行結果も添えながら進めます。
#### Microsoft.ClearScript の準備
fsi は、 nuget から簡単にパッケージをロードできます。
```FSharp:F# Interactive
#r "nuget: Microsoft.ClearScript.linux-x64"
let engine = new Microsoft.ClearScript.V8.V8ScriptEngine();;
```
![ClearScript2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209885/64f944cc-0b10-b199-fa73-4e5415ebcb49.png)
ClearScript は、プラットフォーム別にパッケージが用意されているので、ここでは
`Microsoft.ClearScript.linux-x64`
を選択しています。
（詳しくは、https://github.com/microsoft/ClearScript#composite-packages を参照のこと。）

これで、F# から使える JavaScript エンジン（`engine`）が用意できました。
#### グローバル変数の追加とスクリプトの実行
先に示したスクリプトでは`kingaku`が未定義でしたが、スクリプトエンジンの`Global`プロパティからグローバル変数として追加できます。

あとは、スクリプトを丸ごと文字列にして`Execute`メソッドに渡せば実行されます。
```FSharp: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209885/1df08697-62c3-c6bc-bfb1-28475dc8e7b3.png)
#### 実行後の変数や定数の取得
グローバル変数は`Global`プロパティで値を取得できます。
まずは、`Global`プロパティでアクセスできるものを見てみます。
```FSharp:F# Interactive
engine.Global.PropertyNames;;
```
![ClearScript4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209885/ef0bf863-505c-fa82-f7fb-ebfd71cdaf30.png)
`kingaku`と`maisuu`が見えますが、`kinsyu`は見えません。
JavaScript で`const`や`let`宣言したものはブロックスコープなので`Global`プロパティから見えません。
**そのため、`maisuu`を`var`宣言しています。**

スクリプト実行後の`maisuu`の値を取得する方法を`Global`プロパティを含めて３つ示します。
```FSharp:F# Interactive
engine.Global[ "maisuu" ];;

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

"maisuu" |> engine.Evaluate;;
```
![ClearScript5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209885/eab34d11-5e4b-aa65-4337-5a23c6359e00.png)
すべて、同じ結果を返しています。

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

３番目の`Evaluate`メソッドは、式を評価した結果を返すので、スクリプトのデバッグなどにも使えます。
また、**`const`，`let`で宣言された値も取得可能です。**
#### 取得した値を F# で使う
取得した値は、fsi 上ではまだ`obj`型ですので、値が文字列リテラルや数値リテラルならば`string`, `int`など .NET 上の適切な型にダウンキャストする必要があります。

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

`maisuu`は、以下の手順で`System.Array<int>`に変換できます。
```FSharp:F# Interactive
engine.Global[ "maisuu" ] :?> System.Collections.IEnumerable
|> Seq.cast<int>
|> Seq.toArray;;
```
![ClearScript6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209885/26450899-33dd-a510-f552-c1415ab9640d.png)
「56,789円」に必要な１万円札から１円玉までの枚数が配列になっています。
#### 型付き配列（TypedArray）の場合
JavaScript の型付き配列(`TypedArray`)は
`Microsoft.ClearScript.JavaScript.ITypedArray<'T>`
にダウンキャストします。

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

`maisuu`を JavaScript の`Int32Array`に変換して確認してみます。
```FSharp:F# Interactive
"Int32Array.from(maisuu)" |> engine.Evaluate :?> Microsoft.ClearScript.JavaScript.ITypedArray<int>;;
it.ToArray();;
```
![ClearScript7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209885/35c493bb-501f-1e95-d9d1-7ca8f2b14a52.png)
## JavaScript の関数を利用する
#### `function`の扱い
ここまで、値をやりとりしてみましたが、このままでは実用になりません。
同じスクリプトを`Execute`メソッドに渡せば、`const`の再宣言エラーを返されます。

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

```FSharp: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209885/a39417b4-a66b-6a22-530f-64a7e28a0759.png)
JavaScript を関数として呼び出す手段があれば、あとは F# で処理できます。

複数の金額を金種別の２次元配列にする例です。
```FSharp: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209885/f3e40e44-59cc-b888-418e-b34757fa9e03.png)
#### FSharp.Interop.Dynamic を使うとカッコよくなる？
これで、JavaScript の関数を利用できますが、引っかかっていた点があります。
公式の [ClearScript Examples](https://microsoft.github.io/ClearScript/Examples/Examples.html) （C#）に`function`の呼び出し例が示されています。
```C#: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](https://fsprojects.github.io/FSharp.Interop.Dynamic/) を利用してみます。
```FSharp:F# Interactive
#r "nuget: FSharp.Interop.Dynamic"
open FSharp.Interop.Dynamic;;
```
![ClearScript9.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209885/1406c56e-9d23-aba7-ae7f-425ab28fcb1a.png)
FSharp.Interop.Dynamic で定義された[`?`演算子](https://fsprojects.github.io/FSharp.Interop.Dynamic/reference/fsharp-interop-dynamic-topleveloperators.html)によって、C# に近い記述が可能になります。

`Invoke`メソッドの使用例２つを書き換えてみました。
```FSharp: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209885/bcfeee3b-e4b6-d4b0-6627-afaae74aaccc.png)
「金額」だけをパイプラインに渡せるだけでなく、型推論が効いて`System.Collections.IEnumerable`へのダウンキャストなしに動作します。

つまり、もっとカッコよく(F# らしく、関数合成で)書き換えられるということ。
```FSharp: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209885/0eff9116-800d-4ad5-72bf-e3284abe1f43.png)
実務で使うかは別にして:rolling_eyes:、芸の幅が少し広がりました。
VBScriptもサポートしているので、暇なときに遊んでみようと思います。
## おまけ
C# で動作確認した際のシェルスクリプトと実行画面を載せておきます。
スクリプト下方に "Program.cs"（ClearScript Examples の一部コピー）を書いています。

**<font color=red>注：同名のプロジェクトがあっても上書きします。</font>**
```bash: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209885/2c01a593-d1d6-8d85-41ac-3d20d6d45442.png)
今日は日曜日でした。
## 資料
https://microsoft.github.io/ClearScript/Reference/html/R_Project_Reference.htm

https://microsoft.github.io/ClearScript/Examples/Examples.html

https://microsoft.github.io/ClearScript/Tutorial/FAQtorial.html

https://fsprojects.github.io/FSharp.Interop.Dynamic/index.html