Interpreter
文法規則をクラスで表現する
概要
言語単体で柔軟性が得られないときは、別言語のインタプリタを使え 他の人でも書ける
マンガでわかる Interpreter
マンガでわかる Interpreter #デザインパターン - Qiita
でざぱたんで覚える Interpreter
ちびキャラは「インタプリタたん」。未知の言語の本を1時間で構造解析してしまう放浪の言語学者。「構造さえ見抜ければ、どんな言語だって解釈可能」——文法を定義し、それに従って文を評価するのがInterpreterで、処理を無限に書き続けるくらいならミニ言語(DSL)を定義して解釈させる方が楽、という動機まで含めて描かれる。式が式を含む再帰構造ゆえ、実質Compositeと同じ形になるという指摘も。
出典: いしだけ『でざぱたん: ちびキャラで覚えるデザインパターン』(P.187〜)
その前に
DSL:ドメイン特化言語
色々な意味があるみたいで
あるところでは「ある言語の用途特価機能関数」みたいな文脈で使われていた KotlinでHTMLを書くための「DSL」とか
ユースケース
ゲームシナリオの分岐
ゲームのシナリオ分岐するにあたり、PHPで柔軟な分岐に対応するのは無理があるので、yamlで分岐内容を書いて、PHPではその解析だけを行う
SQL
ORマッパーでOOPからSQLに変換したい時とか、構文要素をオブジェクトに閉じ込めたらいいじゃない
クラス図
このサイトの実装(ミニ言語のパーサ)での対応関係:
classDiagram
class Node {
<<abstract>>
+parse(context)
}
class ProgramNode {
-commandListNode Node
+parse(context)
}
class CommandListNode {
-list ArrayList
+parse(context)
}
class CommandNode {
-node Node
+parse(context)
}
class RepeatCommandNode {
-number int
-commandListNode Node
+parse(context)
}
class PrimitiveCommandNode {
-name String
+parse(context)
}
class Context {
-tokenizer StringTokenizer
-currentToken String
+nextToken() String
+currentToken() String
+skipToken(token)
+currentNumber() int
}
class ParseException
Node <|-- ProgramNode
Node <|-- CommandListNode
Node <|-- CommandNode
Node <|-- RepeatCommandNode
Node <|-- PrimitiveCommandNode
ProgramNode o-- CommandListNode
CommandListNode o-- CommandNode : list
CommandNode o-- Node : node
RepeatCommandNode o-- CommandListNode
Node ..> Context : parse(context)
Node ..> ParseExceptionJava
public abstract class Node {
public abstract void parse(Context context) throws ParseException;
}
import java.util.ArrayList;
// <command list> ::= <command>* end
public class CommandListNode extends Node {
private ArrayList list = new ArrayList();
public void parse(Context context) throws ParseException {
while (true) {
if (context.currentToken() == null) {
throw new ParseException("Missing 'end'");
} else if (context.currentToken().equals("end")) {
context.skipToken("end");
break;
} else {
Node commandNode = new CommandNode();
commandNode.parse(context);
list.add(commandNode);
}
}
}
public String toString() {
return list.toString();
}
}
import java.util.*;
public class Context {
private StringTokenizer tokenizer;
private String currentToken;
public Context(String text) {
tokenizer = new StringTokenizer(text);
nextToken();
}
public String nextToken() {
if (tokenizer.hasMoreTokens()) {
currentToken = tokenizer.nextToken();
} else {
currentToken = null;
}
return currentToken;
}
public String currentToken() {
return currentToken;
}
public void skipToken(String token) throws ParseException {
if (!token.equals(currentToken)) {
throw new ParseException("Warning: " + token + " is expected, but " + currentToken + " is found.");
}
nextToken();
}
public int currentNumber() throws ParseException {
int number = 0;
try {
number = Integer.parseInt(currentToken);
} catch (NumberFormatException e) {
throw new ParseException("Warning: " + e);
}
return number;
}
}
// <program> ::= program <command list>
public class ProgramNode extends Node {
private Node commandListNode;
public void parse(Context context) throws ParseException {
context.skipToken("program");
commandListNode = new CommandListNode();
commandListNode.parse(context);
}
public String toString() {
return "[program " + commandListNode + "]";
}
}
// <primitive command> ::= go | right | left
public class PrimitiveCommandNode extends Node {
private String name;
public void parse(Context context) throws ParseException {
name = context.currentToken();
context.skipToken(name);
if (!name.equals("go") && !name.equals("right") && !name.equals("left")) {
throw new ParseException(name + " is undefined");
}
}
public String toString() {
return name;
}
}
public class ParseException extends Exception {
public ParseException(String msg) {
super(msg);
}
}
// <command> ::= <repeat command> | <primitive command>
public class CommandNode extends Node {
private Node node;
public void parse(Context context) throws ParseException {
if (context.currentToken().equals("repeat")) {
node = new RepeatCommandNode();
node.parse(context);
} else {
node = new PrimitiveCommandNode();
node.parse(context);
}
}
public String toString() {
return node.toString();
}
}
// <repeat command> ::= repeat <number> <command list>
public class RepeatCommandNode extends Node {
private int number;
private Node commandListNode;
public void parse(Context context) throws ParseException {
context.skipToken("repeat");
number = context.currentNumber();
context.nextToken();
commandListNode = new CommandListNode();
commandListNode.parse(context);
}
public String toString() {
return "[repeat " + number + " " + commandListNode + "]";
}
}
import java.util.*;
import java.io.*;
public class Main {
public static void main(String[] args) {
try {
BufferedReader reader = new BufferedReader(new FileReader("program.txt"));
String text;
while ((text = reader.readLine()) != null) {
System.out.println("text = \"" + text + "\"");
Node node = new ProgramNode();
node.parse(new Context(text));
System.out.println("node = " + node);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
Go
Nodeはfmt.Stringerを合成したinterfaceで表現し、parse(Context) throws ParseExceptionは Go に例外機構が無いので慣用のerror戻り値で置き換える(if err := child.Parse(ctx); err != nil { return err }を積み重ねるだけで、Javaのtry/catchが「途中で打ち切って上位へ伝播する」のと同じ効果になる)。CommandListNodeのtoStringはJavaのjava.util.ArrayList既定の"[e1, e2, e3]"書式をstrings.Joinで手動再現している。また、Java版はPrimitiveCommandNode.parse()がcontext.currentToken()のnull(文法上リーチしないが)に対して無防備にNullPointerExceptionを踏みうる作りだが、Go版はトークン切れを先に検出してerrorへ倒す。
実行: go run ./GoF/patterns/Interpreter/go
package main
// ---- node 層(Java版の Node.java に相当)----
//
// Java版 Node は parse(Context) throws ParseException だけを持つ抽象クラスで、
// toString()はObjectから継承したものを各サブクラスがオーバーライドする。
// Goには抽象クラスも例外機構も無いため、次の2点で置き換える。
//
// - 抽象メソッドparse: フィールドを持たないインタフェースのメソッドとして表現する。
//
// - throws ParseException: error戻り値として表現する。これがGoの慣用的な
// エラーハンドリングで、呼び出し側は各呼び出しの直後で
//
// if err := child.Parse(ctx); err != nil {
// return err
// }
//
// という形を積み重ねるだけで、Javaのtry/catchで「途中で打ち切って上位へ伝播する」
// のと同じ効果を得る(パニック/リカバーは使わない)。
type Node interface {
Parse(ctx *Context) error
String() string
}
package main
import (
"fmt"
"strconv"
"strings"
)
// ---- context 層(Java版の Context.java に相当)----
//
// Java版はjava.util.StringTokenizerで字句を1つずつ取り出すが、Go標準ライブラリに
// 直接の相当品は無い。strings.Fields()で行全体をあらかじめ空白区切りのトークン列へ
// 分割しておき、添字posを1つずつ進める方式にする。「現在のトークンを覗いて
// (CurrentToken)、1つ読み進める(NextToken)」という逐次パーサ用の操作性はJava版と
// 変わらない。
//
// Java版 currentToken() はトークン切れでnullを返すが、GoのstringにはJavaのnullに
// 相当する値が無いため、ここでは (string, bool) の2値返し(mapの comma-ok と同じ形)で
// 「トークンが残っているか」を表す。
// Context は構文解析中の字句列とその読み取り位置を保持する。
type Context struct {
tokens []string
pos int
}
// NewContext はコンストラクタ相当。strings.Fields()は連続する空白をまとめて捨てるので、
// Java版のStringTokenizer(デフォルト区切り文字=空白類)と同じトークン列が得られる。
func NewContext(text string) *Context {
return &Context{tokens: strings.Fields(text)}
}
// CurrentToken は現在のトークンを返す。okがfalseならトークン切れ
// (Java版でcurrentToken()がnullを返す状態)を表す。
func (c *Context) CurrentToken() (token string, ok bool) {
if c.pos >= len(c.tokens) {
return "", false
}
return c.tokens[c.pos], true
}
// NextToken は読み取り位置を1つ進め、新しい現在のトークンを返す。
func (c *Context) NextToken() (token string, ok bool) {
c.pos++
return c.CurrentToken()
}
// SkipToken はJava版 skipToken(String) 相当。現在のトークンが期待した文字列と
// 一致しなければエラーを返す(Java版はParseExceptionをthrowする)。
// エラーメッセージはGoの慣習(小文字始まり・末尾ピリオドなし)に合わせているが、
// 内容はJava版の "X is expected, but Y is found." と同じ情報を持つ。
func (c *Context) SkipToken(token string) error {
cur, ok := c.CurrentToken()
if !ok {
return fmt.Errorf("%s is expected, but <eof> is found", token)
}
if cur != token {
return fmt.Errorf("%s is expected, but %s is found", token, cur)
}
c.NextToken()
return nil
}
// CurrentNumber はJava版 currentNumber() 相当。現在のトークンを数値として読む
// (読み取り位置そのものは進めない。進めるかどうかの判断は呼び出し側=RepeatCommandNodeに
// 委ねているのはJava版と同じ)。
func (c *Context) CurrentNumber() (int, error) {
cur, ok := c.CurrentToken()
if !ok {
return 0, fmt.Errorf("number is expected, but <eof> is found")
}
n, err := strconv.Atoi(cur)
if err != nil {
// %w でJava版のNumberFormatException相当の元エラーをラップしておく
// (errors.Is/Asで下位のstrconv.NumErrへ辿れる、Goらしい振る舞い)。
return 0, fmt.Errorf("invalid number %q: %w", cur, err)
}
return n, nil
}
package main
import "fmt"
// ---- programNode 層(Java版の ProgramNode.java に相当)----
//
// <program> ::= program <command list>
// ProgramNode は構文解析の最上位(プログラム全体)を表すNode。
type ProgramNode struct {
commandListNode Node
}
// NewProgramNode はコンストラクタ相当。フィールドはJava版同様Parse時に埋まる。
func NewProgramNode() *ProgramNode {
return &ProgramNode{}
}
// Parse はJava版 parse(Context) throws ParseException 相当。
func (n *ProgramNode) Parse(ctx *Context) error {
if err := ctx.SkipToken("program"); err != nil {
return err
}
commandListNode := NewCommandListNode()
if err := commandListNode.Parse(ctx); err != nil {
return err
}
n.commandListNode = commandListNode
return nil
}
// String はJava版 toString() 相当。
func (n *ProgramNode) String() string {
return fmt.Sprintf("[program %s]", n.commandListNode)
}
package main
import (
"errors"
"strings"
)
// ---- commandListNode 層(Java版の CommandListNode.java に相当)----
//
// <command list> ::= <command>* end
// CommandListNode はcommand列(<command>*)を表すNode。
type CommandListNode struct {
list []Node
}
// NewCommandListNode はコンストラクタ相当。
func NewCommandListNode() *CommandListNode {
return &CommandListNode{}
}
// Parse はJava版 parse(Context) throws ParseException 相当。
func (n *CommandListNode) Parse(ctx *Context) error {
for {
token, ok := ctx.CurrentToken()
switch {
case !ok:
return errors.New("missing 'end'")
case token == "end":
return ctx.SkipToken("end")
default:
commandNode := NewCommandNode()
if err := commandNode.Parse(ctx); err != nil {
return err
}
n.list = append(n.list, commandNode)
}
}
}
// String はJava版 list.toString()(java.util.ArrayListの既定のtoString)相当。
// "[e1, e2, e3]" 形式を手で組み立てる(GoのスライスはこのtoString書式を持たないため)。
func (n *CommandListNode) String() string {
parts := make([]string, len(n.list))
for i, node := range n.list {
parts[i] = node.String()
}
return "[" + strings.Join(parts, ", ") + "]"
}
package main
// ---- commandNode 層(Java版の CommandNode.java に相当)----
//
// <command> ::= <repeat command> | <primitive command>
// CommandNode はrepeatコマンドかprimitiveコマンドかを判定して委譲するだけのNode。
type CommandNode struct {
node Node
}
// NewCommandNode はコンストラクタ相当。
func NewCommandNode() *CommandNode {
return &CommandNode{}
}
// Parse はJava版 parse(Context) throws ParseException 相当。
func (n *CommandNode) Parse(ctx *Context) error {
var child Node
if token, ok := ctx.CurrentToken(); ok && token == "repeat" {
child = NewRepeatCommandNode()
} else {
child = NewPrimitiveCommandNode()
}
if err := child.Parse(ctx); err != nil {
return err
}
n.node = child
return nil
}
// String はJava版同様、内側のNodeへそのまま委譲する(自分では括弧を付けない)。
func (n *CommandNode) String() string {
return n.node.String()
}
package main
import "fmt"
// ---- primitiveCommandNode 層(Java版の PrimitiveCommandNode.java に相当)----
//
// <primitive command> ::= go | right | left
// validPrimitiveCommands はJava版の name.equals("go") || name.equals("right") ||
// name.equals("left") という判定を集合で表したもの。
var validPrimitiveCommands = map[string]bool{"go": true, "right": true, "left": true}
// PrimitiveCommandNode は go/right/left いずれかの終端コマンドを表すNode。
type PrimitiveCommandNode struct {
name string
}
// NewPrimitiveCommandNode はコンストラクタ相当。
func NewPrimitiveCommandNode() *PrimitiveCommandNode {
return &PrimitiveCommandNode{}
}
// Parse はJava版 parse(Context) throws ParseException 相当。
//
// Java版は context.currentToken() がnullでも構わず name.equals(...) を呼び出して
// しまうため、文法上リーチしない経路とはいえNullPointerExceptionの芽がある。
// Go版ではその芽を先に断ち切り、トークン切れなら(パニックにせず)ParseException相当の
// errorを返す。
func (n *PrimitiveCommandNode) Parse(ctx *Context) error {
token, ok := ctx.CurrentToken()
if !ok {
return fmt.Errorf("go, right or left is expected, but <eof> is found")
}
n.name = token
if err := ctx.SkipToken(n.name); err != nil {
return err
}
if !validPrimitiveCommands[n.name] {
return fmt.Errorf("%s is undefined", n.name)
}
return nil
}
// String はJava版 toString() 相当。
func (n *PrimitiveCommandNode) String() string {
return n.name
}
package main
import "fmt"
// ---- repeatCommandNode 層(Java版の RepeatCommandNode.java に相当)----
//
// <repeat command> ::= repeat <number> <command list>
// RepeatCommandNode はrepeat...endで囲まれた繰り返しコマンドを表すNode。
type RepeatCommandNode struct {
number int
commandListNode Node
}
// NewRepeatCommandNode はコンストラクタ相当。
func NewRepeatCommandNode() *RepeatCommandNode {
return &RepeatCommandNode{}
}
// Parse はJava版 parse(Context) throws ParseException 相当。
func (n *RepeatCommandNode) Parse(ctx *Context) error {
if err := ctx.SkipToken("repeat"); err != nil {
return err
}
number, err := ctx.CurrentNumber()
if err != nil {
return err
}
n.number = number
// Java版もここではskipToken(number)ではなく素のnextToken()を呼ぶだけ
// (回数トークンの妥当性チェックはCurrentNumber()側で既に済んでいるため、
// 一致確認をせずにただ読み進める)。
ctx.NextToken()
commandListNode := NewCommandListNode()
if err := commandListNode.Parse(ctx); err != nil {
return err
}
n.commandListNode = commandListNode
return nil
}
// String はJava版 toString() 相当。
func (n *RepeatCommandNode) String() string {
return fmt.Sprintf("[repeat %d %s]", n.number, n.commandListNode)
}
package main
import "fmt"
// 実行: go run ./GoF/patterns/Interpreter/go
//
// Java版Main.javaはprogram.txtを1行ずつFileReaderで読み込んでparseするが、
// 「ファイルをどう読むか」の差でGo/TypeScript/Python 3言語間の実行結果が
// ぶれないよう、program.txtの5行をそのままスライスリテラルとして埋め込む
// (他言語版も同じ方針)。1行目から5行目まではJava版付属のprogram.txtと完全に同じ内容で、
// 実際にJava版(javac Main.java && java Main)を動かしてnode = ...の出力を照合済み。
// 6行目は「end後に余ったトークンは黙って読み捨てられる」というJava版の(バグではなく
// 仕様上の)挙動を確認するための追加ケース(これもJava版で同じ出力になることを確認済み)。
var programs = []string{
"program end",
"program go end",
"program go right go right go right go right end",
"program repeat 4 go right end end",
"program repeat 4 repeat 3 go right go left end right end end",
"program go end program go go go end",
}
// invalidPrograms はエラーハンドリングの確認用。Java版でParseExceptionが飛ぶのと
// 同じ入力を3種類(未定義コマンド/end不足/数値解析失敗)用意し、Go版でerrorとして
// どう返ってくるかを見る。
var invalidPrograms = []string{
"program foo end", // PrimitiveCommandNode: "foo is undefined" 相当
"program go", // CommandListNode: "missing 'end'" 相当
"program repeat foo go end end", // Context.CurrentNumber: NumberFormatException相当
}
func main() {
fmt.Println("-- valid programs (Java版と同じ構文木になることを確認済み) --")
for _, text := range programs {
runOne(text)
}
fmt.Println()
fmt.Println("-- invalid programs (error handling demo) --")
for _, text := range invalidPrograms {
runOne(text)
}
}
func runOne(text string) {
fmt.Printf("text = %q\n", text)
node := NewProgramNode()
if err := node.Parse(NewContext(text)); err != nil {
// Java版はここでParseExceptionがスタックトレース付きでMain.mainのtry/catchまで
// 一気に伝播し、readLoop自体を打ち切る。Go版はerrorを受け取ってそのまま
// 処理を続けられる。これがtry/catchと戻り値ベースのエラーハンドリングの
// 一番わかりやすい違い: Goでは「どこまで伝播させるか」を呼び出し側が
// (return errで上に投げるか、ここで処理するか)その都度選べる。
fmt.Println("error:", err)
return
}
fmt.Println("node =", node)
}
PHP
<?php
ini_set("display_errors", 1);
/**
* DSLを定義して複雑度を減らす
* DSL: ドメイン固有言語 domain-specific language
* 字句解析をして式と値に同じ「値」を返させ
* ブロックは式の集合、式は式の集合
* つまりComposite
*/
/**
* フリーワード検索の文法を簡易にしてSQLにして変換する仕組み
*/
class QueryParser
{
protected $args = array();
protected $query = null;
public function __construct($q)
{
$this->query = $q;
$this->parse();
}
public function setArgs($args)
{
$this->args = $args;
}
public function parse($query = null)
{
if (!$query) $query = $this->query;
$quote = "";
$buf = "";
$length = mb_strlen($query, "UTF8");
// 引数で渡された文字列の長さ分ループ
for ($i = 0; $i < $length; $i++) {
// i番目の文字
$char = mb_substr($query, $i, 1, "UTF8");
// $quoteに["] or [']が代入されているとき
if ($quote) {
// $charが["] or [']ならその文字を取り出す
// "hoge"なら 「hoge」を取り出す
if ($quote == $char) {
$this->args[] = $buf;
$quote = null;
$buf = null;
}
// 現在の文字がエスケープ文字の場合次の文字をbufに足す
elseif ($char == "\\") {
$buf .= mb_substr($query, ++$i, 1, "UTF8");
// 現在の文字をbufに足す
} else {
$buf .= $char;
}
}
// 現在の文字が["] or [']の時
elseif ($char == "'" || $char == '"') {
// これまでbufに追加した文字列をargsに代入
if ($buf) {
$this->args[] = $buf;
}
// クオートの開始判定変数用に代入
$quote = $char;
// $bufをクリア
$buf = null;
}
// 現在の文字が空白の場合
elseif ($char == "") {
if ($buf) {
// これまでにbufに追加した文字列を
// argsに代入してbufをクリア
$this->args[] = $buf;
$buf = null;
}
}
// 特殊文字ではない場合は普通にbufに現在の文字を追加
else {
$buf .= $char;
}
}
// ループを抜けたとき、これまでbufに追加された文字列を配列に追加
if ($buf) $this->args[] = $buf;
return $this->args;
}
public function getArgs()
{
return $this->args;
}
/**
* 分解した単語からSQLのクエリを生成
*/
public function createQuery($idx = 0)
{
$group = new QueryGroup();
for ($i = $idx; $i < count($this->args); $i++) {
// [(]の場合再帰的にcreateQueryを読んでQueryGroupを作る
if ($this->args[$i] == "(") {
list($obj, $i) = $this->createQuery($i + 1);
$group->set($obj);
$i++;
} elseif ($this->args[$i] == "&&") {
$group->set(new QueryAnd($this->args[$i]));
} elseif ($this->args[$i] == "||") {
$group->set(new QueryOr($this->args[$i]));
} elseif ($this->args[$i] == ")") {
break;
} else {
$group->set(new QueryOperand($this->args[$i]));
}
if ($group->isFull()) {
break;
}
if ($idx) {
return array($group, $i);
} else {
return $group;
}
}
}
}
interface IQuery
{
public function toQuery();
}
class QueryAnd implements IQuery
{
public function __construct($e)
{
$this->_Element = $e;
}
public function toQuery()
{
return "AND";
}
}
class QueryOr implements IQuery
{
public function __construct($e)
{
$this->_Element = $e;
}
public function toQuery()
{
return "OR";
}
}
/**
* [idx like "%hoge%"]のQueryを生成
*/
class QueryOperand implements IQuery
{
private $_Element;
public function __construct($e)
{
$this->_Element = $e;
}
public function toQuery()
{
return "idx LIKE %" . $this->escape() . "%";
}
public function escape()
{
return str_replace("'", "''", $this->_Element);
}
}
/**
* [A 演算子 B]のグループ
*
*/
class QueryGroup implements IQuery
{
protected $left = null;
protected $right = null;
protected $operator = null;
public function set($obj)
{
// 左、演算子、右の順で代入
if (!$this->left) {
$this->left = $obj;
} elseif (!$this->operator) {
$this->operator = $obj;
} elseif (!$this->right) {
$this->right = $obj;
}
}
public function isFull()
{
return $this->left && $this->operator && $this->right;
}
public function toQuery()
{
$result = "";
if ($this->left) {
$result .= $this->left->toQuery();
}
if ($this->operator) {
$result .= $this->operator->toQuery();
}
if ($this->right) {
$result .= $this->right->toQuery();
}
if ($result) {
return "( " . trim($result) . ")";
} else {
return "";
}
}
}
$obj = new QueryParser(
'("hoge" && "doya") || ("fuga" && "pi\\"yo")'
);
// 単語の分解
$args = $obj->getArgs();
var_dump($args);
// sql生成
print $obj->createQuery()->toQuery();TypeScript
Java版に一番近い移植。Nodeはinterface(共有実装が無いのでabstract classにする理由が無い)、ParseExceptionはErrorを継承したクラスで、parse()はJava同様throw/catchでエラーを伝える。Context.currentToken()はstring | nullを返し、Javaのnullチェックの形をそのまま持ち込める。currentNumber()はInteger.parseInt()相当の厳密さを正規表現(/^-?\d+$/)で担保している(素のNumber()はJavaのparseIntより緩いため)。
実行: npx tsx GoF/patterns/Interpreter/typescript/main.ts
// Node層 (Java版の Node.java 相当)
// 単体では実行不可。エントリポイントは main.ts (npx tsx main.ts)。
//
// Java版 Node は parse(Context) throws ParseException だけを持つ抽象クラスで、
// toString()はObjectから継承したものを各サブクラスがオーバーライドする。
// TypeScriptでは共有実装が何もない(Composite/ChainOfResponsibility版のような
// abstract classにする理由が無い)ため、interfaceで十分。
//
// 例外の伝え方はJava版そのまま: parse()はChain of throwで、失敗したら
// ParseExceptionをthrowする(Go版のようなerror戻り値ではない)。
import { Context } from "./context";
export interface Node {
parse(context: Context): void;
toString(): string;
}
// Context層 (Java版の Context.java 相当)
// 単体では実行不可。エントリポイントは main.ts (npx tsx main.ts)。
//
// Java版はjava.util.StringTokenizerで字句を1つずつ取り出すが、TypeScript標準には
// 直接の相当品が無い。文字列全体をあらかじめ空白区切りでトークン配列に分割しておき、
// 添字posを1つずつ進める形にする。「現在のトークンを覗いて(currentToken)、
// 1つ読み進める(nextToken)」という操作性はJava版と変わらない。
//
// Java版 currentToken() はトークン切れでnullを返す。TypeScript版もそれに合わせて
// string | null を返す(Go版のように(string, bool)の2値返しにはしない。Java版の
// nullチェックの形をそのまま持ち込める言語なので、ここは素直にJavaへ寄せた)。
import { ParseException } from "./parse_exception";
export class Context {
private readonly tokens: string[];
private pos = 0;
constructor(text: string) {
this.tokens = text.split(/\s+/).filter((t) => t.length > 0);
}
currentToken(): string | null {
return this.pos < this.tokens.length ? this.tokens[this.pos] : null;
}
nextToken(): string | null {
this.pos += 1;
return this.currentToken();
}
// Java版 skipToken(String) throws ParseException 相当。
// currentがnullのときは`${current}`が"null"という文字列に評価されるので、
// Java版のエラーメッセージ(nullを文字列連結したもの)とほぼ同じ見た目になる。
skipToken(token: string): void {
const current = this.currentToken();
if (current !== token) {
throw new ParseException(`Warning: ${token} is expected, but ${current} is found.`);
}
this.nextToken();
}
// Java版 currentNumber() throws ParseException 相当。
// Integer.parseInt()相当の厳密さ(符号+数字列のみ)を正規表現で担保する。
// (Number("4.5")やNumber("")のような、JSではNaNにならない値を誤って通さないため)
currentNumber(): number {
const current = this.currentToken();
if (current === null || !/^-?\d+$/.test(current)) {
throw new ParseException(`Warning: NumberFormatException: For input string: "${current}"`);
}
return Number(current);
}
}
// ParseException層 (Java版の ParseException.java 相当)
// 単体では実行不可。エントリポイントは main.ts (npx tsx main.ts)。
//
// Java版はcheckedな独自例外(Exceptionのサブクラス)。TypeScriptには例外の
// checked/uncheckedの区別が無いので、標準のErrorを継承するだけで十分。
export class ParseException extends Error {
constructor(message: string) {
super(message);
this.name = "ParseException";
}
}
// ProgramNode層 (Java版の ProgramNode.java 相当)
// 単体では実行不可。エントリポイントは main.ts (npx tsx main.ts)。
//
// <program> ::= program <command list>
import { CommandListNode } from "./command_list_node";
import { Context } from "./context";
import { Node } from "./node";
export class ProgramNode implements Node {
private commandListNode!: Node;
parse(context: Context): void {
context.skipToken("program");
this.commandListNode = new CommandListNode();
this.commandListNode.parse(context);
}
toString(): string {
return `[program ${this.commandListNode}]`;
}
}
// CommandListNode層 (Java版の CommandListNode.java 相当)
// 単体では実行不可。エントリポイントは main.ts (npx tsx main.ts)。
//
// <command list> ::= <command>* end
import { CommandNode } from "./command_node";
import { Context } from "./context";
import { Node } from "./node";
import { ParseException } from "./parse_exception";
export class CommandListNode implements Node {
private readonly list: Node[] = [];
parse(context: Context): void {
for (;;) {
const token = context.currentToken();
if (token === null) {
throw new ParseException("Missing 'end'");
} else if (token === "end") {
context.skipToken("end");
return;
} else {
const commandNode = new CommandNode();
commandNode.parse(context);
this.list.push(commandNode);
}
}
}
// Java版 list.toString() (java.util.ArrayListの既定のtoString)相当。"[e1, e2, e3]"形式。
toString(): string {
return `[${this.list.map((n) => n.toString()).join(", ")}]`;
}
}
// CommandNode層 (Java版の CommandNode.java 相当)
// 単体では実行不可。エントリポイントは main.ts (npx tsx main.ts)。
//
// <command> ::= <repeat command> | <primitive command>
import { Context } from "./context";
import { Node } from "./node";
import { PrimitiveCommandNode } from "./primitive_command_node";
import { RepeatCommandNode } from "./repeat_command_node";
export class CommandNode implements Node {
private node!: Node;
parse(context: Context): void {
this.node = context.currentToken() === "repeat" ? new RepeatCommandNode() : new PrimitiveCommandNode();
this.node.parse(context);
}
// Java版同様、内側のNodeへそのまま委譲する(自分では括弧を付けない)。
toString(): string {
return this.node.toString();
}
}
// PrimitiveCommandNode層 (Java版の PrimitiveCommandNode.java 相当)
// 単体では実行不可。エントリポイントは main.ts (npx tsx main.ts)。
//
// <primitive command> ::= go | right | left
import { Context } from "./context";
import { Node } from "./node";
import { ParseException } from "./parse_exception";
const VALID_NAMES = new Set(["go", "right", "left"]);
export class PrimitiveCommandNode implements Node {
private name = "";
// Java版は context.currentToken() がnullでも構わず name.equals(...) を呼び出して
// しまうため、文法上リーチしない経路とはいえNullPointerExceptionの芽がある
// (CommandListNode/CommandNodeの構造上、実際にはnullで呼ばれることはない)。
// TS版はその芽を先に断ち切り、トークン切れなら例外を投げる。
parse(context: Context): void {
const token = context.currentToken();
if (token === null) {
throw new ParseException("Warning: go, right or left is expected, but null is found.");
}
this.name = token;
context.skipToken(this.name);
if (!VALID_NAMES.has(this.name)) {
throw new ParseException(`${this.name} is undefined`);
}
}
toString(): string {
return this.name;
}
}
// RepeatCommandNode層 (Java版の RepeatCommandNode.java 相当)
// 単体では実行不可。エントリポイントは main.ts (npx tsx main.ts)。
//
// <repeat command> ::= repeat <number> <command list>
import { CommandListNode } from "./command_list_node";
import { Context } from "./context";
import { Node } from "./node";
export class RepeatCommandNode implements Node {
private number = 0;
private commandListNode!: Node;
parse(context: Context): void {
context.skipToken("repeat");
this.number = context.currentNumber();
// Java版もここではskipToken(number)ではなく素のnextToken()を呼ぶだけ
// (回数トークンの妥当性チェックはcurrentNumber()側で既に済んでいるため、
// 一致確認をせずにただ読み進める)。
context.nextToken();
this.commandListNode = new CommandListNode();
this.commandListNode.parse(context);
}
toString(): string {
return `[repeat ${this.number} ${this.commandListNode}]`;
}
}
// Interpreterパターン: ミニ言語(go/right/left/repeat)のパーサ (Java版Main.javaと同じお題)
//
// 実行: npx tsx GoF/patterns/Interpreter/typescript/main.ts
//
// Java版Main.javaはprogram.txtを1行ずつBufferedReaderで読み込んでparseするが、
// 「ファイルをどう読むか」の差でGo/TypeScript/Python 3言語間の実行結果がぶれないよう、
// program.txtの5行をそのまま配列リテラルとして埋め込む(他言語版も同じ方針)。
// 1〜5行目はJava版付属のprogram.txtと完全に同じ内容で、実際にJava版
// (javac Main.java && java Main)を動かしてnode = ...の出力を照合済み。6行目は
// 「end後に余ったトークンは黙って読み捨てられる」というJava版の(バグではなく仕様上の)
// 挙動を確認するための追加ケース(これもJava版で同じ出力になることを確認済み)。
import { Context } from "./context";
import { ParseException } from "./parse_exception";
import { ProgramNode } from "./program_node";
const programs = [
"program end",
"program go end",
"program go right go right go right go right end",
"program repeat 4 go right end end",
"program repeat 4 repeat 3 go right go left end right end end",
"program go end program go go go end",
];
// invalidPrograms はエラーハンドリングの確認用。Java版でParseExceptionが飛ぶのと
// 同じ入力を3種類(未定義コマンド/end不足/数値解析失敗)用意し、TS版で例外として
// どう捕まえられるかを見る。
const invalidPrograms = [
"program foo end", // PrimitiveCommandNode: "foo is undefined" 相当
"program go", // CommandListNode: "Missing 'end'" 相当
"program repeat foo go end end", // Context.currentNumber: NumberFormatException相当
];
function runOne(text: string): void {
console.log(`text = "${text}"`);
const node = new ProgramNode();
try {
node.parse(new Context(text));
console.log(`node = ${node}`);
} catch (e) {
// Java版はここでParseExceptionがスタックトレース付きでMain.mainのtry/catchまで
// 一気に伝播し、readLoop自体を打ち切る。TS版もJava版と同じくthrow/catchで
// 表現しているので一番構造が近いが、ここでは1行ごとにcatchして次へ進めることで、
// 複数の壊れた入力を1回の実行でまとめて確認できるようにしている
// (Javaのtry/catchのスコープをMain全体ではなく1行単位に絞っただけで、
// 例外機構そのものの使い方はJava版と変わらない)。
if (e instanceof ParseException) {
console.log(`error: ${e.message}`);
} else {
throw e;
}
}
}
function main(): void {
console.log("-- valid programs (Java版と同じ構文木になることを確認済み) --");
for (const text of programs) {
runOne(text);
}
console.log("");
console.log("-- invalid programs (error handling demo) --");
for (const text of invalidPrograms) {
runOne(text);
}
}
main();
Python
Node(ABC)にparse()だけを抽象メソッドとして持たせ、ParseException(Exception)をraiseで伝播させるJava版そのままの構造。唯一の言語固有事情として、文法がCommandList → Command → RepeatCommand → CommandListと相互再帰するため、repeat_command_node.pyがトップレベルでcommand_list_nodeをimportすると循環importでエラーになる(Javaはコンパイル時にクラス群を一括解決するため、Goは同一パッケージ内で複数ファイルにまたがるimportが不要なため、どちらもこの問題自体が存在しない)。対処としてRepeatCommandNode.parse()内でのみCommandListNodeを遅延importしている。
実行: python3 GoF/patterns/Interpreter/python/main.py
"""Node層 (Java版の Node.java 相当)
単体では実行不可。エントリポイントは main.py (python3 main.py)。
Java版 Node は parse(Context) throws ParseException だけを持つ抽象クラスで、
toString()はObjectから継承したものを各サブクラスがオーバーライドする。
Composite版Python実装と同じ方針で抽象基底クラス(ABC)を使い、parse()を抽象メソッドに
する。__str__()はPythonのobjectが既定で持つが、意味のある表現を返すよう各サブクラスで
オーバーライドする(Java版のtoString()オーバーライドに相当)。
例外の伝え方はJava版そのまま: parse()はchain of raiseで、失敗したらParseExceptionを
raiseする(Go版のようなerror戻り値ではない)。
"""
from __future__ import annotations
from abc import ABC, abstractmethod
from context import Context
class Node(ABC):
@abstractmethod
def parse(self, context: Context) -> None: ...
"""Context層 (Java版の Context.java 相当)
単体では実行不可。エントリポイントは main.py (python3 main.py)。
Java版はjava.util.StringTokenizerで字句を1つずつ取り出すが、Python標準には直接の
相当品が無い。文字列全体をあらかじめ str.split() (引数なし=連続空白をまとめて区切る、
StringTokenizerのデフォルト区切り文字と同じ挙動)でトークン列へ分割しておき、添字_pos
を1つずつ進める形にする。「現在のトークンを覗いて(current_token)、1つ読み進める
(next_token)」という操作性はJava版と変わらない。
Java版 currentToken() はトークン切れでnullを返す。Python版もそれに合わせて
str | None を返す(Go版のように(str, bool)の2値返しにはしない。Java版のnullチェックの
形をそのまま持ち込める言語なので、ここは素直にJavaへ寄せた)。
"""
from __future__ import annotations
import re
from parse_exception import ParseException
_INTEGER_RE = re.compile(r"^-?\d+$")
class Context:
def __init__(self, text: str) -> None:
self._tokens = text.split()
self._pos = 0
def current_token(self) -> str | None:
if self._pos < len(self._tokens):
return self._tokens[self._pos]
return None
def next_token(self) -> str | None:
self._pos += 1
return self.current_token()
def skip_token(self, token: str) -> None:
"""Java版 skipToken(String) throws ParseException 相当。"""
current = self.current_token()
if current != token:
raise ParseException(f"Warning: {token} is expected, but {current} is found.")
self.next_token()
def current_number(self) -> int:
"""Java版 currentNumber() throws ParseException 相当。
Integer.parseInt()相当の厳密さ(符号+数字列のみ)を正規表現で担保する
(int("4.5")やint(" ")のような、Pythonの緩い変換規則を誤って通さないため)。
"""
current = self.current_token()
if current is None or not _INTEGER_RE.match(current):
raise ParseException(f'Warning: NumberFormatException: For input string: "{current}"')
return int(current)
"""ParseException層 (Java版の ParseException.java 相当)
単体では実行不可。エントリポイントは main.py (python3 main.py)。
Java版はcheckedな独自例外(Exceptionのサブクラス)。Pythonには例外のchecked/uncheckedの
区別が無いので、標準のExceptionを継承するだけで十分(FactoryMethod版等、他パターンの
Python実装と同じ流儀)。
"""
from __future__ import annotations
class ParseException(Exception):
"""Java版 ParseException 相当。"""
"""ProgramNode層 (Java版の ProgramNode.java 相当)
単体では実行不可。エントリポイントは main.py (python3 main.py)。
<program> ::= program <command list>
"""
from __future__ import annotations
from command_list_node import CommandListNode
from context import Context
from node import Node
class ProgramNode(Node):
def __init__(self) -> None:
self._command_list_node: Node | None = None
def parse(self, context: Context) -> None:
context.skip_token("program")
self._command_list_node = CommandListNode()
self._command_list_node.parse(context)
def __str__(self) -> str:
return f"[program {self._command_list_node}]"
"""CommandListNode層 (Java版の CommandListNode.java 相当)
単体では実行不可。エントリポイントは main.py (python3 main.py)。
<command list> ::= <command>* end
"""
from __future__ import annotations
from command_node import CommandNode
from context import Context
from node import Node
from parse_exception import ParseException
class CommandListNode(Node):
def __init__(self) -> None:
self._list: list[Node] = []
def parse(self, context: Context) -> None:
while True:
token = context.current_token()
if token is None:
raise ParseException("Missing 'end'")
elif token == "end":
context.skip_token("end")
return
else:
command_node = CommandNode()
command_node.parse(context)
self._list.append(command_node)
def __str__(self) -> str:
"""Java版 list.toString() (java.util.ArrayListの既定のtoString)相当。
"[e1, e2, e3]" 形式(Pythonのlist.__repr__は要素をrepr()するので流用できず、
ここは手で組み立てる)。
"""
return "[" + ", ".join(str(n) for n in self._list) + "]"
"""CommandNode層 (Java版の CommandNode.java 相当)
単体では実行不可。エントリポイントは main.py (python3 main.py)。
<command> ::= <repeat command> | <primitive command>
"""
from __future__ import annotations
from context import Context
from node import Node
from primitive_command_node import PrimitiveCommandNode
from repeat_command_node import RepeatCommandNode
class CommandNode(Node):
def __init__(self) -> None:
self._node: Node | None = None
def parse(self, context: Context) -> None:
if context.current_token() == "repeat":
self._node = RepeatCommandNode()
else:
self._node = PrimitiveCommandNode()
self._node.parse(context)
def __str__(self) -> str:
"""Java版同様、内側のNodeへそのまま委譲する(自分では括弧を付けない)。"""
return str(self._node)
"""PrimitiveCommandNode層 (Java版の PrimitiveCommandNode.java 相当)
単体では実行不可。エントリポイントは main.py (python3 main.py)。
<primitive command> ::= go | right | left
"""
from __future__ import annotations
from context import Context
from node import Node
from parse_exception import ParseException
_VALID_NAMES = {"go", "right", "left"}
class PrimitiveCommandNode(Node):
def __init__(self) -> None:
self._name = ""
def parse(self, context: Context) -> None:
"""Java版は context.currentToken() がNoneでも構わず name.equals(...) を呼び出して
しまうため、文法上リーチしない経路とはいえNullPointerExceptionの芽がある
(CommandListNode/CommandNodeの構造上、実際にはNoneで呼ばれることはない)。
Python版ではその芽を先に断ち切り、トークン切れなら例外を送出する。
"""
token = context.current_token()
if token is None:
raise ParseException("Warning: go, right or left is expected, but None is found.")
self._name = token
context.skip_token(self._name)
if self._name not in _VALID_NAMES:
raise ParseException(f"{self._name} is undefined")
def __str__(self) -> str:
return self._name
"""RepeatCommandNode層 (Java版の RepeatCommandNode.java 相当)
単体では実行不可。エントリポイントは main.py (python3 main.py)。
<repeat command> ::= repeat <number> <command list>
"""
from __future__ import annotations
from context import Context
from node import Node
class RepeatCommandNode(Node):
def __init__(self) -> None:
self._number = 0
self._command_list_node: Node | None = None
def parse(self, context: Context) -> None:
# command_list_node は関数内でimportする(トップレベルでは循環import: this module
# -> command_list_node -> command_node -> this module 、になってしまうため)。
# Java版はコンパイル時にクラス群を一括解決するのでこの問題自体が存在せず、Go版も
# 同一パッケージ内でファイル間import不要なので同様に存在しない。文法が
# CommandList -> Command -> RepeatCommand -> CommandList と相互再帰する構造上、
# Pythonのモジュール単位import(実行時に上から順に評価される)だとどこかの矢印を
# 遅延させる必要がある、というPython特有の事情。
from command_list_node import CommandListNode
context.skip_token("repeat")
self._number = context.current_number()
# Java版もここではskip_token(number)ではなく素のnext_token()を呼ぶだけ
# (回数トークンの妥当性チェックはcurrent_number()側で既に済んでいるため、
# 一致確認をせずにただ読み進める)。
context.next_token()
self._command_list_node = CommandListNode()
self._command_list_node.parse(context)
def __str__(self) -> str:
return f"[repeat {self._number} {self._command_list_node}]"
"""Interpreterパターン: ミニ言語(go/right/left/repeat)のパーサ (Java版Main.javaと同じお題)
実行: python3 main.py
(もしくはリポジトリルートから python3 GoF/patterns/Interpreter/python/main.py)
Java版Main.javaはprogram.txtを1行ずつBufferedReaderで読み込んでparseするが、
「ファイルをどう読むか」の差でGo/TypeScript/Python 3言語間の実行結果がぶれないよう、
program.txtの5行をそのままリストリテラルとして埋め込む(他言語版も同じ方針)。
1〜5行目はJava版付属のprogram.txtと完全に同じ内容で、実際にJava版
(javac Main.java && java Main)を動かしてnode = ...の出力を照合済み。6行目は
「end後に余ったトークンは黙って読み捨てられる」というJava版の(バグではなく仕様上の)
挙動を確認するための追加ケース(これもJava版で同じ出力になることを確認済み)。
"""
from __future__ import annotations
from context import Context
from parse_exception import ParseException
from program_node import ProgramNode
PROGRAMS = [
"program end",
"program go end",
"program go right go right go right go right end",
"program repeat 4 go right end end",
"program repeat 4 repeat 3 go right go left end right end end",
"program go end program go go go end",
]
# INVALID_PROGRAMS はエラーハンドリングの確認用。Java版でParseExceptionが飛ぶのと
# 同じ入力を3種類(未定義コマンド/end不足/数値解析失敗)用意し、Python版で例外として
# どう捕まえられるかを見る。
INVALID_PROGRAMS = [
"program foo end", # PrimitiveCommandNode: "foo is undefined" 相当
"program go", # CommandListNode: "Missing 'end'" 相当
"program repeat foo go end end", # Context.current_number: NumberFormatException相当
]
def run_one(text: str) -> None:
print(f'text = "{text}"')
node = ProgramNode()
try:
node.parse(Context(text))
print(f"node = {node}")
except ParseException as e:
# Java版はここでParseExceptionがスタックトレース付きでMain.mainのtry/exceptまで
# 一気に伝播し、readLoop自体を打ち切る。Python版もJava版と同じくraise/exceptで
# 表現しているので一番構造が近いが、ここでは1行ごとにexceptして次へ進めることで、
# 複数の壊れた入力を1回の実行でまとめて確認できるようにしている
# (Javaのtry/catchのスコープをMain全体ではなく1行単位に絞っただけで、
# 例外機構そのものの使い方はJava版と変わらない)。
print(f"error: {e}")
def main() -> None:
print("-- valid programs (Java版と同じ構文木になることを確認済み) --")
for text in PROGRAMS:
run_one(text)
print()
print("-- invalid programs (error handling demo) --")
for text in INVALID_PROGRAMS:
run_one(text)
if __name__ == "__main__":
main()