例外による処理分岐をヘルパーオブジェクトに変換する

AquaSKK のローマ字かな変換は本家 SKK と同じく trie(prefix tree) を利用しています。基本的なアルゴリズムは TSM 版と一緒ですが、IMK 版になってクラスのインタフェースが大きく変化しました。

TSM 版

例外を利用して処理を分岐しています。

CppCFString KanaTreeController::trace(int output_type,CppCFString& queue) {
    CppCFString result;
    while (queue.length() > 0) {
	try {
	    root.trace(queue[0]);
	}
	catch (ChildNotFoundException& e) {
	    // rootにqueue[0]の要素が存在しない場合はqueueそのものを返す。この時queueは空にする。
	    result += queue;
	    queue.clear();
	    break;
	}
	
	try {
	    // 再帰検索でノードを探す
	    KanaTreeElement& leaf = trace(queue,root);
	    
	    switch (output_type) {
	    case OUTPUT_HIRAGANA:
		result += leaf.getContentAsHiragana();
		break;
		
	    case OUTPUT_ZENKATA:
		result += leaf.getContentAsZenKata();
		break;
		
	    case OUTPUT_HANKATA:
		result += leaf.getContentAsHanKata();
		break;
	    }
	}
	catch (ReachedEndOfQueueException& e) {
	    // ループを抜ける
	    break;
	}
    }
    return result;
}

ノード検索インタフェースはこちら。

KanaTreeElement& KanaTreeController::trace(CppCFString& queue,KanaTreeElement& parent,unsigned level) {
    try {
	KanaTreeElement& child = parent.trace(queue[level]);
	
	// 例外が起きなかった
	if (!child.hasAnyChilds()) {
	    // これ以上辿れない。
	    queue.eraseFirst(level + 1);
	    if (child.getNextState() != 0x00) {
		queue.append(child.getNextState());
	    }
	    return child;
	}
	else {
	    if (level >= queue.length() - 1) {
		// これ以上辿れるにも関わらずキューの終わりに来てしまった。
		throw ReachedEndOfQueueException();
	    }
	    else {
		// 再帰的にトレースを続ける。
		return trace(queue,child,level + 1);
	    }
	}
    }
    catch (ChildNotFoundException& e) {
	// これ以上辿れない。
	queue.eraseFirst(level);
	if (parent.getNextState() != 0x00) {
	    queue.append(parent.getNextState());
	}
	return parent;
    }
}

スタイルの問題ですが、引数の文字列を変更しているのが気になるところです。

初期の IMK 版

例外を使わずにやろうと思って作ったバージョンです。

bool SKKRomanKanaConverter::Execute(SKKInputMode mode, const std::string& in, std::string& out, std::string& next) {
    bool converted = false;
    std::string str(in);

    out.clear();
    next.clear();

    while(!str.empty()) {
	int state;
	const SKKTrie* node = root_.Traverse(str, state);

	// 変換できた?
	if(node) {
	    out += node->KanaString(mode);
	    next = node->NextState();
	    converted = true;
	} else {
	    converted = false;
	}

	// 部分的に一致しているがデータ不足のためこれ以上処理できない
	if(!state) {
	    next = str;
	    return false;
	}

	// 最初の一文字が木に存在しない場合、出力にコピーして次の文字を調べる
	if(state < 0) {
	    out += str[0];
	    state = 1;
	}
	
	// 調べた部分を削り取って次の文字を調べる
	str = str.substr(state);
    }

    return converted;
}

ノード検索部分は以下のようになっています。

const SKKTrie* SKKTrie::Traverse(const std::string& str, int& state, unsigned depth) {
    // [1] データ不足(ex. "k" や "ch" など)
    if(depth == str.size()) {
	state = 0;
	return 0;
    }

    // 一致?
    if(children_.find(str[depth]) != children_.end()) {
	SKKTrie* node = &children_[str[depth]];

	// 末端でないなら再帰検索
	if(!node->children_.empty()) {
	    return node->Traverse(str, state, depth + 1);
	}

	// [2] 完全一致
	state = depth + 1;
	return node;
    }

    // [3] 部分一致(ex. "kb" や "chm" など)
    if(0 < depth) {
	state = depth;

	// 節かつ葉でもある「n」のような場合には、一致として扱う
	if(leaf_) {
	    return this;
	}

	return 0;
    }

    // [4] 最初の一文字が木に存在しない
    state = -1;
    return 0;
}

