Разбор феникса

У меня есть код парсера, как показано ниже для функции "TakeOne". Функция TakeOne работает так, как будто возвращает первый параметр, который не равен "%null%". Пример:

TakeOne( %null% , '3', 'defaultVal'); -->  result = 3
TakeOne( 5 , 'asd', 'defaultVal');   -> result = 5

Теперь я хочу изменить эту функцию

TakeOne(parm1, parm2, ... , defaultValue);

Возможно ли сделать это без использования функций C++11? Спасибо

#include <string>
#include <fstream>
#include <algorithm>
#include "sstream"
#include <locale.h>
#include <iomanip>

#define BOOST_SPIRIT_USE_PHOENIX_V3

#include <boost/functional/hash.hpp>
#include <boost/variant.hpp>
#include <boost/smart_ptr.hpp>
#include <boost/algorithm/string.hpp>
#include <boost/numeric/conversion/cast.hpp>
#include <boost/tokenizer.hpp>
#include <boost/spirit/include/qi.hpp>
#include <boost/spirit/include/phoenix.hpp>
#include <boost/phoenix/function/adapt_function.hpp>
#include <boost/lexical_cast.hpp>
#include <boost/math/constants/constants.hpp>
#include <boost/math/special_functions/round.hpp>
#include <boost/exception/diagnostic_information.hpp> 
#include <boost/algorithm/string.hpp> 

namespace qi    = boost::spirit::qi;
namespace phx   = boost::phoenix;

typedef double NumValue;
typedef boost::variant<double, std::wstring> GenericValue;

const std::wstring ParserNullChar = L"%null%";
const double NumValueDoubleNull = std::numeric_limits<double>::infinity();

//Convert string to numeric values
struct AsNumValue : boost::static_visitor<double>
{
    double operator()(double d)              const { return d; }
    double operator()(std::wstring const& s) const
    { 
        if(boost::iequals(s, ParserNullChar))
        {
            return NumValueDoubleNull;
        }
        try { return boost::lexical_cast<double>(s); } 
        catch(...) 
        {
            throw;
        }
    }
};

double Num(GenericValue const& val)
{ 
    return boost::apply_visitor(AsNumValue(), val);
}

bool CheckIfNumValueIsNull(double num)
{
    if(num == NumValueDoubleNull)
        return true;
    else
        return false;
}


bool CheckIfGenericValIsNull(const GenericValue& val)
{
    std::wostringstream woss;
    woss << val;

    if(boost::iequals(woss.str(), ParserNullChar))
    {
        return true;
    }
    else if(val.which() != 1 && CheckIfNumValueIsNull(Num(val)))
    {
        return true;
    }
    else
        return false;
}


GenericValue TakeOne(GenericValue val1, GenericValue val2, GenericValue def)
{
  if(!CheckIfGenericValIsNull(val1))
    {
         return val1;
    }
    else if(!CheckIfGenericValIsNull(val2))
    {
        return val2;
    }
    else
        return def;
}



BOOST_PHOENIX_ADAPT_FUNCTION(GenericValue, TakeOne_, TakeOne, 3)

template <typename It, typename Skipper = qi::space_type >
struct MapFunctionParser : qi::grammar<It, GenericValue(), Skipper>
{
    MapFunctionParser() : MapFunctionParser::base_type(expr_)
    {
        using namespace qi;

        function_call_ = 
          (no_case[L"TakeOne"] > '(' > expr_ > ',' > expr_ > ',' > expr_ > ')') 
            [_val = TakeOne_(_1, _2, _3) ];


        string_ =   (L'"' > *~char_('"') > L'"')
            | (L"'" > *~char_("'") > L"'"); 


        factor_ =
    (no_case[ParserNullChar])     [_val = NumValueDoubleNull]
        |   double_                    [ _val = _1]
        |   string_              [ _val = _1]
        |   function_call_       [ _val = _1]
        ;


        expr_ = factor_;

        on_error<fail> ( expr_, std::cout
            << phx::val("Error! Expecting ") << _4 << phx::val(" here: \"")
            << phx::construct<std::string>(_3, _2) << phx::val("\"\n"));

#ifdef _DEBUG 
        BOOST_SPIRIT_DEBUG_NODE(function_call_);
        BOOST_SPIRIT_DEBUG_NODE(expr_);
        BOOST_SPIRIT_DEBUG_NODE(string_);
        BOOST_SPIRIT_DEBUG_NODE(factor_);
#endif
    }

private:
    qi::rule<It, std::wstring()> string_;
    qi::rule<It, GenericValue(), Skipper> function_call_, expr_, factor_;

};


int main()
{
    std::wstringstream wss;
    typedef std::wstring::const_iterator AttIter;
    MapFunctionParser<AttIter , boost::spirit::qi::space_type> mapFunctionParser;
    bool ret;
    GenericValue result;

    std::wstring functionStr = L"TakeOne(%null%, 5, 'default')";
    std::wstring::const_iterator beginExpression(functionStr.begin());
    std::wstring::const_iterator endExpression(functionStr.end());

    ret = boost::spirit::qi::phrase_parse(beginExpression,endExpression,mapFunctionParser,boost::spirit::qi::space,result);

    std::wcout << result << std::endl;
    return 0;
}

2 ответа

Решение

Вот второй ответ на некоторые (некоторые из) проблем XY, которые вы, вероятно, пытаетесь решить.

Как я заметил в комментарии, в вашем примере есть несколько запахов кода [1]. Позвольте мне объяснить, что я имею в виду.


Цель

Давайте рассмотрим, какова цель программы: вы делаете анализ ввода.

Результатом синтаксического анализа должны быть данные, предпочтительно в типах данных C++ со строгой информацией о типах, чтобы вы могли избежать работы с причудливыми (возможно, недопустимыми) представлениями переменных текста и сосредоточиться на бизнес-логике.


Запахи

Теперь о запахах:

  • Вы определяете "абстрактные типы данных" (например, NumValue) но тогда вы не сможете использовать их последовательно:

    typedef double NumValue;
    typedef boost::variant<double, std::wstring> GenericValue; 
    //                     ^--- should be NumValue
    

    Будьте более последовательны и сделайте так, чтобы ваш код отражал дизайн:

    namespace ast {
        typedef double                         Number;
        typedef std::wstring                   String;
        typedef boost::variant<Number, String> Value;
    }
    
  • Вы используете генератор синтаксического анализа для анализа, но вы также вызываете

    • boost::lexical_cast<double> на... струнах
    • wostringstream от которого ты забыл std::ios::skipws...) извлечь строку "a"
    • boost::iequals сравнивать строки, которые уже должны были быть проанализированы в их строго типизированные типы AST, независимо от регистра.
    • У тебя есть static_visitor действовать по вариантным типам, но вы полагаетесь на строковое преобразование (используя wostringstream). На самом деле, вы только звоните посетителю по этому варианту, если вы уже знаете, что это номер:

      else if(val.which() != 1 && CheckIfNumValueIsNull(Num(val)))
      

      Это немного забавно, потому что в этом случае вы могли бы просто использовать boost::get<NumValue>(val) чтобы получить значение известного типа.

    Совет от профессионала: использование низкоуровневых операций синтаксического анализа / потоковой передачи при использовании высокоуровневого генератора синтаксических анализаторов - это запах кода

  • Ваш вариант родового значения предполагает, что ваша грамматика поддерживает два вида значений. Тем не менее, ваше определение грамматики ясно показывает, что у вас есть третий тип значения: %null%,

    Есть доказательства того, что вы сами немного запутались, так как мы можем видеть парсер

    • разбор буквального %null% (или же %NULL% и т.д.) в... какое-то магическое число.
    • поэтому мы знаем, что если %null% разобрали, всегда будет NumValue в вашем АСТ
    • мы также знаем, что строки всегда будут разбираться в wstring подтип GenericValue
    • тем не менее, мы можем видеть, как вы относитесь к любому GenericValue как потенциально нуль?

    В целом это приводит к довольно удивительному...

    Резюме: у вас есть AsNumValue который вы (по иронии судьбы), кажется, используете, чтобы выяснить, является ли String может быть на самом деле Null

    Подсказка: String никогда не мог представлять %null% для начала нет смысла преобразовывать случайные строки в числа, и случайные "магические числовые значения" не должны были использоваться для представления Null на первом месте.

  • Ваша грамматика делает несбалансированным использование семантических действий:

    factor_ =
        (no_case[ParserNullChar])     [_val = NumValueDoubleNull]
            |   double_               [ _val = _1]
            |   string_              [ _val = _1]
            |   function_call_       [ _val = _1]
            ;
    

    Мы замечаем, что вы одновременно

    • использование SA для ручного выполнения того, что должно делать автоматическое распространение атрибутов ([_val = _1])
    • использование одной ветви для "магических" целей (это где вам требуется Null Тип данных AST)

    В моем предложенном решении ниже, правило становится:

    factor_ = null_ | string_ | double_ | function_call_;
    

    Вот и все.

    Совет для профессионалов: экономное использование семантических действий (см. Также " Повышение духа: " семантические действия - зло "?)


Решение

В общем, много места для упрощения и очистки. В отделе АСТ,

  • Расширить вариант значения явным Null подтип
  • Переименование типов и перемещение в пространство имен для удобства чтения
  • Брось AsNumValue функция, которая не имеет цели. Вместо этого есть IsNull посетитель, который просто сообщает true за Null ценности.
namespace ast {
    typedef double       Number;
    typedef std::wstring String;
    struct               Null {};

    typedef boost::variant<Null, Number, String> Value;

    struct IsNull
    {
        typedef bool result_type;
        template <typename... T>
        constexpr result_type operator()(T const&...) const { return false; }
        constexpr result_type operator()(Null const&) const { return true;  }
    };
}

В отделе грамматики

  • Разложить грамматику в правила, соответствующие узлам AST

    qi::rule<It, ast::String()>         string_;
    qi::rule<It, ast::Number()>         number_;
    qi::rule<It, ast::Null()>           null_;
    qi::rule<It, ast::Value(), Skipper> factor_, expr_, function_call_;
    
  • Это делает вашу грамматику простой в обслуживании и рассуждает о:

    string_ = (L'"' > *~char_('"') > L'"')
            | (L"'" > *~char_("'") > L"'")
            ; 
    number_ = double_;
    null_   = no_case["%null%"] > attr(ast::Null());
    factor_ = null_ | string_ | double_ | function_call_;
    expr_   = factor_;
    
    BOOST_SPIRIT_DEBUG_NODES((expr_)(factor_)(null_)(string_)(number_)(function_call_))
    
  • Обратите внимание, что это делает вывод отладочной информации более информативным

  • Я взял на себя смелость переименовать TakeOne в Coalesce [2]:

    function_call_ = no_case[L"Coalesce"] 
        > ('(' > expr_ % ',' > ')') [ _val = Coalesce_(_1) ];
    

    Это все еще использует подход, как я показал в другом ответе, только реализация стала намного проще, потому что больше нет путаницы в том, что может быть Null

    Забери: Нулевые значения просто... Нулевые!

Удаление теперь неиспользуемого заголовка включает и добавление загрузки тестовых входов:

int main()
{
    typedef std::wstring::const_iterator It;
    MapFunctionParser<It, boost::spirit::qi::space_type> parser;

    for (std::wstring input : { 
            L"Coalesce()",
            L"Coalesce('simple')",
            L"CoALesce(99)",
            L"CoalESCe(%null%, 'default')",
            L"coalesce(%null%, -inf)",
            L"COALESCE(%null%, 3e-1)",
            L"Coalesce(%null%, \"3e-1\")",
            L"COALESCE(%null%, 5, 'default')",
            L"COALESCE(%NULL%, %null%, %nuLl%, %NULL%, %null%, %null%, %nUll%, %null%, %NULL%, %nUll%, %null%, \n"
                    L"%NULL%, %NULL%, %null%, %null%, %nUll%, %null%, %nUll%, %nULl%, %null%, %null%, %null%, \n"
                    L"%null%, %nUll%, %NULL%, %null%, %null%, %null%, %null%, %null%, %null%, %nUll%, %nulL%, \n"
                    L"%null%, %null%, %nUll%, %NULL%, 'this is the first nonnull',    %nUll%, %null%, %Null%, \n"
                    L"%NULL%, %null%, %null%, %null%, %NULL%, %null%, %null%, %null%, %NULL%, %NULL%, %nuLl%, \n"
                    L"%null%, %null%, %nUll%, %nuLl%, 'this is the default')",
        })
    {
        It begin(input.begin()), end(input.end());

        ast::Value result;
        bool ret = phrase_parse(begin, end, parser, qi::space, result);

        std::wcout << std::boolalpha << ret << ":\t" << result << std::endl;
    }
}

Теперь мы можем проверить синтаксический анализ, оценку и обработку ошибок:

Error! Expecting <list><expr_>"," here: ")"
false:  %null%
true:   simple
true:   99
true:   default
true:   -inf
true:   0.3
true:   3e-1
true:   5
true:   this is the first nonnull

Посмотри это в прямом эфире на Колиру

Без дополнительных тестовых случаев требуется около 77 строк кода, что составляет менее половины вашего исходного кода.


Полный код

Для дальнейшего использования

//#define BOOST_SPIRIT_DEBUG
#define BOOST_SPIRIT_USE_PHOENIX_V3
#include <boost/spirit/include/qi.hpp>
#include <boost/spirit/include/phoenix.hpp>
#include <boost/phoenix/function/adapt_function.hpp>

namespace qi  = boost::spirit::qi;
namespace phx = boost::phoenix;

namespace ast {
    typedef double       Number;
    typedef std::wstring String;
    struct  Null { 
        friend std::wostream& operator<<(std::wostream& os, Null) { return os << L"%null%"; } 
        friend std:: ostream& operator<<(std:: ostream& os, Null) { return os <<  "%null%"; } 
    };

    typedef boost::variant<Null, Number, String> Value;

    struct IsNull
    {
        typedef bool result_type;
        template <typename... T>
        constexpr result_type operator()(T const&...) const { return false; }
        constexpr result_type operator()(Null const&) const { return true;  }
    };

    Value Coalesce(std::vector<Value> const& arglist) {
        for (auto& v : arglist)
            if (!boost::apply_visitor(IsNull(), v))
                return v;
        //
        if (arglist.empty())
            return Value(Null());
        else
            return arglist.back(); // last is the default, even if Null
    }
}

BOOST_PHOENIX_ADAPT_FUNCTION(ast::Value, Coalesce_, ast::Coalesce, 1)

template <typename It, typename Skipper = qi::space_type >
struct MapFunctionParser : qi::grammar<It, ast::Value(), Skipper>
{
    MapFunctionParser() : MapFunctionParser::base_type(expr_)
    {
        using namespace qi;

        function_call_ = no_case[L"Coalesce"] 
            > ('(' > expr_ % ',' > ')') [ _val = Coalesce_(_1) ];

        string_ =   
                (L'"' > *~char_('"') > L'"')
            | (L"'" > *~char_("'") > L"'"); 

        number_ = double_;

        null_   = no_case["%null%"] > attr(ast::Null());

        factor_ = null_ | string_ | double_ | function_call_;

        expr_   = factor_;

        on_error<fail> (expr_, std::cout
            << phx::val("Error! Expecting ") << _4 << phx::val(" here: \"")
            << phx::construct<std::string>(_3, _2) << phx::val("\"\n"));

        BOOST_SPIRIT_DEBUG_NODES((expr_)(factor_)(null_)(string_)(number_)(function_call_))
    }

private:
    qi::rule<It, ast::String()>         string_;
    qi::rule<It, ast::Number()>         number_;
    qi::rule<It, ast::Null()>           null_;
    qi::rule<It, ast::Value(), Skipper> factor_, expr_, function_call_;
};

int main()
{
    typedef std::wstring::const_iterator It;
    MapFunctionParser<It, boost::spirit::qi::space_type> parser;

    for (std::wstring input : { 
            L"Coalesce()",
            L"Coalesce('simple')",
            L"CoALesce(99)",
            L"CoalESCe(%null%, 'default')",
            L"coalesce(%null%, -inf)",
            L"COALESCE(%null%, 3e-1)",
            L"Coalesce(%null%, \"3e-1\")",
            L"COALESCE(%null%, 5, 'default')",
            L"COALESCE(%NULL%, %null%, %nuLl%, %NULL%, %null%, %null%, %nUll%, %null%, %NULL%, %nUll%, %null%, \n"
                    L"%NULL%, %NULL%, %null%, %null%, %nUll%, %null%, %nUll%, %nULl%, %null%, %null%, %null%, \n"
                    L"%null%, %nUll%, %NULL%, %null%, %null%, %null%, %null%, %null%, %null%, %nUll%, %nulL%, \n"
                    L"%null%, %null%, %nUll%, %NULL%, 'this is the first nonnull',    %nUll%, %null%, %Null%, \n"
                    L"%NULL%, %null%, %null%, %null%, %NULL%, %null%, %null%, %null%, %NULL%, %NULL%, %nuLl%, \n"
                    L"%null%, %null%, %nUll%, %nuLl%, 'this is the default')",
        })
    {
        It begin(input.begin()), end(input.end());

        ast::Value result;
        bool ret = phrase_parse(begin, end, parser, qi::space, result);

        std::wcout << std::boolalpha << ret << ":\t" << result << std::endl;
    }
}

[1] Происхождение? http://c2.com/cgi/wiki?CodeSmell (может, это был Кент Бек?)

[2] Coalesce ссылаясь на соответствующие функции в некоторых языках программирования

Обновление: я только что обратился к другим проблемам во втором ответе. Код сократился до 77 строк, при этом он стал проще и надежнее (и отвечал на ваш вопрос тоже).

Прямой ответ на ваш вопрос о Фениксе: да

struct TakeOne
{
    template <typename...> struct result { typedef GenericValue type; };

    template <typename... Args> 
        GenericValue operator()(Args const&... args) const {
            return first(args...);
        }

private:
    GenericValue first(GenericValue const& def) const {
        return def;
    }
    template <typename... Args> GenericValue first(GenericValue const& def, GenericValue const& head, Args const&... tail) const {
        if (CheckIfGenericValIsNull(head)) 
            return first(def, tail...);
        else 
            return head;
    }
};

static const boost::phoenix::function<TakeOne> TakeOne_;

И он будет вести себя в основном так же (хотя вам нужно будет передать значение по умолчанию в качестве первого аргумента):

function_call_ = no_case[L"TakeOne"] > (
        ('(' > expr_ > ',' > expr_ > ')'                      ) [_val=TakeOne_(_2,_1)]
      | ('(' > expr_ > ',' > expr_ > ',' > expr_ > ')'        ) [_val=TakeOne_(_3,_1,_2)]
      | ('(' > expr_ > ',' > expr_ > ',' > expr_ > expr_ > ')') [_val=TakeOne_(_4,_1,_2,_3)]
      // ... etc
      );

Однако, как видите, он не такой гибкий! Variadics, вероятно, не то, что вы хотели, потому что variadics подразумевают статически известное количество аргументов. То, что зависит от входных данных (анализируемых) во время выполнения, никогда не может вписаться в категорию "статически известных". Итак, я бы предложил это:

    function_call_ = 
        no_case[L"TakeOne"] > 
        ('(' > expr_ % ',' > ')') [_val=TakeOne_(_1)];

Итак, вы передаете std::vector<GenericValue> вместо. Сейчас, TakeOne становится бризом:

GenericValue TakeOne(std::vector<GenericValue> const& arglist) {
    assert(!arglist.empty());
    for (auto& v : arglist)
        if (!CheckIfGenericValIsNull(v))
            return v;
    return arglist.back(); // last is the default
}
BOOST_PHOENIX_ADAPT_FUNCTION(GenericValue, TakeOne_, TakeOne, 1)

Бонус:

Чтобы упростить, вот посетитель переосмыслил:

struct IsNull : boost::static_visitor<bool> {
    bool operator()(double num) const { 
        return (num == NumValueDoubleNull);
    }
    bool operator()(std::wstring const& s) const { 
        return boost::iequals(s, ParserNullChar);
    }
};

GenericValue TakeOne(std::vector<GenericValue> const& arglist) {
    assert(!arglist.empty());
    for (auto& v : arglist)
        if (!boost::apply_visitor(IsNull(), v))
            return v;
    return arglist.back(); // last is the default
}
BOOST_PHOENIX_ADAPT_FUNCTION(GenericValue, TakeOne_, TakeOne, 1)

Одно только это исправление спасет вас ~51 LoC (112 против 163). Посмотреть это в прямом эфире на Coliru

Другие вопросы по тегам