非専門的シンギュラリティー研究所

無限に動き続けるシステムを表す方法を AI なども使って考えていきます。

C++ の調査(5)

前回のプログラムは行の構文解析の部分が想定と違ったので、やはり少し改造は難しいと感じました。そこで構文解析の部分の別のやり方を ChatGPT で作ってもらうことにしました。最後は想定と違うものになりましたが(これはこれで使えそうです)、必要な部分を組み合わせることはできそうです。最近の C++ はよくわからないので、慣れるまでは ChatGPT でやった方が早くできそうです。

C++ で文字列が条件を満たすかどうかを調べる関数を作ってください。関数の戻り値は
&& (条件を満たす文字列が連続している)
|| (どちらかの条件を満たす)
が使えるようにしてください。

と入力すると以下の結果となりました。

C++で、文字列が特定の条件を満たすかどうかを判定する関数を作成し、
その関数の戻り値に対して `&&` や `||` を使えるようにするには、
カスタム型に論理演算子オーバーロードするのがよい方法です。

以下に、条件を満たす文字列の検出・連結・論理演算ができるサンプルを示します。

🔧 サンプルコード

#include <iostream>
#include <string>
#include <vector>

class ConditionResult {
public:
    bool value;
    std::vector<std::string> matches;

    ConditionResult(bool v = false) : value(v) {}

    // 論理積 (AND)
    ConditionResult operator&&(const ConditionResult& other) const {
        ConditionResult result(value && other.value);
        if (result.value) {
            result.matches = matches;
            result.matches.insert(result.matches.end(), other.matches.begin(), other.matches.end());
        }
        return result;
    }

    // 論理和 (OR)
    ConditionResult operator||(const ConditionResult& other) const {
        ConditionResult result(value || other.value);
        if (result.value) {
            result.matches = value ? matches : other.matches;
            if (value && other.value) {
                result.matches.insert(result.matches.end(), other.matches.begin(), other.matches.end());
            }
        }
        return result;
    }

    // デバッグ用
    void print() const {
        std::cout << "Result: " << (value ? "true" : "false") << "\nMatches:\n";
        for (const auto& m : matches) {
            std::cout << "  " << m << "\n";
        }
    }
};

// 条件:文字列に特定の単語が含まれているか
ConditionResult containsWord(const std::string& input, const std::string& word) {
    if (input.find(word) != std::string::npos) {
        return ConditionResult{true, {word}};
    }
    return ConditionResult{false};
}

int main() {
    std::string text = "apple banana cherry";

    auto result = containsWord(text, "banana") && containsWord(text, "apple");
    result.print();

    auto result2 = containsWord(text, "banana") || containsWord(text, "orange");
    result2.print();

    return 0;
}

🔍 出力例

Result: true
Matches:
  banana
  apple

Result: true
Matches:
  banana

✅ ポイント

  • `ConditionResult` に `operator&&` と `operator||` を定義することで、関数の戻り値に論理演算子が使えるようになります。
  • マッチした文字列も保持することで、後からどの条件に一致したのか確認できます。
  • C++ではビルトイン型に対しては短絡評価(lazy evaluation)ですが、自作クラスでの `&&`, `||` は常に両方評価される点に注意してください。

必要に応じて、部分一致・正規表現などの条件を拡張できます。
どういう条件をチェックしたいですか?それに合わせて最適化もできますよ。

&& が連続するとき、タプルのような構造としても返されるようにしてください

と入力すると以下の結果となりました。

了解です!

`&&` が連続して使われるときに、条件をすべて満たす場合は、それぞれの結果をまとめた「タプルのような構造」で返したい、ということですね。
つまり:

auto result = cond1 && cond2 && cond3;

となったときに、`result` には `cond1`〜`cond3` の情報がすべて入っている構造になっていてほしい、ということ。

🧠 方針

  • 条件ごとの情報を `ConditionResult` というクラスで管理。
  • `&&` のオペレータで新たな `ConditionResult` を返すとき、これまでのマッチ情報を蓄積
  • 「タプルのような構造」としては、`std::vector` や `std::vector`、あるいは `std::tuple` も考えられますが、拡張性を持たせて `std::vector` にします(カスタム構造にもできます)。

