LoginSignup
2
0

More than 3 years have passed since last update.

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

Posted at

はじめに

※ この記事は文言 wenyan-lang アドベントカレンダー 5日目の記事です。
昨日1に引き続きまして@Mopepe51がお送りします。

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

本記事のコンテンツは

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

となっています。文言の基本文法等の解説は公式WikiのCheatsheetや他の記事に譲ります。
また漢文はからっきしなのでGoogle翻訳で適当に中国語繁體字に翻訳して命名しています。何か誤りがありましたらご容赦ください。

環境はUbuntu 20.04 LTS on WSL2 です。

環境導入

公式オンラインエディタにはそもそも標準入力を受け付けるUIがないので、まずは文言をローカル環境にインストールします。
まずNode.jsのパッケージマネージャ「npm」をインストールし、この上でwenyanを導入します。
参考:
Nodejsのインストール
Wenyanのインストール

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

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

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

$ npm install -g @wenyanlang/cli

のようにします。
さらに文言のパッケージマネージャ「wyg」をインストールします
参考:公式Wiki Packages-Manager

$ npm i -g @wenyan/wyg

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

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

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

その他のパッケージは公式Wiki Available-Packagesに記載されている……はずなのですが「子曰」5と「简体秘术」6しか載っていません。
オンラインエディタの「文淵閣 Packages」に載っているもののほうが網羅性が高いようです。
公式に登録されているパッケージは同様にwyg i <パッケージ名>とすると導入できます。詳しくは各パッケージのレポジトリのREADMEを参照してください。

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

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

無事パッケージがインストールできたので使ってみましょう。
echoのプログラムが例としてパッケージ開発者により公開されています

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

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

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

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

$ wenyan 響應.wy

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

このプログラムは吾嘗觀『交互秘術』之書。方悟~之義。7で「交互秘術」の各種変数をインポートし、
吾有一術。名之曰「響應」。~是謂「響應」之術也。で定義した関数「響應」をイベントリスナに登録して受け取った入力に対し作動するようになっています。
動作環境がNode.jsなので漢文関係なく標準入力の扱いがめんどくさいんですね。
Node.jsでの標準入力の扱い方はこちらの記事のように色々ありますが、どれもややこしいです。

パッケージの中身を追ってNode.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への コンパイル 8機能があるので上記echoプログラムをJSに コンパイル してAtCoder JavaScript(Node.js 12.16.1)でコードテストを実行してみると

/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 を解いてみると以下のようになります9

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

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

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

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

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

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

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

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

のように、たしかに無事回答が得られるのですが、やはりこれをAtCoderに提出すると REとなってしまいます。

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

今度こそ標準入力を得る

実は文言の関数は施「<関数名>」於「<引数>」……。がJavaScriptの<関数名>(<引数>,……);に展開されるので<関数名>のところにJavaScriptの関数オブジェクトを直接書くとそれを実行することができます10
コードインジェクションみたいでお行儀が悪いですが、実は交互秘術をはじめ公式パッケージでも内部でこのような処理をしています。
昨日の文言(wenyan-lang)で乱数を得るでもある程度同様の解説をしているので参考にしてみてください。

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

前述の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 を解くと

安裝大理石_改.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())」。取一以書之。
是謂「原始數字輸出」之術也。

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

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

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

夫「甲」。取一書之。

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

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

または

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

のようにします。


  1. 文言アドベントカレンダー4日目 文言(wenyan-lang)で乱数を得る 

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

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

  4. ちなみに「藏書樓」は図書館の意味 

  5. 孔子に言いたいことを言わせるAAを生成する。なんだそれ 

  6. 簡体字によるコーディングサポート 

  7. なお、『 』(=「「 」」)は文字列リテラルのクォーテーションです。つまりJSと同様に文字列で指定してパッケージを呼び出しています。 

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

  9. このあとでJavaScriptの関数を文言の関数として埋め込む方法を解説しますが、実は変数名として埋め込むことも可能です(下から七行目)。 

  10. 複数の引数を取る場合はカリー化されている必要があります。 

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

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

2
0
0

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
  3. You can use dark theme
What you can do with signing up
2
0