例外がなくなったかわりに謎の引数が増え、戻り値の解釈も複雑になってしまいました。これではとても改善とは言えませんねぇ。

現在の IMK 版

悩んだ末に辿り着いたのが、ヘルパーオブジェクトを導入したバージョンです。

bool SKKRomanKanaConverter::Convert(SKKInputMode mode, const std::string& str, SKKRomanKanaConversionResult& result) {
    bool converted = false;
    std::string queue(str);

    result = SKKRomanKanaConversionResult();

    while(!queue.empty()) {
        ConversionHelper helper(mode, queue);

        root_.Traverse(helper);

        result.output += helper.Output();
        result.next = helper.Next();
        result.intermediate = helper.Intermediate();

        converted = helper.IsConverted();

        if(helper.IsShort()) {
            result.next = helper.Queue();
            break;
        }

        queue = helper.Queue();
    }

    return converted;
}

検索部分はこちら。

void SKKTrie::Traverse(SKKTrieHelper& helper, unsigned depth) {
    const std::string& str = helper.SKKTrieRomanString();

    // [1] データ不足(ex. "k" や "ch" など)
    if(depth == str.size()) {
        if(IsLeaf()) {
            helper.SKKTrieNotifyIntermediate(this);
        }

	return helper.SKKTrieNotifyShort();
    }

    // 一致?
    if(children_.find(str[depth]) != children_.end()) {
	SKKTrie* node = &children_[str[depth]];

	// 末端でないなら再帰検索
	if(!node->children_.empty()) {
	    return node->Traverse(helper, depth + 1);
	}

	// [2] 完全一致
        helper.SKKTrieNotifyConverted(node);
        helper.SKKTrieNotifySkipLength(depth + 1);

        return;
    }

    // [3] 部分一致(ex. "kb" や "chm" など)
    if(0 < depth) {
        if(IsLeaf()) {
            helper.SKKTrieNotifyConverted(this);
        }
        helper.SKKTrieNotifySkipLength(depth);

        return;
    }

    // [4] 最初の一文字が木に存在しない
    helper.SKKTrieNotifyNotConverted(str[0]);
    helper.SKKTrieNotifySkipLength(1);
}

ConversionHelper は SKKRomanKanaConverter.cpp の内部で定義されています。

   class ConversionHelper : public SKKTrieHelper {
        bool converted_;
        bool short_;
        SKKInputMode mode_;
        std::string queue_;
        std::string output_;
        std::string next_;
        std::string intermediate_;

        virtual const std::string& SKKTrieRomanString() const {
            return queue_;
        }

        virtual void SKKTrieNotifyConverted(const SKKTrie* node) {
            converted_ = true;
            output_ += node->KanaString(mode_);
            next_ = node->NextState();
            intermediate_.clear();
        }

        virtual void SKKTrieNotifyIntermediate(const SKKTrie* node) {
            intermediate_ = node->KanaString(mode_);
        }

        virtual void SKKTrieNotifyNotConverted(char code) {
            converted_ = false;
            output_ += code;
        }

        virtual void SKKTrieNotifySkipLength(int length) {
            queue_ = queue_.substr(length);
        }

        virtual void SKKTrieNotifyShort() {
            short_ = true;
        }

    public:
        ConversionHelper(SKKInputMode mode, const std::string& queue)
            : converted_(false), short_(false), mode_(mode), queue_(queue) {}

        const std::string& Output() const {
            return output_;
        }

        const std::string& Next() const {
            return next_;
        }

        const std::string& Intermediate() const {
            return intermediate_;
        }

        const std::string& Queue() const {
            return queue_;
        }

        bool IsConverted() const {
            return converted_;
        }

        bool IsShort() const {
            return short_;
        }
    };

やあやあやあ。随分とコードが増えてしまいました。これは改善と言えるのかな。ううむ。微妙ですね。しかし、プロトコルが明確になったおかげでコードは鬱陶しいくらい説明的になりました。

考察

TSM 版の例外による処理分岐はコンパクトでわかりやすいのですが、ポチポチと入力するたびに例外が飛ぶというトレードオフは、どうにも受け入れにくいものがあります。

処理の分岐に例外を使いたいとしたら、おそらくそこには手強い複雑さが隠れています。その複雑さを取り除くことが不可能ならせめて、オブジェクト同士のインタラクションに変換してしまおう、というのが今回のヘルパーオブジェクトの考え方です。

長いことこういう発想ができなくて苦しかったのですが、最近になってすっと出てくるようになってきた気がします。