✅ 改良版サンプル

#include <iostream>
#include <string>
#include <vector>

class ConditionResult {
public:
    bool value;
    std::vector<std::string> matches;

    ConditionResult(bool v = false) : value(v) {}

    ConditionResult(bool v, const std::vector<std::string>& m) : value(v), matches(m) {}

    // 論理積 (AND)
    ConditionResult operator&&(const ConditionResult& other) const {
        ConditionResult result(value && other.value);
        if (result.value) {
            result.matches.reserve(matches.size() + other.matches.size());
            result.matches.insert(result.matches.end(), matches.begin(), matches.end());
            result.matches.insert(result.matches.end(), other.matches.begin(), other.matches.end());
        }
        return result;
    }

    // 論理和 (OR)
    ConditionResult operator||(const ConditionResult& other) const {
        ConditionResult result(value || other.value);
        if (value && other.value) {
            result.matches = matches;
            result.matches.insert(result.matches.end(), other.matches.begin(), other.matches.end());
        } else if (value) {
            result.matches = matches;
        } else if (other.value) {
            result.matches = other.matches;
        }
        return result;
    }

    // 出力
    void print() const {
        std::cout << "Result: " << (value ? "true" : "false") << "\nMatches:\n";
        for (const auto& m : matches) {
            std::cout << "  " << m << "\n";
        }
    }
};

// 条件:指定した単語が含まれているか
ConditionResult containsWord(const std::string& input, const std::string& word) {
    if (input.find(word) != std::string::npos) {
        return ConditionResult{true, {word}};
    }
    return ConditionResult{false};
}

int main() {
    std::string text = "apple banana cherry";

    // AND条件の連鎖(タプル的に記録される)
    auto result = containsWord(text, "apple") && containsWord(text, "banana") && containsWord(text, "cherry");
    result.print();

    // OR条件(どれか一つでも)
    auto result2 = containsWord(text, "grape") || containsWord(text, "banana");
    result2.print();

    return 0;
}

🧾 出力例

Result: true
Matches:
  apple
  banana
  cherry

Result: true
Matches:
  banana

🧩 拡張ヒント

もし `matches` に単なる文字列ではなく、どの条件だったのかも記録したければ、`std::vector>` にして「条件名と一致内容」をセットで記録するのもいいアイデアです。

必要であれば、`matches` を実際に `std::tuple` にして、型安全なタプルっぽい構造にもできますよ。
どういう情報を「タプル」として保持したいですか?条件名、値、位置など?

再帰的に構造の中に構造を含むことができるようにして、構文解析に使えるようにしてください

と入力すると以下の結果となりました。

了解です!
再帰的に構造を含む条件評価システムC++で作って、構文解析にも使えるようにしたいということですね。

🔧 方針:構文解析向け 再帰的な条件構造

🧱 基本設計

1. `ConditionNode` という抽象クラスを作成。
2. AND / OR / 単一条件 それぞれを継承クラスで表現。
3. `evaluate()` 関数で再帰的に評価。
4. `matches` の情報も階層的に持たせる。

✅ 実装例(再帰的構造)

#include <iostream>
#include <memory>
#include <string>
#include <vector>

// 評価結果構造
struct EvalResult {
    bool value;
    std::vector<std::string> matches;

    void print(int indent = 0) const {
        std::string pad(indent, ' ');
        std::cout << pad << "Result: " << (value ? "true" : "false") << "\n";
        std::cout << pad << "Matches:\n";
        for (const auto& m : matches) {
            std::cout << pad << "  " << m << "\n";
        }
    }
};

// 抽象クラス
class ConditionNode {
public:
    virtual ~ConditionNode() {}
    virtual EvalResult evaluate(const std::string& input) const = 0;
};

// 単一条件ノード
class WordCondition : public ConditionNode {
    std::string word;

public:
    WordCondition(const std::string& w) : word(w) {}

    EvalResult evaluate(const std::string& input) const override {
        if (input.find(word) != std::string::npos) {
            return { true, { word } };
        }
        return { false, {} };
    }
};

// ANDノード(すべての子が成立)
class AndCondition : public ConditionNode {
    std::vector<std::shared_ptr<ConditionNode>> children;

public:
    void add(const std::shared_ptr<ConditionNode>& cond) {
        children.push_back(cond);
    }

