提升精神:如何在使用文本说明符解析双打列表时使用自定义逻辑
Posted
技术标签:
【中文标题】提升精神:如何在使用文本说明符解析双打列表时使用自定义逻辑【英文标题】:Boost spirit: how to use custom logic when parsing a list of doubles with text specifiers 【发布时间】:2019-12-05 15:36:48 【问题描述】:我想解析一个双精度向量。但是,此向量也可能包含两种类型的语句,它们会稍微压缩数据:FOR
和 RAMP
。
如果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_for
和handle_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(捕获双向量)尽了最大努力,但我无法找出斜坡处理程序的正确签名。[&vec](const double &val)
作为double_
的处理程序非常有用,但我无法为qi::no_case[qi::lit("ramp")] >> qi::int_ >> qi::double_
解决这个问题。希望有一些不那么冗长的东西。
刚刚想通了!这是[&vec](const boost::fusion::vector<int, double> & vals)
。让编译器通过使用[&vec](auto &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_ >> &qi::space]
或负面的!strict_double_ >> qi::uint_
)。 This Live Demo 向您展示了在将所有操作捆绑在单个处理程序中的同时,集中了错误报告(使用 pass
变量)。
Backgrounders: 1. params to SA handlers 2. why the lexeme[]
具有积极的前瞻功能 3. 注意使用>> qi::eoi
而不是对结束迭代器进行繁琐的检查 4. 注意@的使用987654337@ 5. 琐碎的注意:如果您不打算通过引用来引用它们,则将double
或unsigned
通过const&
是一种反模式。以上是关于提升精神:如何在使用文本说明符解析双打列表时使用自定义逻辑的主要内容,如果未能解决你的问题,请参考以下文章