文言(wenyan-lang)へのパッケージ導入と標準出入力

# はじめに
※ この記事は[文言 wenyan-lang アドベントカレンダー](https://qiita.com/advent-calendar/2020/wenyan-lang) 5日目の記事です。
昨日[^0]に引き続きまして@Mopepe51がお送りします。

[^0]: 文言アドベントカレンダー4日目 [文言（wenyan-lang）で乱数を得る](https://qiita.com/Mopepe51/items/a718f1bcbd73ef159c0a)

プログラムをコンソールアプリなどに応用しようと思うとやはり標準入出力が欲しくなります。
文言では標準出力はそれなりに簡単にできますが、標準入力を行うには外部パッケージ、または闇テクニックが必要です。
外部パッケージ導入について日本語文献はなさそうだったのでアドベントカレンダーに便乗し環境構築からまとめていきます。

本記事のコンテンツは

+ 文言(wenyan-lang)のインストール
+ 文言へのbuilt-in以外のパッケージ導入と使用方法
+ 文言での標準入出力の使用方法
+ 公式に提供されていないJavaScriptの機能の使用方法

となっています。文言の基本文法等の解説は[公式WikiのCheatsheet](https://github.com/wenyan-lang/wenyan/wiki/Syntax-Cheatsheet)や他の記事に譲ります。
また漢文はからっきしなのでGoogle翻訳で適当に中国語繁體字に翻訳して命名しています。何か誤りがありましたらご容赦ください。

環境はUbuntu 20.04 LTS on WSL2 です。

# 環境導入
[公式オンラインエディタ](https://ide.wy-lang.org/)にはそもそも標準入力を受け付けるUIがないので、まずは文言をローカル環境にインストールします。
まずNode.jsのパッケージマネージャ「npm」をインストールし、この上でwenyanを導入します。
参考：
[Nodejsのインストール](https://qiita.com/seibe/items/36cef7df85fe2cefa3ea)
[Wenyanのインストール](https://qiita.com/libru1/items/6542b45a945fc7fe10cf)

まっさらなUbuntuにインストールする場合は
Node.js環境インストール[^1]

```
$ sudo apt update
$ sudo apt upgrade
$ sudo apt install -y nodejs npm
```

[^1]: 上記参考記事ではこのあと「n」というバージョン管理ツールを導入しています。このままでも文言は導入できますがお好みで。

Wenyanコンパイラインストール

```
$ npm install -g @wenyanlang/cli
```

のようにします。
さらに文言のパッケージマネージャ「wyg」をインストールします
参考：[公式Wiki Packages-Manager](https://github.com/wenyan-lang/wenyan/wiki/Packages-Manager)

```
$ npm i -g @wenyan/wyg
```

これで文言の各種パッケージをインストールできます。
I/Oのために「交互秘術」というパッケージが作られています。

```
$ mkdir wenyan
$ cd wenyan
$ wyg i 交互秘術
```

[^2]とすると`wenyan/藏書樓/`[^3]に`交互秘術`というフォルダが作られ、パッケージ導入が完了します。

[^2]: `wenyan`のところは好きなフォルダに変更して構いません。また`交互秘術`は`jiaohu`というエイリアスが設定されているのでこちらでも構いません。

[^3]: ちなみに「藏書樓」は図書館の意味

その他のパッケージは[公式Wiki Available-Packages](https://github.com/wenyan-lang/wenyan/wiki/Available-Packages)に記載されている……はずなのですが「子曰」[^4]と「简体秘术」[^5]しか載っていません。
[オンラインエディタ](https://ide.wy-lang.org/?file=import)の「文淵閣 Packages」に載っているもののほうが網羅性が高いようです。
公式に登録されているパッケージは同様に`wyg i <パッケージ名>`とすると導入できます。詳しくは各パッケージのレポジトリのREADMEを参照してください。

[^4]: 孔子に言いたいことを言わせるAAを生成する。なんだそれ

[^5]: 簡体字によるコーディングサポート

自分で書いたパッケージを利用したい場合は上で生成した`藏書樓`の下に`藏書樓/<パッケージ名>/序.wy`、または実行ディレクトリ直下に`./<パッケージ名>.wy`と名付けたファイルを置くと公式のものと同様に`吾嘗觀『<パッケージ名>』之書。方悟「<関数名>」之義。`で呼び出せるようになります。

# 交互秘術パッケージの利用

無事パッケージがインストールできたので使ってみましょう。
echoのプログラムが例として[パッケージ開発者により公開されています](https://github.com/GLanguage/jiaohu-wy/tree/master#%E4%B8%80%E4%BE%8B)。

```wenyan:響應.wy
吾嘗觀『交互秘術』之書。方悟
「正閱」
「已閱」
「化言」
「發生」
「監聽」
「閱止」
「輸出」
之義。

吾有一術。名之曰「響應」。欲行是術。必先得一言。曰「輸入」。乃行是術曰。
    施「化言」於「輸入」。昔之「輸入」者今其是矣。
    寫「輸入」焉。
    生「已閱」之事。
是謂「響應」之術也。

聽得「正閱」之事乃行「響應」之術。
聽得「已閱」之事乃行「閱止」之術。
```

ReferenceError: process is not defined のようなエラーが出ている場合は残念ながらどこかで失敗しています。私ははじめからやり直したら何故かうまくいきました。
導入が成功していれば

```
$ wenyan 響應.wy
```

で入力待ちとなり、文字列を入力、改行すると入力した文字列が標準出力に表示されます。

このプログラムは`吾嘗觀『交互秘術』之書。方悟～之義。`[^quote]で「交互秘術」の各種変数をインポートし、
`吾有一術。名之曰「響應」。～是謂「響應」之術也。`で定義した関数「`響應`」をイベントリスナに登録して受け取った入力に対し作動するようになっています。
動作環境がNode.jsなので漢文関係なく標準入力の扱いがめんどくさいんですね。
Node.jsでの標準入力の扱い方は[こちらの記事](https://qiita.com/hiroqn@github/items/c927bc97780c34eda562#processstdin%E3%81%AE%E3%82%A4%E3%83%99%E3%83%B3%E3%83%88%E3%82%92%E7%9B%A3%E8%A6%96%E3%81%99%E3%82%8B)のように色々ありますが、どれもややこしいです。

[^quote]: なお、`『 』`（=`「「 」」`）は文字列リテラルのクォーテーションです。つまりJSと同様に文字列で指定してパッケージを呼び出しています。

パッケージの中身を追ってNode.jsのコードに直すと以下のようになります。

```js:echo.wenyan.js

var echo = input => {
  process.stdout.write(input.toString());
  emit("end");
};
process.stdin.on("data", echo);
process.stdin.on("end", porocess.stdin.end());
```

他のプログラムに応用する場合は、`響應`の部分を入力の文字列を受け取る関数で置き換えれば大丈夫です。

# AtCoderでは使えない
文言としてまっとうな方法はここまでで述べたとおりですが、標準入出力ができるようになったので競プロerの性として競プロの問題を解いてみたいところです。
文言はJavaScriptへの _コンパイル_ [^compile]機能があるので上記echoプログラムをJSに _コンパイル_ してAtCoder JavaScript(Node.js 12.16.1)でコードテストを実行してみると

[^compile]: 公式ドキュメントではコンパイルとよんでいますが、実際はトランスパイルですね。

```
/imojudge/sandbox/Main.js:1
/*___wenyan_module_交互秘術_start___*/ var 交互秘術 = new function () { var 正閱 = this.正閱 = "data"; var 已閱 = this.已閱 = "end"; var 化言 = this.化言 = _ => { }; 化言 = this.化言 = 甲 => { const _ans1 = 甲.toString(); return _ans1; }; var 發生 = this.發生 = _ => { }; 發生 = this.發生 = 事 => { const _ans2 = ((event) => process.stdin.emit(event))(事); }; var 監聽 = this.監聽 = _ => { }; 監聽 = this.監聽 = 事件 => 響應 => { const _ans3 = ((event) => ((func) => process.stdin.on(event, func)))(事件)(響應); }; var 閱止 = this.閱止 = _ => { }; 閱止 = this.閱止 = () => { const _ans4 = (() => process.stdin.end())(); }; var 輸出 = this.輸出 = _ => { }; 輸出 = this.輸出 = 文 => { const _ans5 = ((s) => process.stdout.write(s))(文); }; };/*___wenyan_module_交互秘術_end___*/var 正閱 = 交互秘術.正閱; var 已閱 = 交互秘術.已閱; var 化言 = 交互秘術.化言; var 發生 = 交互秘術.發生; var 監�

TypeError: process.stdin.end is not a function
    at /imojudge/sandbox/Main.js:1:551
    at ReadStream.閱止.閱止 (/imojudge/sandbox/Main.js:1:557)
    at ReadStream.emit (events.js:323:22)
    at /imojudge/sandbox/Main.js:1:305
    at 發生.發生 (/imojudge/sandbox/Main.js:1:317)
    at ReadStream.響應 (/imojudge/sandbox/Main.js:1:931)
    at ReadStream.emit (events.js:311:20)
    at addChunk (_stream_readable.js:294:12)
    at readableAddChunk (_stream_readable.js:275:11)
    at ReadStream.Readable.push (_stream_readable.js:209:10)
```

のようにランタイムエラーが出てしまいます。どうやら入力を止めるときに用いている`process.stdin.end`がこの環境ではうまく動かないようです。

ローカル環境で実行する分には問題はなく、交互秘術を用いて例えば [ABC 081 A - Placing Marbles](https://atcoder.jp/contests/abs/tasks/abc081_a) を解いてみると以下のようになります[^hensuu]。

[^hensuu]: このあとでJavaScriptの関数を文言の関数として埋め込む方法を解説しますが、実は変数名として埋め込むことも可能です（下から七行目）。

```wenyan:安裝大理石.wy
吾嘗觀『交互秘術』之書。方悟「正閱」「已閱」「化言」「發生」「監聽」「閱止」「輸出」之義。

吾有一列。名之曰「輸入字串」。

吾有一術。名之曰「接收輸入」。欲行是術。必先得一言。曰「輸入」。乃行是術曰。
    施「化言」於「輸入」。昔之「輸入」者今其是矣。
    充「輸入字串」以「輸入」
    生「已閱」之事。
是謂「接收輸入」之術也。

吾有一術。名之曰「解決」。是術曰。
    夫「輸入字串」之一。名之曰「輸入」。
    有數零。名之曰「回答」。
    夫「輸入」之一。名之曰「字符一」。
    若「字符一」等於『1』者。
        加「回答」以一。名之曰「臨時」。
        昔之「回答」者。今「臨時」是矣。
    也。
    夫「輸入」之二。名之曰「字符二」。
    若「字符二」等於『1』者。
        加「回答」以一。名之曰「臨時」。
        昔之「回答」者。今「臨時」是矣。
    也。
    夫「輸入」之三。名之曰「字符三」。
    若「字符三」等於『1』者。
        加「回答」以一。名之曰「臨時」。
        昔之「回答」者。今「臨時」是矣。
    也。

    寫「回答.toString()」焉。
    寫『\n』焉。
    施「閱止」。
是謂「解決」之術也。

聽得「正閱」之事乃行「接收輸入」之術。
聽得「已閱」之事乃行「解決」之術。
```

ローカルで実行してみると

```
$ wenyan 安裝大理石.wy
> 101
> 2
```

のように、たしかに無事回答が得られるのですが、やはりこれを[AtCoderに提出すると](https://atcoder.jp/contests/abs/submissions/18529392) REとなってしまいます。

`process.stdin.end`は交互秘術パッケージ内で使用しているためこれを使用せずに標準入出力をするにはどうしても自分で書かなければなりません。

# 今度こそ標準入力を得る
実は文言の関数は`施「<関数名>」於「<引数>」……。`がJavaScriptの`<関数名>(<引数>,……);`に展開されるので`<関数名>`のところにJavaScriptの関数オブジェクトを直接書くとそれを実行することができます[^curry]。
コードインジェクションみたいでお行儀が悪いですが、実は交互秘術をはじめ公式パッケージでも内部でこのような処理をしています。
昨日の[文言（wenyan-lang）で乱数を得る](https://qiita.com/Mopepe51/items/a718f1bcbd73ef159c0a#javascript%E3%81%AErandom%E9%96%A2%E6%95%B0%E3%81%A7%E4%B9%B1%E6%95%B0%E3%82%92%E5%BE%97%E3%82%8B)でもある程度同様の解説をしているので参考にしてみてください。

[^curry]: 複数の引数を取る場合はカリー化されている必要があります。

文言のせっかくの漢文のコード中にJavaScriptのコードが混入してしまうのは業腹ですが、背に腹は変えられません[^js]。自分で整備して公式に組み込んでもらうなりするしか無いでしょう。

[^js]: 文言はJS以外の言語にトランスパイルする機能もありますが、このようなコードを書くと当然ながら正常に動きません。

前述のNode.jsでの標準入力で一番簡単なのは入力を`require('fs').readFileSync('/dev/stdin', 'utf8')`で一気に受け取ってしまう方法です。

```
今有一術。名之曰「輸入」。是術曰。
    施「(() => require('fs').readFileSync('/dev/stdin', 'utf8'))」。乃得矣。
是謂「輸入」之術也。

施「輸入」。名之曰「甲」。
夫「甲」。以一書之。
```

アロー関数で関数オブジェクトを作り`施`のなかに埋め込んでしまえばOKです。
`輸入`関数の結果を`甲`に格納し、これを`書之`で書き出せばechoプログラムの完成です。

一方、出力については標準で`書之`が用意されていますが、これは数字を漢数字に変換して出力されたり、変数スタックに残っている変数が全て書き出されてしまうなど少々使い勝手が悪いので、変数をそのまま文字列として出力する関数を作っておきましょう。
型変換の構文が見つからないためまたもJSを埋め込み

```
今有一術。名之曰「原始數字輸出」。欲行是術。必先得一數。曰「甲」。乃行是術曰。
    夫「甲」。取一以施「((x) => x.toString())」。取一以書之。
是謂「輸入」之術也。

有數一名之曰「甲」。
施「原始數字輸出」於「甲」。
```

で"1"が出力されます。

これで今度こそ求めた標準入出力が揃いました。
これで [ABC 081 A - Placing Marbles](https://atcoder.jp/contests/abs/tasks/abc081_a) を解くと

```wenyan:安裝大理石_改.wy
今有一術。名之曰「輸入」。是術曰。
    施「(() => require('fs').readFileSync('/dev/stdin', 'utf8'))」。乃得矣。
是謂「輸入」之術也。

今有一術。名之曰「原始數字輸出」。欲行是術。必先得一數。曰「甲」。乃行是術曰。
    夫「甲」。取一以施「((x) => x.toString())」。取一以書之。
是謂「原始數字輸出」之術也。

施「輸入」。名之曰「輸入字串」。

有數零。名之曰「回答」。
夫「輸入字串」之一。名之曰「字符一」。
若「字符一」等於『1』者。
    加「回答」以一。名之曰「臨時」。
    昔之「回答」者。今「臨時」是矣。
也。
夫「輸入字串」之二。名之曰「字符二」。
若「字符二」等於『1』者。
    加「回答」以一。名之曰「臨時」。
    昔之「回答」者。今「臨時」是矣。
也。
夫「輸入字串」之三。名之曰「字符三」。
若「字符三」等於『1』者。
    加「回答」以一。名之曰「臨時」。
    昔之「回答」者。今「臨時」是矣。
也。
施「原始數字輸出」於「回答」。
```

のようになります。今度こそJSに _コンパイル_ してそのまま提出すればACが得られます。
https://atcoder.jp/contests/abs/submissions/18530565

このようにJSの関数を埋め込むことにより、（「文言でのプログラミング」としてどうなのかということはさておき）かなり表現力が上がりました。
いずれsplitのようなよく使うツールをパッケージとして整備してJSが表に出てこないようにしていきたいですね。

# まとめ
文言で（JSに _コンパイル_ したときにAtCoderの環境で動くように）標準入出力を扱うには以下のようにします。

まず入出力用の関数を定義し

```
今有一術。名之曰「輸入」。是術曰。
    施「(() => require('fs').readFileSync('/dev/stdin', 'utf8'))」。乃得矣。
是謂「輸入」之術也。

今有一術。名之曰「原始數字輸出」。欲行是術。必先得一數。曰「甲」。乃行是術曰。
    夫「甲」。取一以施「((x) => x.toString())」。取一以書之。
是謂「原始數字輸出」之術也。
```

入力は以下で改行を含め全入力文字列を`甲`に受け取ります。

```
施「輸入」。名之曰「甲」。
```

出力は、単純に文言標準の方法では

```
夫「甲」。取一書之。
```

で`甲`の内容が標準出力に書き出されます。ただし数字は漢数字として出力される[^cmp]のでローマ数字を出力したい場合は上で定義した`原始數字輸出`を用いて

[^cmp]: コンパイラオプションに `--no-outputHanzi `をつければ `書之` でもローマ数字のまま出力されます。複数出力の機能もあるので慣れればこちらの方が便利なこともあるでしょう。

```
夫「甲」。取一以施「原始數字輸出」。
```

または

```
施「原始數字輸出」於「甲」。
```

のようにします。