谢谢,事实是 json 只是我必须使用的众多格式之一,有些格式是专有的并且没有库,所以我想为所有人使用统一的方式。我决定使用 json 来解决这个问题,因为社区知道的不仅仅是 asciimath 或我们创建的其他格式 – Jepessen 9 hours ago
这对我的建议没有任何改变。如果有的话,它确实强调了您不希望施加任意限制。
Karma 的问题
-
Karma 是用于静态生成器的“内联”DSL。它们适用于静态类型的事物。您的 AST 使用动态多态性。
这消除了编写简洁生成器的任何机会,除非使用许多复杂的语义动作。我不记得写过很多与 Karma 相关的明确答案,但动态多态性和语义动作的问题在 Qi 方面都差不多:
所有主要缺点都适用,除了显然没有创建 AST,因此分配的性能影响不如 Qi 解析器严重。
但是,相同的逻辑仍然存在:Karma 生成器静态组合以提高效率。但是,您的动态类型层次结构排除了大部分效率。换句话说,你不是 Karma 的目标受众。
-
无论您的 AST 是如何设计的,Karma 都有另一个结构性限制会在这里受到影响:(非常)难以利用有状态规则进行漂亮的打印。
对我来说,这是几乎从不使用 Karma 的一个关键原因。即使漂亮的打印不是目标,您仍然可以获得类似的里程,只需直接使用 Boost Fusion 生成访问 AST 的输出(我们在我们的项目中使用它来生成 API 类型的不同版本的 OData XML 和 JSON 表示形式,以用于 restful API )。
当然,有一些有状态生成任务具有内置到 Karma 的自定义指令,有时它们会达到快速原型设计的最佳位置,例如
不管怎样,让我们做吧
因为我不是受虐狂,所以我会从other answer 借用一个概念:创建一个中间表示,以更好地促进 Karma。
在此示例中,中间表示可能非常简单,但我怀疑您的其他要求(例如 "for example, asciimath or other formats created by us")将需要更详细的设计。
///////////////////////////////////////////////////////////////////////////////
// A simple intermediate representation
#include <boost/variant.hpp>
namespace output_ast {
struct Function;
struct Value;
using Expression = boost::variant<Function, Value>;
using Arguments = std::vector<Expression>;
struct Value { std::string name, value; };
struct Function { std::string name; Arguments args; };
}
首先,因为我们要使用 Karma,所以我们确实需要实际调整中间表示:
#include <boost/fusion/include/struct.hpp>
BOOST_FUSION_ADAPT_STRUCT(output_ast::Value, name, value)
BOOST_FUSION_ADAPT_STRUCT(output_ast::Function, name, args)
生成器
这是我能想到的最简单的生成器,给予和接受两件事:
- 我已经对其进行了相当长的调整,以获得一些“可读”的格式。如果您删除所有无关紧要的空格,它会变得更简单。
- 我选择不存储冗余信息(例如中间表示中的静态“类型”表示)。这样做会稍微简单一些,主要是通过使
type 规则更类似于name 和value。
namespace karma_json {
namespace ka = boost::spirit::karma;
template <typename It>
struct Generator : ka::grammar<It, output_ast::Expression()> {
Generator() : Generator::base_type(expression) {
expression = function|value;
function
= "{\n " << ka::delimit(",\n ")
[name << type(+"Function") ]
<< arguments
<< "\n}"
;
arguments = "\"arguments\": [" << -(("\n " << expression) % ",") << ']';
value
= "{\n " << ka::delimit(",\n ")
[name << type(+"Value") ]
<< value_
<< "\n}"
;
type = "\"type\":\"" << ka::string(ka::_r1) << "\"";
string = '"' << *('\\' << ka::char_("\\\"") | ka::char_) << '"';
name = "\"name\":" << string;
value_ = "\"value\":" << string;
}
private:
ka::rule<It, output_ast::Expression()> expression;
ka::rule<It, output_ast::Function()> function;
ka::rule<It, output_ast::Arguments()> arguments;
ka::rule<It, output_ast::Value()> value;
ka::rule<It, std::string()> string, name, value_;
ka::rule<It, void(std::string)> type;
};
}
后记
为了完整起见,我进行了简化。并遇到了这个 excellent 演示完全不明显的属性处理怪癖。以下(只是剥离空白处理)不工作:
function = '{' << ka::delimit(',') [name << type] << arguments << '}';
value = '{' << ka::delimit(',') [name << type] << value_ << '}' ;
如果您喜欢戏剧,可以阅读错误小说here。问题是delimit[] 块神奇地将属性合并到一个字符串中(呵呵)。错误消息反映了字符串属性没有被使用,例如启动arguments 生成器。
治疗症状最直接的办法就是拆属性,但没有真正的办法:
function = '{' << ka::delimit(',') [name << ka::eps << type] << arguments << '}';
value = '{' << ka::delimit(',') [name << ka::eps << type] << value_ << '}' ;
没有区别
function = '{' << ka::delimit(',') [ka::as_string[name] << ka::as_string[type]] << arguments << '}';
value = '{' << ka::delimit(',') [ka::as_string[name] << ka::as_string[type]] << value_ << '}' ;
如果它真的有效,那就太好了。没有任何添加包含或替换为ka::as<std::string>()[...] 之类的咒语使编译错误消失。²
所以,为了结束这个悲伤的故事,我们将陷入令人麻木的乏味:
function = '{' << name << ',' << type << ',' << arguments << '}';
arguments = "\"arguments\":[" << -(expression % ',') << ']';
有关现场演示,请参阅下面标有“简化版”的部分。
使用它
使用该语法生成的最短方法是创建中间表示:
///////////////////////////////////////////////////////////////////////////////
// Expression -> output_ast
struct serialization {
static output_ast::Expression call(Expression const* e) {
if (auto* f = dynamic_cast<Function const*>(e)) {
output_ast::Arguments args;
for (auto& a : f->m_arguments) args.push_back(call(a));
return output_ast::Function { f->getName(), args };
}
if (auto* v = dynamic_cast<Value const*>(e)) {
return output_ast::Value { v->getName(), v->getValue() };
}
return {};
}
};
auto to_output(Expression const* expression) {
return serialization::call(expression);
}
然后使用它:
using It = boost::spirit::ostream_iterator;
std::cout << format(karma_json::Generator<It>{}, to_output(plus1));
完整演示
Live On Wandbox¹
#include <boost/lexical_cast.hpp>
#include <iostream>
#include <vector>
struct Expression {
virtual std::string getName() const = 0;
};
struct Value : Expression {
virtual std::string getValue() const = 0;
};
struct IntegerValue : Value {
IntegerValue(int value) : m_value(value) {}
virtual std::string getName() const override { return "IntegerValue"; }
virtual std::string getValue() const override { return boost::lexical_cast<std::string>(m_value); }
private:
int m_value;
};
struct Function : Expression {
void addArgument(Expression *expression) { m_arguments.push_back(expression); }
virtual std::string getName() const override { return m_name; }
protected:
std::vector<Expression *> m_arguments;
std::string m_name;
friend struct serialization;
};
struct Plus : Function {
Plus() : Function() { m_name = "Plus"; }
};
///////////////////////////////////////////////////////////////////////////////
// A simple intermediate representation
#include <boost/variant.hpp>
namespace output_ast {
struct Function;
struct Value;
using Expression = boost::variant<Function, Value>;
using Arguments = std::vector<Expression>;
struct Value { std::string name, value; };
struct Function { std::string name; Arguments args; };
}
#include <boost/fusion/include/struct.hpp>
BOOST_FUSION_ADAPT_STRUCT(output_ast::Value, name, value)
BOOST_FUSION_ADAPT_STRUCT(output_ast::Function, name, args)
#include <boost/spirit/include/karma.hpp>
namespace karma_json {
namespace ka = boost::spirit::karma;
template <typename It>
struct Generator : ka::grammar<It, output_ast::Expression()> {
Generator() : Generator::base_type(expression) {
expression = function|value;
function
= "{\n " << ka::delimit(",\n ")
[name << type(+"Function") ]
<< arguments
<< "\n}"
;
arguments = "\"arguments\": [" << -(("\n " << expression) % ",") << ']';
value
= "{\n " << ka::delimit(",\n ")
[name << type(+"Value") ]
<< value_
<< "\n}"
;
type = "\"type\":\"" << ka::string(ka::_r1) << "\"";
string = '"' << *('\\' << ka::char_("\\\"") | ka::char_) << '"';
name = "\"name\":" << string;
value_ = "\"value\":" << string;
}
private:
ka::rule<It, output_ast::Expression()> expression;
ka::rule<It, output_ast::Function()> function;
ka::rule<It, output_ast::Arguments()> arguments;
ka::rule<It, output_ast::Value()> value;
ka::rule<It, std::string()> string, name, value_;
ka::rule<It, void(std::string)> type;
};
}
///////////////////////////////////////////////////////////////////////////////
// Expression -> output_ast
struct serialization {
static output_ast::Expression call(Expression const* e) {
if (auto* f = dynamic_cast<Function const*>(e)) {
output_ast::Arguments args;
for (auto& a : f->m_arguments) args.push_back(call(a));
return output_ast::Function { f->getName(), args };
}
if (auto* v = dynamic_cast<Value const*>(e)) {
return output_ast::Value { v->getName(), v->getValue() };
}
return {};
}
};
auto to_output(Expression const* expression) {
return serialization::call(expression);
}
int main() {
// Build expression 4 + 5 + 6 as 4 + (5 + 6)
Function *plus1 = new Plus();
Function *plus2 = new Plus();
Value *iv4 = new IntegerValue(4);
Value *iv5 = new IntegerValue(5);
Value *iv6 = new IntegerValue(6);
plus2->addArgument(iv5);
plus2->addArgument(iv6);
plus1->addArgument(iv4);
plus1->addArgument(plus2);
// Generate json string here, but how?
using It = boost::spirit::ostream_iterator;
std::cout << format(karma_json::Generator<It>{}, to_output(plus1));
}
输出
生成器的可读性/健壮性/功能性如我所愿(存在与分隔符相关的怪癖,当类型包含需要引用的字符时存在问题,没有状态缩进)。
结果看起来不像预期的那样,虽然它是有效的 JSON:
{
"name":"Plus",
"type":"Function",
"arguments": [
{
"name":"IntegerValue",
"type":"Value",
"value":"4"
},
{
"name":"Plus",
"type":"Function",
"arguments": [
{
"name":"IntegerValue",
"type":"Value",
"value":"5"
},
{
"name":"IntegerValue",
"type":"Value",
"value":"6"
}]
}]
}
修复它是一个不错的挑战,如果你想尝试的话。
简化版
简化版本,包含上面记录的属性处理解决方法:
Live On Coliru
namespace karma_json {
namespace ka = boost::spirit::karma;
template <typename It>
struct Generator : ka::grammar<It, output_ast::Expression()> {
Generator() : Generator::base_type(expression) {
expression = function|value;
function = '{' << name << ',' << type << ',' << arguments << '}';
arguments = "\"arguments\":[" << -(expression % ',') << ']';
value = '{' << name << ',' << type << ',' << value_ << '}' ;
string = '"' << *('\\' << ka::char_("\\\"") | ka::char_) << '"';
type = "\"type\":" << string;
name = "\"name\":" << string;
value_ = "\"value\":" << string;
}
private:
ka::rule<It, output_ast::Expression()> expression;
ka::rule<It, output_ast::Function()> function;
ka::rule<It, output_ast::Arguments()> arguments;
ka::rule<It, output_ast::Value()> value;
ka::rule<It, std::string()> string, name, type, value_;
};
}
产生以下输出:
{"name":"Plus","type":"Function","arguments":[{"name":"IntegerValue","type":"Value","value":"4"},{"name":"Plus","type":"Function","arguments":[{"name":"IntegerValue","type":"Value","value":"5"},{"name":"IntegerValue","type":"Value","value":"6"}]}]}
我倾向于认为这是比“漂亮”格式化失败的尝试更好的成本/收益比很多。但这里的真实情况是,维护成本无论如何都是天价。
¹有趣的是,Coliru 超过了编译时间...这也可能是指导您的设计决策的一个论点
² 让您想知道有多少人每天实际使用 Karma