January 6, 2009, Tuesday, 5

Article:RuleBasedSpirit

From IdeA thinKING

Jump to: navigation, search

Contents

Rule-based Spirit 사용법

이번 강좌에서는 복잡한 문법을 parsing할 경우에도 사용할 수 있는 rule-based spirit 사용법에 대해 알아보겠습니다.

예제

먼저 이번장에서는 간단한 메신저 프로그램에서 사용할 수 있는 파일 형식을 만든다고 가정합니다. 내부적으로 사용하는 데이터 구조는 다음과 같습니다.

enum status_e
{
  SIGNOUT,
  BUSY,
  AVAILABLE  
};
 
struct contact_t
{
  std::string id;
  std::string name;
  std::string email;
  std::string ip4;
  status_e    sts;
};

이 데이터 구조를 저장하기 위해 아래와 같은 파일 형식을 사용합니다.

 iwongu = { iwongu@abc.com, "Lee Wongoo", LOGOUT,  };
 gdhong = { gdhong@foo.bar.com, "Hong Gilldong", BUSY, 127.0.0.1:5060 };

물론 아래와 같이 줄을 바꾸거나 마음대로 띄워쓰기도 가능합니다.

 iwongu = {
   iwongu@abc.com,
   "Lee Wongoo",
   LOGOUT,
 };
 
 gdhong = {
   gdhong@foo.bar.com,
   "Hong Gilldong",
   BUSY,
   127.0.0.1:5060
 };

EBNF 정의

위에서 정의한 문법을 EBNF로 표현해 보면 다음과 같습니다. (밑에서부터 읽으면 편합니다.)

 digit        ::= [0-9]
 alpha        ::= [a-zA-Z]
 alnum        ::= alpha | digit
 status       ::= alpha+
 port         ::= ':' digit+
 ip4          ::= digit+ '.' digit+ '.' digit+ '.' digit+ [port]
 domainName   ::= ip4 | domain
 domain       ::= nameString
 nameString   ::= (alpha) (alnum | '-' | '.')*
 quotedString ::= '"' (anychar - '"')* '";
 name         ::= quotedString
 email        ::= nameString '@' domainName
 id           ::= nameString
 contact      ::= id '=' '{' email ',' name ',' status ',' [ip4] '}' ';'
 contacts     ::= contact*

참고로 위의 EBNF는 실제로 domain이나 ip4 형식을 정확히 parsing 하지는 않습니다. 예제를 위해 필요한 정도로만 정확합니다.


위의 문법을 inline spirit으로 표현한다면 아마 매우 복잡하고 알아보기 어려운 코드가 될 것입니다. 따라서 이 문법을 boost::spirit의 grammar와 rule 클래스를 사용하여 보기 쉽게 작성해보겠습니다.

boost::spirit::grammar, rule

먼저 다음과 같이 grammar 클래스를 상속받는 클래스를 선언합니다.

struct contact_list : public boost::spirit::grammar<contact_list>
{
  template <typename ScannerT>
  struct definition
  {
    definition(contact_list const& self)
    {
      using namespace boost::spirit;
      ... (1)
    }
 
    boost::spirit::rule<ScannerT> contacts, ... (2);
 
    boost::spirit::rule<ScannerT> const& start() const {
      return contacts;
    }
  };
};

여기서 start()함수는 parser가 parsing을 시작할 rule을 지정합니다. 위의 코드에서는 parser가 contacts라는 rule을 가지고 parsing을 시작하도록 하고 있습니다.

그럼 위의 EBNF 형식을 가지고 ...(1)번 부분을 구현해보면 다음과 같습니다.

status       = +alpha_p;
port         = ch_p(':') >> +digit_p;
ip4          = +digit_p >> '.' >> +digit_p >> '.' >> +digit_p >> '.' >> +digit_p >> !port;
domainName   = ip4 | domain;
domain       = nameString;
nameString   = alpha_p >> *(alnum_p | '-' | '.');
quotedString = '"' >> *(~ch_p('"')) >> '"';
name         = quotedString;
email        = nameString >> '@' >> domainName;
id           = nameString;
contact      = id >> '=' >> '{' >> email >> ',' >> name >> ',' >> status >> ',' >> !ip4 >> '}' >> ';';
contacts     = *contact;

위의 spirit 구문에서 ! 기호는 optional 을 나타냅니다.

마지막으로 ...(2)번 항목에 위에서 사용한 rule들의 이름을 선언합니다.

boost::spirit::rule<ScannerT> contacts, contact, id, email, name,
                              quotedString, nameString, domain, domainName,
                              ip4, port, status;

이것으로 parser는 완성입니다. 생각보다 쉽죠? 이제 이 parsing한 결과를 이용하기 위해 action들을 사용할 차례입니다.

잠시... 사소한 개선 사항들

action들을 사용하기에 앞서 위의 rule들을 confix_p와 c_escape_ch_p를 사용하여 조금 개선해봅시다. 먼저 confix_p는 opening, expression, closing으로 이루어진 문법을 위해 사용합니다. 예를 들어 "( ... )"나 "/* ... */" 등과 같은 문법을 위해 사용합니다. confix_p를 사용하는 것이 사용하지 않는 것과 어떤 차이가 있는지는 [1]를 참고하세요.

위의 경우 이 문법에 해당하는 경우가 두 군데 있습니다. 하나는 contact를 정의하기 위해 사용된 '{', '}' 문법이고 나머지 하나는 quotedString을 정의하기 위해 사용된 '"', string, '"' 부분입니다.

이를 confix_p를 사용하여 다시 정의하면 다음과 같습니다.

quotedString = confix_p('"', (*c_escape_ch_p), '"');
contact      = id >> '=' >> confix_p('{', email >> ',' >> name >> ',' >> status >> ',' >> !ip4, '}') >> ';';

여기서 c_escape_ch_p는 C언어에서 사용되는 것과 같은 문자열 escape문자를 인식하여 처리해줍니다. [2]

actions

이제 parsing 결과를 action과 연결시킬 차례입니다. 여기서도 inline usage와 같이 바로 assign_a, push_back_a와 같은 action을 바로 연결시킬 수 있습니다. 하지만 이렇게 문법안에 action이 hard-coding되면 애써 작성한 문법의 재사용이 불가능해집니다.

따라서 action을 정의하는 클래스를 인자로 받아들일 수 있는 문법을 정의하여 재사용성을 높여 보겠습니다.

먼저 action 클래스를 인자로 받아야 하므로 contact_list 선언을 template을 사용하여 수정합니다. 그리고 action 객체를 reference로 받아 저장하는 생성자를 다음과 같이 선언합니다.

template <class Action>
struct contact_list : public boost::spirit::grammar<contact_list<Action> >
{
  contact_list(Action& action_) : action(action_)
  {
  }
 
  template <typename ScannerT>
  struct definition
  {
    definition(contact_list const& self)
    {
      using namespace boost::spirit;
 
      Action& action = self.action;
      ...
    }
    ...
  };
 
  Action& action;
};

그리고 아래 코드처럼 필요한 부분에 적당히 action을 호출하는 코드를 넣어줍니다.

contact      = id[action.id] >> '='
               >> confix_p(
               '{',
                  email[action.email] >> ','
                  >> name[action.name] >> ','
                  >> status[action.status] >> ','
                  >> !ip4[action.ip4],
               '}')
               >> ';';
contacts     = *contact[action.contact];

물론 위의 parser를 사용하려면 위의 문법에 맞도록 action 클래스를 만들어주어야 합니다. 위의 코드를 살펴 보면 위의 문법과 같이 사용할 action 클래스는 id, email, name, status, ip4, 그리고 contact이라는 action을 제공해야 함을 알 수 있습니다.

그럼 첫번째로 파일을 읽어 줄을 맞춰주는 간단한 beautifier action 클래스를 만들어보겠습니다.

beautifier

이번에 만들 beautifier action 클래스는 파일 안의 내용이 어떤 들여쓰기나 newline을 사용하더라도 각줄에 한 항목씩 들어가도록 줄을 맞춰주는 action 클래스입니다.

즉, 위쪽의 파일 예제의 첫번째 형식을 입력받아 두번째 형식으로 출력해주는 action입니다.

먼저 contact_list grammar에서 요구하는 action은 id, email, name, status, ip4, contact, 이렇게 여섯개이므로 이 여섯개 action의 틀을 구현합니다. rule에 대한 action은 입력된 문자열에서 현재 parsing된 부분을 iterator쌍으로 받게 되며 다음과 같이 작성하면 됩니다.

struct beautifier
{
  struct id_a
  {
    template <typename IteratorT>
    void operator()(IteratorT first, IteratorT last) const {
    }
  } id;
 
  struct email_a
  {
    template <typename IteratorT>
    void operator()(IteratorT first, IteratorT last) const {
    }
  } email;
  ...
};

그럼 이 틀에 실제 beautifier의 기능을 할 수 있는 코드를 넣어보겠습니다. 여기서는 코드를 간단히 하기 위해 결과를 std::cout으로 출력하도록 하였습니다.

struct beautifier
{
  struct id_a
  {
    template <typename IteratorT>
    void operator()(IteratorT first, IteratorT last) const {
      std::cout << std::string(first, last) << " = {\n";
    }
  } id;
 
  struct email_a
  {
    template <typename IteratorT>
    void operator()(IteratorT first, IteratorT last) const {
      std::cout << '\t' << std::string(first, last) << ",\n";
    }
  } email;
 
  ... name, status, ip4의 구현은 email과 같습니다.
 
  struct contact_a
  {
    template <typename IteratorT>
    void operator()(IteratorT first, IteratorT last) const {
      std::cout << "};\n";
    }
  } contact;
};

위의 action들중 email_a에서 ip4_a까지는 모두 같은 일을 합니다. 따라서 이 경우 하나의 action을 가지고 다음과 같이 간단히 구현할 수 있습니다.

struct beautifier
{
  struct id_a
  {
    template <typename IteratorT>
    void operator()(IteratorT first, IteratorT last) const {
      std::cout << std::string(first, last) << " = {\n";
    }
  } id;
 
  struct comma_a
  {
    template <typename IteratorT>
    void operator()(IteratorT first, IteratorT last) const {
      std::cout << '\t' << std::string(first, last) << ",\n";
    }
  } email, name, status, ip4;
 
  struct contact_a
  {
    template <typename IteratorT>
    void operator()(IteratorT first, IteratorT last) const {
      std::cout << "};\n";
    }
  } contact;
};

컴파일 후 실행시키면 어떤 형식으로 파일을 작성했건 문법에 맞게만 작성했다면 다음과 같은 형식으로 변경되어 출력됩니다.

 iwongu = {
         iwongu@abc.com,
         "Lee Wongoo",
         SIGNOUT,
 };
 gdhong = {
         gdhong@foo.bar.com,
         "Hong Gilldong",
         BUSY,
         127.0.0.1:5060,
 };

Symbol table

Symbol table은 특정 symbol과 그와 관련된 값을 가지고 있는 테이블입니다. Spirit에서는 dynamic symbol table를 지원하여 parsing도중에 동적으로 symbol을 추가할 수도 있습니다.

처음에 살펴보았듯이 예제에는 status_e라는 enum 값이 정의되어 있습니다. 하지만 현재 parser에서는 이 enum값을 parsing해주는 것이 아니라 단순히 문자열로써 인식하고 있습니다. 만약 이 enum 값을 parser가 인식하여 enum값으로 변경하여 전달해준다면 사용자는 더욱 쉽게 parser를 사용할 수 있게 될 것입니다.

따라서 이 enum 값을 symbol table을 사용하여 인식하도록 해보겠습니다. 여기서는 symbol table의 동적인 관리는 사용하지 않습니다.

먼저 다음과 같이 symbol table을 정의합니다. 보시는 바와 같이 spirit의 symbols 클래스를 사용합니다.

struct statusST : boost::spirit::symbols<status_e>
{
  statusST()
  {
    add
      ("SIGNOUT"   , SIGNOUT)
      ("BUSY"      , BUSY)
      ("AVAILABLE" , AVAILABLE)
      ;
  }
};

여기서는 생성자에서 각 symbol을 적당한 enum 값과 정적으로 연결시킵니다.

다음으로 문법 부분에서 기존의 status 부분을 다음과 같이 변경합니다.

template <class Action>
struct contact_list : public boost::spirit::grammar<contact_list<Action> >
{
  ...
  template <typename ScannerT>
  struct definition
  {
    definition(contact_list const& self)
    {
      ...
      contact      = id[action.id] >> '='
                     >> confix_p(
                     '{',
                       email[action.email] >> ','
                       >> name[action.name] >> ','
                       >> status_p[action.status] >> ','
                       >> !ip4[action.ip4],
                     '}')
                     >> ';';
      ...
    }
 
    statusST status_p;
    ...
  };
  ...
};

다음으로 symbol table에 matching되는 action의 타입은 연결된 값을 받도록 구현해야 합니다. 따라서 beautifier의 status를 다음과 같이 수정합니다.

struct beautifier
{
  ...
  struct status_a
  {
    void operator()(status_e sts) const {
      std::cout << '\t' << status2str(sts) << ",\n";
    }
  } status;
  ...
};

여기서 status2str() 함수는 enum 값을 문자열로 바꿔주는 helper function입니다.

이렇게 symbol table을 사용함으로써 얻을 수 있는 장점중의 하나는 각 symbol들의 약어들을 사용하도록 쉽게 확장할 수 있다는 것입니다.

예를 들어 symbol table에 아래와 같은 entry를 추가하면 다음과 같은 파일 형식도 인식할 수 있습니다.

struct statusST : boost::spirit::symbols<status_e>
{
  statusST()
  {
    add
      ...
      ("S/O"   , SIGNOUT)
      ("B/S"      , BUSY)
      ("A/V" , AVAILABLE)
      ;
  }
};
 iwongu = { iwongu@abc.com, "Lee Wongoo", S/O,  };
 gdhong = { gdhong@foo.bar.com, "Hong Gilldong", B/S, 127.0.0.1:5060 };

위 파일을 beautifier로 parsing한 결과는 위에서 나온 결과와 같습니다.

contact_callback

그럼 마지막으로 위에서 만든 문법을 가지고 beautifier 외에 다른 action클래스를 만들어봄으로써 문법의 재사용성을 확인해 보겠습니다.

이번에는 파일을 parsing하면서 하나의 항목을 인식하면 인식한 결과를 contact_t 객체에 담아 이를 인자로 action 클래스에 사용자가 등록한 callback함수를 호출하는 클래스를 만들어보겠습니다.

먼저 사용자가 callback 함수를 등록할 수 있도록 하는 action 클래스는 다음과 같습니다. 여기서는 callback 함수의 타입으로 boost::function을 사용하였습니다. [3] 그리고 사용한 컴파일러가 preferred syntax를 지워하지 않아 portable syntax를 사용했습니다. [4]

struct contact_callback
{
  typedef boost::function1<void, const contact_t&> callback_t;
 
  contact_callback(callback_t cb_) : cb(cb_),
				     id(data.id),
				     email(data.email),
				     name(data.name),
				     ip4(data.ip4),
				     status(data.sts),
				     contact(cb, data) {
  }
 
  struct assign_a
  {
    assign_a(std::string& ref_) : ref(ref_) {
    }
 
    template <typename IteratorT>
    void operator()(IteratorT first, IteratorT last) const {
      ref = std::string(first, last);
    }
 
    std::string& ref;
  };
 
  struct status_a
  {
    status_a(status_e& ref_) : ref(ref_) {
    }
 
    void operator()(status_e sts) const {
      ref = sts;
    }
 
    status_e& ref;
  };
 
  struct contact_a
  {
    contact_a(callback_t cb_, contact_t& data_) : cb(cb_), data(data_) {
    }
 
    template <typename IteratorT>
    void operator()(IteratorT first, IteratorT last) const {
      cb(data);
    }
   
    callback_t cb;
    contact_t& data;
  };
 
  callback_t cb;
 
  contact_t data;
 
  assign_a id, email, name, ip4;
  status_a status;
  contact_a contact;
};

먼저 생성자에서는 사용자가 등록한 callback함수를 저장하고 id와 같은 action들을 멤버 변수인 data의 적당한 필드와 연결시켜주는 작업을 합니다. 이때 한가지 신경써야 할 점은 contact의 초기화시에 멤버 변수 cb가 사용되므로 이 cb가 먼저 초기화된 후 contact 이 초기화되어야 한다는 점입니다. 즉, struct 선언시에 cb가 contact보다 윗쪽에 위치해야 합니다. [5]

마지막으로 contact_a action은 저장된 data 객체를 가지고 callback 함수를 호출합니다.

그럼 이 action 클래스를 사용하는 callback 함수를 작성해 보겠습니다. 아래의 callback 함수가 하는 일은 위에서 살펴본 beautifier 과 같습니다. 다만 이전의 beautifier는 각 action 들에서 맡은 부분만을 출력했지만 이번 callback 함수는 contact_t 객체를 받아 이를 출력한다는 점이 다릅니다.

struct callback
{
  void operator()(const contact_t& ct) {
    std::cout << ct.id << " = {\n\t"
 	      << ct.email << ",\n\t" 
	      << ct.name << ",\n\t"
	      << status2str(ct.sts) << ",\n\t"
	      << ct.ip4 << "\n"
	      << "}\n";
  }
};

마지막으로 위의 callback함수를 action 클래스에 등록한 후 이 action 클래스를 가지고 parsing을 합니다. 물론 결과는 위의 beautifier 결과와 동일하게 출력됩니다.

bool do_parse(const char* buff)
{
  using namespace std;
  using namespace boost::spirit;
 
  contact_callback::callback_t f = callback();
  contact_callback action(f);
  contact_list<contact_callback> parser(action);
  parse_info<> info = parse(buff, parser, space_p);
  if (!info.full) {
    cout << ">> parse error" << endl;
    cout << info.full << endl;
    cout << info.hit << endl;
    cout << info.length << endl;
    cout << info.stop << endl;
  }
 
  return info.full;
}

결론

이번 강좌에서는 boost::spirit의 사용법에 대해 살펴보았습니다. 처음 접근하기에는 조금 난이도가 있으나 조금만 익숙해지면 여러 상황에서 유용하게 사용할 수 있습니다.

이번에 살펴본 내용은 boost::spirit의 사용법의 일부로 더욱 많은 내용이 Spirit|User's Guide에 있습니다. 예를 들어 lambda expression을 구현한 Phoenix는 간단한 action을 만드는데 편리하게 사용할 수 있습니다.

그럼 이것으로 boost::spirit 강좌를 마치겠습니다. 긴 글 읽어주셔서 감사합니다.