提升精神:如何在使用文本说明符解析双打列表时使用自定义逻辑

Posted

技术标签:

【中文标题】提升精神:如何在使用文本说明符解析双打列表时使用自定义逻辑【英文标题】:Boost spirit: how to use custom logic when parsing a list of doubles with text specifiers 【发布时间】:2019-12-05 15:36:48 【问题描述】:

我想解析一个双精度向量。但是,此向量也可能包含两种类型的语句,它们会稍微压缩数据:FORRAMP

如果FOR 在字符串中,它的格式应该是"<double> FOR <int>"。这意味着重复<double> <int> 次。

例如"1 1.5 2 2.5 3 FOR 4 3.5" 应该解析为 1, 1.5, 2, 2.5, 3, 3, 3, 3, 3.5

如果RAMP 在字符串中,则格式应为"<double1> RAMP <int> <double2>"。这意味着在<double1><double2> 之间线性插值<int> 周期。

例如"1 2 3 4 RAMP 3 6 7 8" 应该解析为 1, 2, 3, 4, 5, 6, 7, 8

我不知道如何继续为单个元素定义解析器。遇到扩展时如何提供自定义代码来执行扩展?

谢谢!

【问题讨论】:

【参考方案1】:

没有语义动作的最简单的方法¹是解析成一个 AST,然后你再解释它。

更繁琐的方法是使用语义操作来构建结果。 (请记住,这会导致回溯语法出现问题。)

我做出了类似的回答:

Parsing comma-separated list of ranges and numbers with semantic actions 这里是基于正则表达式的范围表达式方法method for expand a-z to abc...xyz form 使用 C 来提高性能的竞争版本method for expand a-z to abc...xyz form

事不宜迟:

使用 AST 表示

一个示例 AST:

namespace AST 
    using N = unsigned long;
    using V = double;

    struct repeat  N n; V value; ;
    struct interpolate 
        N n; V start, end;
        bool is_valid() const;
    ;

    using element = boost::variant<repeat, interpolate>;
    using elements = std::vector<element>;    

is_valid 是一个很好的地方,我们可以在其中执行诸如“周期数不为零”或“如果周期数为 1,则开始和结束必须重合”之类的逻辑断言。 em>

现在,对于我们的最终结果,我们希望将其转换为 just-a-vector-of-V:

    using values = std::vector<V>;

    static inline values expand(elements const& v) 
        struct 
            values result;
            void operator()(repeat const& e) 
                result.insert(result.end(), e.n, e.value);
            
            void operator()(interpolate const& e) 
                if (!e.is_valid()) 
                    throw std::runtime_error("bad interpolation");
                
                if (e.n>0)  result.push_back(e.start); 
                if (e.n>2) 
                    auto const delta = (e.end-e.start)/(e.n-1);
                    for (N i=1; i<(e.n-1); ++i)
                        result.push_back(e.start + i * delta);
                
                if (e.n>1)  result.push_back(e.end); 
            
         visitor;
        for (auto& el : v) 
            boost::apply_visitor(visitor, el);
        
        return std::move(visitor.result);
    

现在我们已经掌握了基础知识,让我们解析和测试:

解析

首先,让我们调整 AST 类型:

BOOST_FUSION_ADAPT_STRUCT(AST::repeat, value, n)
BOOST_FUSION_ADAPT_STRUCT(AST::interpolate, start, n, end)

注意:适应属性的“自然语法顺序”使属性传播变得轻松而无需语义操作

现在让我们滚动一个语法:

namespace qi = boost::spirit::qi;

template <typename It> struct Grammar : qi::grammar<It, AST::elements()> 
    Grammar() : Grammar::base_type(start) 
        elements_ = *element_;
        element_ = interpolate_ | repeat_;
        repeat_
            = value_ >> "FOR" >> qi::uint_
            | value_ >> qi::attr(1u)
            ;
        interpolate_
            = value_ >> "RAMP" >> qi::uint_ >> value_
            ;

        value_ = qi::auto_;

        start = qi::skip(qi::space) [ elements_ ];

        BOOST_SPIRIT_DEBUG_NODES((start)(elements_)(element_)(repeat_)(interpolate_)(value_))
    
  private:
    qi::rule<It, AST::elements()> start;
    qi::rule<It, AST::elements(),    qi::space_type> elements_;
    qi::rule<It, AST::element(),     qi::space_type> element_;
    qi::rule<It, AST::repeat(),      qi::space_type> repeat_;
    qi::rule<It, AST::interpolate(), qi::space_type> interpolate_;
    qi::rule<It, AST::V(),           qi::space_type> value_;
;

注意:

BOOST_SPIRIT_DEBUG_NODES 启用规则调试 interpolate_ | repeat_ 的顺序很重要,因为repeat_ 也解析单个数字(因此它会阻止FROM 被及时解析。

一个调用解析器的简单实用程序以及expand() 中间表示:

AST::values do_parse(std::string const& input) 
    static const Grammar<std::string::const_iterator> g;

    auto f = begin(input), l = end(input);
    AST::elements intermediate;
    if (!qi::parse(f, l, g >> qi::eoi, intermediate)) 
        throw std::runtime_error("bad input");
    

    return expand(intermediate);

测试

布丁的证据在吃:

Live On Coliru

int main() 
    std::cout << std::boolalpha;

    struct  std::string input; AST::values expected;  cases[] = 
         "1 1.5 2 2.5 3 FOR 4 3.5",  1, 1.5, 2, 2.5, 3, 3, 3, 3, 3.5  ,
         "1 2 3 4 RAMP 3 6 7 8",  1, 2, 3, 4, 5, 6, 7, 8  ,
    ;

    for (auto const& test : cases) 
        try 
            std::cout << std::quoted(test.input) << " -> ";
            auto actual = Parse::do_parse(test.input);
            std::cout << (actual==test.expected? "PASSED":"FAILED") << "  ";

            // print the actual for reference
            std::cout << " ";
            for (auto& v : actual) std::cout << v << ", ";
            std::cout << "\n";
         catch(std::exception const& e) 
            std::cout << "ERROR " << std::quoted(e.what()) << "\n";
        
    

打印

"1 1.5 2 2.5 3 FOR 4 3.5" -> PASSED   1, 1.5, 2, 2.5, 3, 3, 3, 3, 3.5, 
"1 2 3 4 RAMP 3 6 7 8" -> PASSED   1, 2, 3, 4, 5, 6, 7, 8, 

使用语义动作代替

这可能更有效,我发现我实际上更喜欢这种方法的表现力。

不过,随着语法变得越来越复杂,它可能无法很好地扩展。

在这里我们“反转”流程:

Grammar() : Grammar::base_type(start) 
    element_ =
          qi::double_                          [ px::push_back(qi::_val, qi::_1) ]
        | ("FOR" >> qi::uint_)                 [ handle_for(qi::_val, qi::_1) ]
        | ("RAMP" >> qi::uint_ >> qi::double_) [ handle_ramp(qi::_val, qi::_1, qi::_2) ]
        ;

    start = qi::skip(qi::space) [ *element_ ];

这里的语义动作中的handle_forhandle_ramp 是惰性Actor,它们与expand() 在基于AST 的方法中执行的操作基本相同,但是

在飞行中 第一个操作数是隐式的(它是已经在向量后面的最后一个值)

这会进行一些额外的检查(当用户传递以"FOR""RAMP" 开头的字符串时,我们不希望UB):

    struct handle_for_f 
        void operator()(Values& vec, unsigned n) const 
            if (vec.empty() || n<1)
                throw std::runtime_error("bad quantifier");

            vec.insert(vec.end(), n-1, vec.back());
        
    ;

    struct handle_ramp_f 
        void operator()(Values& vec, unsigned n, double target) const 
            if (vec.empty())
                throw std::runtime_error("bad quantifier");
            if ((n == 0) || (n == 1 && (vec.back() != target)))
                throw std::runtime_error("bad interpolation");

            auto start = vec.back();

            if (n>2) 
                auto const delta = (target-start)/(n-1);
                for (std::size_t i=1; i<(n-1); ++i)
                    vec.push_back(start + i * delta);
            
            if (n>1)  vec.push_back(target); 
        
    ;

为了避免boost::phoenix::bind在语义动作中的繁琐,让我们适应凤凰功能:

    px::function<handle_for_f> handle_for;
    px::function<handle_ramp_f> handle_ramp;

解析

do_parse 助手变得更简单,因为我们没有中间表示:

Values do_parse(std::string const& input) 
    static const Grammar<std::string::const_iterator> g;

    auto f = begin(input), l = end(input);
    Values values;
    if (!qi::parse(f, l, g >> qi::eoi, values)) 
        throw std::runtime_error("bad input");
    

    return values;

测试

同样,布丁的证据在于吃。未修改main()的测试程序:

Live On Coliru

#include <boost/spirit/include/qi.hpp>
#include <boost/spirit/include/phoenix.hpp>
#include <iostream>
#include <iomanip>

using Values = std::vector<double>;

namespace Parse 
    namespace qi = boost::spirit::qi;
    namespace px = boost::phoenix;

    template <typename It> struct Grammar : qi::grammar<It, Values()> 
        Grammar() : Grammar::base_type(start) 
            element_ =
                  qi::double_                          [ px::push_back(qi::_val, qi::_1) ]
                | ("FOR" >> qi::uint_)                 [ handle_for(qi::_val, qi::_1) ]
                | ("RAMP" >> qi::uint_ >> qi::double_) [ handle_ramp(qi::_val, qi::_1, qi::_2) ]
                ;

            start = qi::skip(qi::space) [ *element_ ];
        
      private:
        qi::rule<It, Values()> start;
        qi::rule<It, Values(), qi::space_type> element_;

        struct handle_for_f 
            void operator()(Values& vec, unsigned n) const 
                if (vec.empty() || n<1)
                    throw std::runtime_error("bad quantifier");

                vec.insert(vec.end(), n-1, vec.back());
            
        ;

        struct handle_ramp_f 
            void operator()(Values& vec, unsigned n, double target) const 
                if (vec.empty())
                    throw std::runtime_error("bad quantifier");
                if ((n == 0) || (n == 1 && (vec.back() != target)))
                    throw std::runtime_error("bad interpolation");

                auto start = vec.back();

                if (n>2) 
                    auto const delta = (target-start)/(n-1);
                    for (std::size_t i=1; i<(n-1); ++i)
                        vec.push_back(start + i * delta);
                
                if (n>1)  vec.push_back(target); 
            
        ;

        px::function<handle_for_f> handle_for;
        px::function<handle_ramp_f> handle_ramp;
    ;

    Values do_parse(std::string const& input) 
        static const Grammar<std::string::const_iterator> g;

        auto f = begin(input), l = end(input);
        Values values;
        if (!qi::parse(f, l, g >> qi::eoi, values)) 
            throw std::runtime_error("bad input");
        

        return values;
    


int main() 
    std::cout << std::boolalpha;

    struct  std::string input; Values expected;  cases[] = 
         "1 1.5 2 2.5 3 FOR 4 3.5",  1, 1.5, 2, 2.5, 3, 3, 3, 3, 3.5  ,
         "1 2 3 4 RAMP 3 6 7 8",  1, 2, 3, 4, 5, 6, 7, 8  ,
    ;

    for (auto const& test : cases) 
        try 
            std::cout << std::quoted(test.input) << " -> ";
            auto actual = Parse::do_parse(test.input);
            std::cout << (actual==test.expected? "PASSED":"FAILED") << "  ";

            // print the actual for reference
            std::cout << " ";
            for (auto& v : actual) std::cout << v << ", ";
            std::cout << "\n";
         catch(std::exception const& e) 
            std::cout << "ERROR " << std::quoted(e.what()) << "\n";
        
    

打印和以前一样:

"1 1.5 2 2.5 3 FOR 4 3.5" -> PASSED   1, 1.5, 2, 2.5, 3, 3, 3, 3, 3.5, 
"1 2 3 4 RAMP 3 6 7 8" -> PASSED   1, 2, 3, 4, 5, 6, 7, 8, 

¹Boost Spirit: "Semantic actions are evil"?

【讨论】:

哇,谢谢。我用语义动作 lambda(捕获双向量)尽了最大努力,但我无法找出斜坡处理程序的正确签名。 [&amp;vec](const double &amp;val) 作为double_ 的处理程序非常有用,但我无法为qi::no_case[qi::lit("ramp")] &gt;&gt; qi::int_ &gt;&gt; qi::double_ 解决这个问题。希望有一些不那么冗长的东西。 刚刚想通了!这是[&amp;vec](const boost::fusion::vector&lt;int, double&gt; &amp; vals)。让编译器通过使用[&amp;vec](auto &amp;vals) 告诉我它并使用vals 做一些不允许的事情。感谢编译器。【参考方案2】:

这就是我最终的结果。它使用语义动作,但它比@sehe 更简单的答案可能更正确:不使用模板函数,不使用 phoenix,不需要自定义语法结构。

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

#include <boost/spirit/include/qi.hpp>

namespace qi = boost::spirit::qi;
namespace fusion = boost::fusion;

std::vector<double> parseVector(const std::string & vec_str)

    std::vector<double> vec;

    auto for_handler = [&vec, &vec_str](const unsigned &len) 
        if (len == 0)
            throw std::runtime_error("Invalid vector: " + vec_str);
        vec.insert(vec.end(), len - 1, vec.back());
    ;
    auto ramp_handler = [&vec, &vec_str](const fusion::vector<unsigned, double> & vals) 
        double start = vec.back();
        double target = fusion::at_c<1>(vals);
        unsigned len = fusion::at_c<0>(vals);
        if (len == 0 || (len == 1 && start != target))
            throw std::runtime_error("Invalid vector: " + vec_str);
        if (len >= 2) 
            for (unsigned i = 0; i < len - 2; i++)
                vec.push_back(start + (i + 1) * (target - start) / (len - 1));
            vec.push_back(target);
        
    ;
    auto double_handler = [&vec](const double &val) 
        vec.push_back(val);
    ;

    auto for_rule = qi::no_case[qi::lit("for") | qi::lit('*')] >> qi::uint_;
    auto ramp_rule = qi::no_case[qi::lit("ramp")] >> qi::uint_ >> qi::double_;
    auto vec_rule = qi::double_[double_handler] >> *(for_rule[for_handler] | ramp_rule[ramp_handler] | qi::double_[double_handler]);

    auto it = vec_str.begin();
    if (!qi::phrase_parse(it, vec_str.end(), vec_rule, qi::ascii::space) || it != vec_str.end())
        throw std::runtime_error("Invalid vector: " + vec_str);

    return vec;

现在如果我只能让"1 for 4.5 1" 抛出错误而不是解析为1 1 1 1 0.5 1 。叹息。

【讨论】:

我喜欢简洁! Thist + 让它工作是一个优秀开发者的标志。 (轻微的错误:内联 delta 并使循环基于 0 不会通过我的代码审查,因为它会主动混淆逻辑)。 然而,“自定义语法结构”不仅仅是为了展示。至少你需要在这里命名为qi::rules(具有讽刺意味的是,你的命名暗示它们会是这样)。正如所写的,行为是未指定的,很可能是未定义的。见【关于“自动”和灵气规则的众多答案】(用户:85371 boost_spirit_auto)。在最近的 Boost 版本中,您可以使用 qi::copy 工具来确保获得深度克隆的表达式树。 在使用back() 之前删除empty() 检查是准确的,但它取决于您可以强制语法输入双精度值的假设。我不想做出这样的假设,因为它会停止解析空输入。 关于最后一个问题,您可以单独提出一个问题,但现在让我在 cmets 中向您展示。您需要一个前瞻断言(正面的:qi::lexeme[qi::uint_ &gt;&gt; &amp;qi::space] 或负面的!strict_double_ &gt;&gt; qi::uint_)。 This Live Demo 向您展示了在将所有操作捆绑在单个处理程序中的同时,集中了错误报告(使用 pass 变量)。 Backgrounders: 1. params to SA handlers 2. why the lexeme[] 具有积极的前瞻功能 3. 注意使用&gt;&gt; qi::eoi 而不是对结束迭代器进行繁琐的检查 4. 注意@的使用987654337@ 5. 琐碎的注意:如果您不打算通过引用来引用它们,则将doubleunsigned 通过const&amp; 是一种反模式。

以上是关于提升精神:如何在使用文本说明符解析双打列表时使用自定义逻辑的主要内容,如果未能解决你的问题,请参考以下文章

提升精神提取第一个单词并将其存储在向量中

提升精神:如何解析直到我们有“->”

提升精神语义动作参数

提升精神词位及其属性

Android -- 使用自定义列表视图时如何使用 onlistitemclick() 函数

提升精神:将结果复制到字符串向量中