    EvalResult evaluate(const std::string& input) const override {
        EvalResult result { true, {} };
        for (const auto& child : children) {
            EvalResult sub = child->evaluate(input);
            if (!sub.value) {
                result.value = false;
            }
            result.matches.insert(result.matches.end(), sub.matches.begin(), sub.matches.end());
        }
        return result;
    }
};

// ORノード(いずれかの子が成立)
class OrCondition : public ConditionNode {
    std::vector<std::shared_ptr<ConditionNode>> children;

public:
    void add(const std::shared_ptr<ConditionNode>& cond) {
        children.push_back(cond);
    }

    EvalResult evaluate(const std::string& input) const override {
        EvalResult result { false, {} };
        for (const auto& child : children) {
            EvalResult sub = child->evaluate(input);
            if (sub.value) {
                result.value = true;
                result.matches.insert(result.matches.end(), sub.matches.begin(), sub.matches.end());
            }
        }
        return result;
    }
};

🧪 使用例(構文解析風)

int main() {
    std::string text = "apple banana cherry";

    // 条件木を構築
    auto root = std::make_shared<AndCondition>();

    root->add(std::make_shared<WordCondition>("apple"));

    auto orNode = std::make_shared<OrCondition>();
    orNode->add(std::make_shared<WordCondition>("banana"));
    orNode->add(std::make_shared<WordCondition>("grape"));

    root->add(orNode);

    EvalResult result = root->evaluate(text);
    result.print();
}

🧾 出力例

Result: true
Matches:
  apple
  banana

🧩 応用:構文木(AST)への応用

この構造はそのまま、以下のような構文解析にも応用できます:

  • `WordCondition` → トークンやリテラル
  • `AndCondition` → 連結(例: 式 = 項 + 項)
  • `OrCondition` → 選択(例: 式 = 数字 | 変数)

もし構文ルールをBNFのような形式で定義して、そこから木を構築したい場合も対応できます。

さらに進めるなら:

  • ノードに型 (`enum class NodeType { And, Or, Word, ... }`) を追加
  • ソースコード中の位置(開始・終了)を保持
  • `evaluate` の代わりに `parse()` を用意し、パース木全体を返す

この構造をどう応用していきたいですか?
数式解析、言語解析、パターンマッチ…どこにフォーカスして作りこみましょうか?

これを使って数式の構文解析を行って、その結果を数式を表す構造に変換するプログラムを作ってください

と入力すると以下の結果となりました。

了解です!
この仕組みを使って 数式の構文解析 を行い、結果を 構造(AST: 抽象構文木)に変換するプログラムを作りましょう。

🔧 概要

📌 目標:

1. 入力文字列 `"1 + 2 * 3"` のような数式を解析する。
2. 再帰的な構文解析でASTを構築する。
3. ASTをツリー構造で保持して、後で評価・表示が可能。

✅ ステップ構成

1. トークン分割(Tokenizer)
2. パーサー(再帰下降構文解析
3. ASTノードの定義
4. 解析結果をASTとして構築

🧱 1. ASTノード定義

#include <iostream>
#include <memory>
#include <string>
#include <vector>
#include <sstream>
#include <cctype>

enum class NodeType {
    Number,
    BinaryOp
};

class ASTNode {
public:
    virtual ~ASTNode() = default;
    virtual void print(int indent = 0) const = 0;
};

using AST = std::shared_ptr<ASTNode>;

class NumberNode : public ASTNode {
public:
    double value;
    NumberNode(double v) : value(v) {}
    void print(int indent = 0) const override {
        std::string pad(indent, ' ');
        std::cout << pad << "Number(" << value << ")\n";
    }
};

class BinaryOpNode : public ASTNode {
public:
    char op;
    AST left;
    AST right;

    BinaryOpNode(char o, AST l, AST r) : op(o), left(std::move(l)), right(std::move(r)) {}

    void print(int indent = 0) const override {
        std::string pad(indent, ' ');
        std::cout << pad << "BinaryOp('" << op << "')\n";
        left->print(indent + 2);
        right->print(indent + 2);
    }
};

🧩 2. トークナイザ

enum class TokenType { Number, Plus, Minus, Mul, Div, LParen, RParen, End };

struct Token {
    TokenType type;
    double numberValue = 0;
};

class Tokenizer {
    std::string input;
    size_t pos = 0;
    Token current;

public:
    Tokenizer(const std::string& in) : input(in) {
        next();
    }

    Token peek() const { return current; }

    void next() {
        while (pos < input.size() && isspace(input[pos])) ++pos;

        if (pos >= input.size()) {
            current = {TokenType::End};
            return;
        }

        char ch = input[pos];

        if (isdigit(ch)) {
            double val = 0;
            while (pos < input.size() && isdigit(input[pos])) {
                val = val * 10 + (input[pos++] - '0');
            }
            current = {TokenType::Number, val};
        } else {
            ++pos;
            switch (ch) {
                case '+': current = {TokenType::Plus}; break;
                case '-': current = {TokenType::Minus}; break;
                case '*': current = {TokenType::Mul}; break;
                case '/': current = {TokenType::Div}; break;
                case '(': current = {TokenType::LParen}; break;
                case ')': current = {TokenType::RParen}; break;
                default:
                    throw std::runtime_error(std::string("Unknown character: ") + ch);
            }
        }
    }
};

🧠 3. 再帰下降パーサ

class Parser {
    Tokenizer tokenizer;

    AST parsePrimary() {
        Token tok = tokenizer.peek();
        if (tok.type == TokenType::Number) {
            tokenizer.next();
            return std::make_shared<NumberNode>(tok.numberValue);
        } else if (tok.type == TokenType::LParen) {
            tokenizer.next(); // consume '('
            AST node = parseExpr();
            if (tokenizer.peek().type != TokenType::RParen)
                throw std::runtime_error("Expected ')'");
            tokenizer.next(); // consume ')'
            return node;
        } else {
            throw std::runtime_error("Unexpected token in primary");
        }
    }

    AST parseTerm() {
        AST node = parsePrimary();
        while (true) {
            Token tok = tokenizer.peek();
            if (tok.type == TokenType::Mul || tok.type == TokenType::Div) {
                tokenizer.next();
                AST right = parsePrimary();
                node = std::make_shared<BinaryOpNode>(tok.type == TokenType::Mul ? '*' : '/', node, right);
            } else {
                break;
            }
        }
        return node;
    }

    AST parseExpr() {
        AST node = parseTerm();
        while (true) {
            Token tok = tokenizer.peek();
            if (tok.type == TokenType::Plus || tok.type == TokenType::Minus) {
                tokenizer.next();
                AST right = parseTerm();
                node = std::make_shared<BinaryOpNode>(tok.type == TokenType::Plus ? '+' : '-', node, right);
            } else {
                break;
            }
        }
        return node;
    }

public:
    Parser(const std::string& input) : tokenizer(input) {}

    AST parse() {
        AST result = parseExpr();
        if (tokenizer.peek().type != TokenType::End)
            throw std::runtime_error("Unexpected trailing tokens");
        return result;
    }
};

🧪 使用例

int main() {
    std::string expr = "1 + 2 * (3 + 4)";

    try {
        Parser parser(expr);
        AST tree = parser.parse();

        std::cout << "AST for: " << expr << "\n";
        tree->print();

    } catch (const std::exception& ex) {
        std::cerr << "Parse error: " << ex.what() << "\n";
    }

    return 0;
}

✅ 出力例

AST for: 1 + 2 * (3 + 4)
BinaryOp('+')
  Number(1)
  BinaryOp('*')
    Number(2)
    BinaryOp('+')
      Number(3)
      Number(4)

🧠 拡張のヒント

  • 括弧をネストしてさらに複雑な式もOK。
  • `UnaryOpNode` を追加すれば `-3` なども対応。
  • `evaluate()` を実装すれば計算機にもなる!

この構文解析器をどんな方向で使いたいですか?
たとえば以下のような応用が可能です:

  • GUIでASTを操作する
  • 数式をシンボリックに変形する(展開・因数分解など)
  • 条件付き構文やコマンドも扱う

ご希望があればその方向に合わせて拡張します!