はじめに
これからしばらくは、Java 8 にて、REPL の作成にトライします。
REPL(Read(入力の読み取り) - Eval(実行) - Print(結果印刷) -Loop(左記3機能のループ)) といえば、対話形式でプログラムを実行する環境のことで、主にPythonを初めとしたスクリプトプログラムや、CommonLisp などでお馴染みの機能です。
また、入力を解釈・実行して、結果を出力するという意味では、DBMSに付属するコンソールツール( SQLPlus など)も、類似の機能を持つと言えます。
ここでは、プログラム言語のインタラクティブシェルではなく、後者のやや広い意味でのREPL、というよりは、Java製コンソールプログラムの土台を作成します。
以下の記事を参考にさせていただきました。(記載が遅れて申し訳ございません)
REPL(Read-eval-print loop)を作る。
注意点
要件
まずは、作成するREPLの要件です。
- Java8の機能のみで実装する。(SDKと別途ダウンロードが必要なライブラリは使用しない)
- プログラム言語の実行環境ではなく、Java プログラムを内部で呼び出すインターフェースを主眼とする。
- 入力文字列の最初の単語が、コマンド名となる。
プログラムスタイル
Javaの一般的な記述と乖離したオレオレスタイルなので注意して下さい。一応以下の理由があります。
以前のエントリー、Java 8 で字句解析を作る(その1) の再掲+αです。
- 上位プログラムを簡潔にする為、可能な限り例外を投げない、nullを返さない。大量のエラーチェックの中に処理本体が埋没するのを避けたい。
- スコープを最小限度にする。
- 可能な限りイミュータブルなオブジェクトを使用する。
- 一般的な getter、setterは使用しない。(Getter/Setterは悪だ。以上。 原文 Getters/Setters. Evil. Period. 参照)
- ソースをコンパクトにする為、アノテーションは付けない。
- Javaの標準ライブラリに慣れる為、および機能の所在を明らかにする為、import文は原則使わない。記述が冗長になりがちな、Stream関連クラスのインポートくらい。
- ソースファイル数の削減の為、細かいクラスやインターフェースは、1ファイルにまとめることがある。
- ~~職場で長大でディープなネスト文に辟易としている為、~~早期リターンを多用しています。
意外に否定派の方が多いですが、処理中のエラーチェックや、深いネストで処理本体の流れが埋もれることを防ぐ為、あるいはメソッド初期のリターンを表明として読み取って欲しいことから使用しています。
BaseREPL クラス
記事を書くにあたり、トップダウンで説明しようか、ボトムアップで説明しようか悩みましたが、全体像をはっきりさせる為、上から説明することにします。
知らないクラス名が出てきたら、「これから詳細は解説するんだろ?」くらいの気持ちで見てください。
まずは、抽象クラス BaseREPLの全体像です。
abstract class BaseREPL {
enum States {
Quit, // 終了
Finished, // Cleanup() 済み
Error, // エラー
NotFind, // 対象コマンド無し
Input, // 入力 (default)
Incomplete, // 入力未完了(複数行入力をサポートする場合に使用)
Complete, // 入力完了
}
BaseREPL(String encoding) {
if ( encoding == null || encoding.trim().isEmpty() ) { encoding = "UTF-8"; }
console_ = Service.Console.create(encoding);
}
/** メインループ.(template method) */
public final void run() {
if ( state_ == States.Finished ) { return; }
try {
welcome();
while (state_ != States.Quit) {
state_ = response( eval( accept(prompt_) ) );
}
goodbye();
} finally {
cleanup();
state_ = States.Finished;
}
}
// user defined methods ==========================================================================
abstract protected String accept(String prompt); // プロンプト表示・入力受付
abstract protected States eval(String input); // 入力文字列解釈・実行
abstract protected States response(States state); // eval() 後の後処理
abstract protected void cleanup(); // REPL終了時の後始末
abstract protected void welcome(); // REPL開始文字列の表示
abstract protected void goodbye(); // REPL終了文字列の表示
abstract protected void help(); // ヘルプ文字列の表示
// print strings to console ======================================================================
public final void print(String format, Object... args) { console_.print(format, args); }
public final void print(String s) { console_.print(s); }
public final void print(String[] sz) { for (String s : sz) { print(s); } }
public final void print(java.util.Collection<String> sz) { for (String s : sz) { print(s); } }
// internal fields ===============================================================================
protected final Interface.Console console_ = Service.Console.get();
protected String prompt_ = "REPL> ";
protected States state_ = States.Input;
}
解説
ソースの解説です。といっても抽象クラスの為、殆どカラです。
コンストラクタ
2018/10/28 追加
コンソールのエンコーディング名を受け取るコンストラクタを追加しました。
null、あるいは空白が渡された場合は、デフォルトで "UTF-8" に設定します。
BaseREPL(String encoding) {
if ( encoding == null || encoding.trim().isEmpty() ) { encoding = "UTF-8"; }
console_ = Service.Console.create(encoding);
}
protected final Interface.Console console_;
メインループ
メインループは、固定処理になります。
-
welcome()
で、まずは、REPL起動時のあいさつ。 - 状態が、States.QUit になるまで、以下の処理を繰り返し。
-
accept()
で、プロンプト表示と入力受付。 -
eval()
で、accept()
で入力された文字列を解釈・実行。 -
response()
で、eval()
の処理結果を受けた後処理を実行。
-
-
goodbye()
で、REPL終了のあいさつ。 -
cleanup()
で、後始末。
enum States
はREPLの状態を表す列挙型です。
このクラスでは、States.Finished と、States.Quit のみ使用しています。
この他の列挙子はコメントの記述にも関わらず、特に使い方を決めてありません。継承先で好きなように利用することが出来ます。(自分でもStates.input と States.Error、States.Missing 以外の使い道が思い当たらない...)
cleanup() で、使用したオブジェクトやリソースの後始末を行うことを想定している為、一度 run() メソッドを抜けたら、リソース不足で異常終了する可能性大です、その為、メソッドの冒頭に終了済みかどうかのチェックを入れています。また、注意点で書いたように、極力例外を投げないスタイルで記述を行いますが、止む無く例外が発生し場合でも、確実に cleanup()が実行されるよう、処理本体をtry{}
で囲み、finally {}
節の中に cleanup() メソッドの呼び出しを入れています。
従って、exit()なんて使ってはいけません。
enum States {
Quit, // 終了
Finished, // Cleanup() 済み
Error, // エラー
NotFind, // 対象コマンド無し
Input, // 入力 (default)
Incomplete, // 入力未完了(複数行入力をサポートする場合に使用)
Complete, // 入力完了
}
/** メインループ.(template method) */
public final void run() {
if ( state_ == States.Finished ) { return; }
try {
welcome();
while (state_ != States.Quit) {
state_ = response( eval( accept(prompt_) ) );
}
goodbye();
} finally {
cleanup();
state_ = States.Finished;
}
}
protected String prompt_ = "REPL> ";
protected States state_ = States.Input;
ユーザー定義メソッド
ここに含まれるメソッドは、BaseREPL を継承したクラスが実装しなければならないクラスです。
何をするメソッドなのかは、メインループで解説してしまいましたので省略します。
help()
だけは、メインループで使っていません。このメソッドは、eval()
で、ヘルプの表示と解釈された時、そして、ヘルプテキストをサポートする場合に継承先クラスで実装します。
abstract protected String accept(String prompt); // プロンプト表示・入力受付
abstract protected States eval(String input); // 入力文字列解釈・実行
abstract protected States response(States state); // eval 後の後処理
abstract protected void cleanup(); // REPL終了時の後始末
abstract protected void welcome(); // REPL開始文字列の表示
abstract protected void goodbye(); // REPL終了文字列の表示
abstract protected void help(); // ヘルプ文字列の表示
出力サービスメソッド
コンソールへの出力を提供するメソッドです。
メソッド名は全て print に統一して、引数のタイプによるオーバーロードを4種類用意しました。
-
print(String, Object...)
は、フォーマット付き出力( printf() に相当 ) を提供します。 -
print(String)
は、受け取った文字列 + 改行を出力します。println() に相当します。 -
print(String[])
、print(Collection<String>)
は、文字列のコレクションを受け取り、コレクションの1要素ごとに1行の出力を行います。
これらのメソッドは、最終的に、Interface.Console オブジェクトに処理を委譲します。このオブジェクトは、後日(たぶん次回)説明を行います。
public final void print(String format, Object... args) { console_.print(format, args); }
public final void print(String s) { console_.print(s); }
public final void print(String[] sz) { for (String s : sz) { print(s); } }
public final void print(java.util.Collection<String> sz) { for (String s : sz) { print(s); } }
protected final Interface.Console console_;
まとめ
以上が、REPLの基本クラスの説明になります。
ソースを見ていただくとお分かりのように、run()
メソッドが、GoF デザインパターンのTemplateMethodにあたります。
それを表現する為 interface ではなく、抽象クラスとして定義しています。
この他にも、継承先の実装に依存しないメソッドは、final 修飾子付きで実装を済ませてあります。
この抽象クラスをベースに、次回は具体的なREPLクラスの実装と行きたいところですが、いろいろ欲が出てきたので、継承後のクラスで利用するメカニズムを作っていく予定です。