OSS化されたPower Fxで遊んでみる

# はじめに

Microsoft Power Apps等に組み込まれているプログラミング言語[Power Fx](https://docs.microsoft.com/ja-jp/power-platform/power-fx/overview)が11月にOSS化されました[^1]。

https://github.com/microsoft/Power-Fx

公式サンプルにREPLがあったので、軽く触ってみた内容を紹介したいと思います。

# 実行環境

公式のサンプルアプリにREPLのCLIがあったので、こちらを使用しました。

https://github.com/microsoft/power-fx-host-samples/tree/main/Samples/ConsoleREPL

推奨ビルド環境はVisual Studio 2019ですが、~~準備が面倒~~実行環境を手軽に用意したかったのでDocker image化しました。

https://github.com/Syuparn/power-fx-image

使用バージョン: `0.2.1-preview`

（Docker imageのビルドにはハマりポイントがありましたがそちらは後述）

```bash
$ docker run -it --rm ghcr.io/syuparn/power-fx-console-repl:main ConsoleREPL
Microsoft Power Fx Console Formula REPL, Version 0.2
Enter Excel formulas.  Use "Help()" for details.

> 2 + 3
5

> Exit()
```

# 特徴

[公式サイト](https://docs.microsoft.com/ja-jp/power-platform/power-fx/overview)で「Excelのような数式バーで操作できる」と書かれているように、セルに打ち込む数式をそのままプログラミング言語にしたような雰囲気の言語です。

- 関数型・宣言的
- コレクションは`レコード`と`テーブル`で表現(RDB, Excel的)
- 値が依存する変数の変更にあわせて更新（リアクティブプログラミング？）
- UIや（ローコード、ノーコード）ツールに組み込んで使う想定
- 処理系はC#製

詳細については公式リファレンスをご覧ください[^2]。

https://github.com/microsoft/Power-Fx/tree/main/docs

# REPL

1行ごとに評価された値が表示されます。
使い方は `Help()` で確認可能です（ただし、引数の説明は無いので[テストケース](https://github.com/microsoft/Power-Fx/tree/main/src/tests/Microsoft.PowerFx.Core.Tests/ExpressionTestCases)を見よう見まねで使いました）。

```
> 2 + 3
5
> Help()
"
Set( <identifier>, <expression> ) creates or changes a variable's value.
<identifier> = <expression> defines a formula with automatic recalc.
<expression> alone is evaluated and the result displayed.

Available functions (all are case sensitive):
  Help            Reset           Exit            Abs             AddColumns    
  And             Average         Average         Blank           Concat        
  Coalesce        Char            Concatenate     CountIf         CountRows     
  Date            DateAdd         DateDiff        DateValue       DateTimeValue 
  Day             EndsWith        Exp             IsBlank         IsError       
  IsToday         If              IfError         Int             Filter        
  First           FirstN          ForAll          Hour            Last          
  LastN           Left            Len             Ln              Log           
  Lower           Max             Max             Mid             Min           
  Min             Minute          Mod             Month           Not           
  Now             Or              Power           Replace         Right         
  Round           RoundUp         RoundDown       Second          Sequence      
  Sort            StartsWith      Sum             Sum             Split         
  Sqrt            Substitute      Switch          Table           Text          
  Time            TimeValue       Today           Trim            TrimEnds      
  Trunc           Upper           Value           With            Year          
  Set

Available operators: = <> <= >= + - * / % && And || Or ! Not in exactin 

Record syntax is { < field >: < value >, ... } without quoted field names.
    Example: { Name: ""Joe"", Age: 29 }
Use the Table function for a list of records.  
    Example: Table( { Name: ""Joe"" }, { Name: ""Sally"" } )
Use [ <value>, ... ] for a single column table, field name is ""Value"".
    Examples: [ 1, 2, 3 ] 
Records and Tables can be arbitrarily nested.

Once a formula is defined or a variable's type is defined, it cannot be changed.
    Use the Reset() function to clear all formulas and variables.
```

変数は `Set(var, val)` で定義できます。グローバルが汚れてきたら `Reset()` で変数を全削除できます。

```
> 2 + 3
5

> Set(x, 2)
x: 2

> Set(y, 3)
y: 3

> x + y
5

> Reset()
True

> x    
Errors: Error 0-1: Name isn't valid. This identifier isn't recognized.
```

`With` で、閉じたスコープでのみ変数を使うこともできます。

```
> With({x: 2}, x * 3)
6

> With({x: 2}, With({y: 3}, x * y))
6

> x
Errors: Error 0-1: Name isn't valid. This identifier isn't recognized.
```

(上記のような書き方ができるところから、引数は遅延評価されていそうです)

最後に、使い終わったら `Exit()` で終了します。

# 値の自動更新

依存する変数の値に応じて値が更新されます。Excelで1セル書き直すと、そこを参照するセルも芋づる式に更新されるのと同じイメージです。

```
> Set(x, 3)
x: 3

> y = x * 2 # 式で定義
y: 6

> Set(x, 7)
x: 7
y: 14
```

一方、Setで変数に代入した場合は自動更新されません。

```
> Set(x, 2)
x: 2

> Set(y, x * 3)
y: 6

> Set(x, 5)
x: 5

> y
6
```

[リファレンス](https://github.com/microsoft/Power-Fx/blob/main/docs/variables.md)にも

> Power Fx and Excel both automatically recalculate formulas as the input data changes, so you usually don't need to create and update variables. 
> (Power FxとExcelはいずれも、入力データの変更に従って自動で式を再計算します。そのため、普段は変数を作成、更新する必要はありません。)

とあり、式を使うことが推奨されているようです。

式で定義した場合、Setで変数の上書きはできないようです（このあたりの挙動はまだよくわかってない）。

```
> x = 1
x: 1

> Set(x, 2)
x: 2
x: 1

> x
1

> Set(y, x * 2)
y: 2
```

# コレクション処理

`Table` を利用します。

```
> Filter(Table({x: 1, y: 2}, {x: 3, y: 5}), x > 2)
Table({x:3, y:5})

> ForAll(Table({x: 1, y: 2}, {x: 3, y: 5}), x + y) 
[3, 8]
```

配列のように見える `[]` は、Tableのシンタックスシュガーです。

```
> Table({Value: 1}, {Value: 2}, {Value: 3})
[1, 2, 3]
```

数列の生成もできます。

```
# pythonのrangeと違って（個数、初期値、公差）なので注意！
> Sequence(5, 10, 2)
[10, 12, 14, 16, 18]
```

入れ子のTableでも、disambiguation(曖昧性解消)演算子　`@` でシャドーイングを回避できます。

```
# &は文字列結合
> ForAll( X, ForAll( Y, Value & Text( X[@Value] )))
[["A1", "B1"], ["A2", "B2"]]

> ForAll(X, ForAll( Y, Value & Text(ThisRecord.Value)))
[["AA", "BB"], ["AA", "BB"]]
> ForAll(Table({a: 1, b: 2}, {a: 3, b: 4}), a)
[1, 3]
```

# Docker image化する際にはまったところ

イメージ化にあたりASP.NET Coreを使用しましたが、そのままでは `CultureInfo` が読み込めずevaluatorが動きませんでした。

```bash
# dotnet ConsoleREPL.dll
Microsoft Power Fx Console Formula REPL, Version 0.2
Enter Excel formulas.  Use "Help()" for details.

> l
The type initializer for 'Microsoft.PowerFx.Core.Texl.BuiltinFunctionsCore' threw an exception.
```

エラーの概要は以下の通りです。

- いくつかの組み込み関数でロケールを使うため、処理系が `CultureInfo` を参照
  - [CultureInfo.CurrentCulture プロパティ (System.Globalization) \| Microsoft Docs](https://docs.microsoft.com/ja-jp/dotnet/api/system.globalization.cultureinfo.currentculture?view=net-6.0)
- ASP.NET Coreのイメージではロケールが設定されていない（？）
- 結果 `CultureInfo.CurrentCulture.Name` がnullとなりバリデーションチェック失敗、評価が異常終了する
  - `CultureInfo.CurrentCulture.Name` 利用箇所 https://github.com/microsoft/Power-Fx/blob/47f0dfc2b13dcbdd8a2310289c902e19f9752314/src/libraries/Microsoft.PowerFx.Core/Localization/CurrentLocale.cs#L12
  - ここでnullチェック https://github.com/microsoft/Power-Fx/blob/47f0dfc2b13dcbdd8a2310289c902e19f9752314/src/libraries/Microsoft.PowerFx.Core/Localization/StringResources.cs#L131

対策として、REPLのコードを以下のように書き替えています。[^3]

```csharp:power-fx-host-samples/Samples/ConsoleREPL/ConsoleREPL.cs
using System;
using System.Globalization; // 追加
using System.Text.RegularExpressions;
using Microsoft.PowerFx;
using Microsoft.PowerFx.Core.Public.Values;
using Microsoft.PowerFx.Core.Public.Types;

namespace PowerFxHostSamples
{
    class ConsoleRepl
    {
        private static RecalcEngine engine;

        public static void Main()
        {
            // CurrentCultureがnullになるのを防ぐため、評価前にデフォルト値（英語）を設定
            CultureInfo.DefaultThreadCurrentCulture = CultureInfo.CreateSpecificCulture("en-US"); // 追加
            ResetEngine();
            // ...
```

# おわりに

以上、Power Fxの簡単な紹介でした。これからも継続的に関数が追加されていくようなので注目していきたいです。また、サンプルにはweb版のREPLもあったので、機会があればこちらもDockerで動かしてみたいと思います。

[^1]: 正確には3月からリポジトリがあったのですが、今までは文法のドキュメントだけでした
[^2]: ただし、サンプルコードの中にまだ実装されていない関数もいくつかあったので実際に動かすのが確実です (`Ungroup` 等)
[^3]: Dockerfileの中では無理やりsedを使っていますが悪しからず :innocent: