Unifying `serialize` and `to_string` #470

Closed
Neverlord opened this Issue Jun 13, 2016 · 1 comment

Comments

Projects
None yet
1 participant
@Neverlord
Member

Neverlord commented Jun 13, 2016

CAF is currently littered with redundant code to enable serialization and
string conversion for its custom types.

Current Approach

Currently, CAF requires serializable types to provide a free function
serialize. Any type convertible to string is required to provide a free
to_string function:

/// Stores a flow-control configuration.
struct named_actor_config {
  atom_value strategy;
  size_t low_watermark;
  size_t max_pending;
};

template <class Processor>
void serialize(Processor& proc, named_actor_config& x, const unsigned int) {
  proc & x.strategy;
  proc & x.low_watermark;
  proc & x.max_pending;
}

inline std::string to_string(const named_actor_config& x) {
  return "named_actor_config"
         + deep_to_string_as_tuple(x.strategy, x.low_watermark, x.max_pending);
}

Both of these function inspect an object and expose state to the caller.

Generalized Approach

When extending the current system with new type concepts such as parseable,
developers are required to implement one free function per concept per type.
This clearly does not scale. This proposal suggests a general-purpose reflect
function that allows a "mirror" object to visit state as well as meta
information. Different mirrors can ignore meta information in contexts where
they are not required. For example, a serializer can ignore meta information
that is always equal for all instances of an object such as class names.

The reflect function replacing serialize and to_string from the example
above would look as follows.

template <class Mirror>
void reflect(Mirror& mirror, named_actor_config& x, const unsigned int) {
  mirror & meta::type_name("named_actor_config");
  mirror & x.strategy;
  mirror & x.low_watermark;
  mirror & x.max_pending;
}

With:

namespace meta {

struct type_name { const char* value; };

} // namespace meta

Serializers should still fall back to serialize functions and prefer
serialize over reflect. First, to guarantee backward-compatibility as well
as compatibility to other frameworks such as Boost.Serialize. Second, because
some types cannot provide a general-purpose reflect function that is suitable
for serialization (for example actors).

Open Questions

  • What additional meta information do we need to support?
  • Can/should we inject std::ostream<< operators for types supporting reflect
@Neverlord

This comment has been minimized.

Show comment
Hide comment
@Neverlord

Neverlord Jun 17, 2016

Member

For future reference, this is the latest working draft with motivating examples after some discussion in chat.

namespace caf {
namespace meta {

// type tag
struct annotation { };

template <class T>
struct is_annotation {
  static constexpr value = std::is_base<annotation, T>::value;
};

struct type_name_t : annotation {
  const char* value;
};

type_name_t type_name(const char* cstr) {
  return {cstr};
}

template <class F>
struct store_callback_t : annotation {
  F fun;
};

template <class F>
store_callback_t store_callback(F fun) {
  return {fun};
}

template <class F>
struct load_callback_t : annotation {
  F fun;
};

template <class F>
load_callback_t load_callback(F fun) {
  return {fun};
}

struct omittable_if_empty_t : annotation { };

constexpr omittable_if_empty_t omittable_if_empty() {
  return {};
}

// ...

} // namespace meta
} // namespace caf

// example to-string-converter

class stringify_inspector {
public:
  stringify_inspector(std::string& result) : result_(result) {
    // nop
  }

  // end of recursion
  error operator()() {
    return {};
  }

  template <class T, class... Ts>
  error operator()(const meta::omittable_if_empty_t&, const T& x, const Ts&... xs) {
    if (! x.empty())
      (*this)(x);
    return (*this)(xs...);
  }

  template <class... Ts>
  error operator()(const meta::type_name_t& x, const Ts&... xs) {
    result += x.value();
    result += '(';
    stringify_inspector nested{result_};
    auto err = nested(xs...);
    if (! err)
      result += ')';
    return err;
  }

  template <class T, class... Ts>
  error operator()(const T& x, const Ts&... xs) {
    result += ...;
    return (*this)(xs...);
  }

private:
  std::string& result_;
};

// example type using omittable_if_empty

template <class inspector>
error inspect(inspector& inspector, error& err) {
  return inspector(meta::type_name("error"),
                err.category_,
                err.code_,
                meta::omittable_if_empty(),
                err.context_);
}

// advanced use case: actor serialization

actor load_from_registry(actor_id aid, node_id nid);
void store_in_registry(const actor&);

template <class inspector>
error inspect(inspector& inspector, actor& x) {
  auto aid = x.id();
  auto nid = x.node();
  auto load_fun = [&] {
    x = load_from_registry(aid, nid);
  };
  auto store_fun = [&] {
    store_in_registry(x);
  };
  return inspector(meta::type_name("actor"),
                aid,
                nid,
                meta::store_callback(store_fun),
                meta::load_callback(load_fun));
}
Member

Neverlord commented Jun 17, 2016

For future reference, this is the latest working draft with motivating examples after some discussion in chat.

namespace caf {
namespace meta {

// type tag
struct annotation { };

template <class T>
struct is_annotation {
  static constexpr value = std::is_base<annotation, T>::value;
};

struct type_name_t : annotation {
  const char* value;
};

type_name_t type_name(const char* cstr) {
  return {cstr};
}

template <class F>
struct store_callback_t : annotation {
  F fun;
};

template <class F>
store_callback_t store_callback(F fun) {
  return {fun};
}

template <class F>
struct load_callback_t : annotation {
  F fun;
};

template <class F>
load_callback_t load_callback(F fun) {
  return {fun};
}

struct omittable_if_empty_t : annotation { };

constexpr omittable_if_empty_t omittable_if_empty() {
  return {};
}

// ...

} // namespace meta
} // namespace caf

// example to-string-converter

class stringify_inspector {
public:
  stringify_inspector(std::string& result) : result_(result) {
    // nop
  }

  // end of recursion
  error operator()() {
    return {};
  }

  template <class T, class... Ts>
  error operator()(const meta::omittable_if_empty_t&, const T& x, const Ts&... xs) {
    if (! x.empty())
      (*this)(x);
    return (*this)(xs...);
  }

  template <class... Ts>
  error operator()(const meta::type_name_t& x, const Ts&... xs) {
    result += x.value();
    result += '(';
    stringify_inspector nested{result_};
    auto err = nested(xs...);
    if (! err)
      result += ')';
    return err;
  }

  template <class T, class... Ts>
  error operator()(const T& x, const Ts&... xs) {
    result += ...;
    return (*this)(xs...);
  }

private:
  std::string& result_;
};

// example type using omittable_if_empty

template <class inspector>
error inspect(inspector& inspector, error& err) {
  return inspector(meta::type_name("error"),
                err.category_,
                err.code_,
                meta::omittable_if_empty(),
                err.context_);
}

// advanced use case: actor serialization

actor load_from_registry(actor_id aid, node_id nid);
void store_in_registry(const actor&);

template <class inspector>
error inspect(inspector& inspector, actor& x) {
  auto aid = x.id();
  auto nid = x.node();
  auto load_fun = [&] {
    x = load_from_registry(aid, nid);
  };
  auto store_fun = [&] {
    store_in_registry(x);
  };
  return inspector(meta::type_name("actor"),
                aid,
                nid,
                meta::store_callback(store_fun),
                meta::load_callback(load_fun));
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment