Convert complex ChaiScript types (e.g. Map of Map) to user-defined types


#1

Hello,

I started experimenting with ChaiScript a couple of days ago and I’m having some troubles to convert a Map of Maps of ChaiScript to a C++'s map<string, map<string, string>>.

To give you some more context: I’m working in the refactoring of a framework of probabilistic models used in bioinformatics. In this system, we have a small language to describe parameters of these probabilistic models. Because of some extensions our research group is developing, we’ll need this language have functions that could be used inside C++. Our first though was to create a wrapper of our framework in some script language (Ruby, Lua, Python, etc.). Then, we discovered ChaiScript, with its very nice binding with std::function.

For back compatibility + usability reasons, our DSL can’t be too similar to a programming language (it’s meant to be used by biologists, with little or no knowledge of programming). However, ChaiScript’s syntax of arrays and maps is very similar to our old language. To get rid of var and auto from the scripts, we need only to provide the valid variables from a C++ struct to Chai. Then, the user can assign them and we can use them in the library.

Here is a minimal example of what I’m saying:

// Standard headers
#include <map>
#include <string>
#include <iostream>

// External headers
#include <chaiscript/chaiscript.hpp>

struct Config {
  std::string model_type;
  std::map<std::string, std::map<std::string, std::string>> states;
};

int main() {
  using namespace chaiscript;
  ChaiScript chai;

  chai.add(fun([] (std::map<std::string, std::map<std::string, std::string>> &conv,
                   const std::map<std::string, Boxed_Value> &orig) {
    for (const auto &pair : orig)
      conv[pair.first] = boxed_cast<std::map<std::string, std::string>>(pair.second);
  }), "=");

  Config cfg;
  chai.add(var(&cfg.model_type), "model_type");
  chai.add(var(&cfg.states), "states");

  chai.eval(R"(
    model_type = "Gene"

    states = [
          "Coding"    : [ "duration": "geometric", "transition": "iid" ],
          "NonCoding" : [ "duration": "geometric", "transition": "iid" ]
        ]
  )");

  return 0;
}

As you can see, I wrote a lambda to overload the assignment operator within Chai, making it possible to convert the Map of Maps in a std::map<std::string, std::map<std::string, std::string>>.

This example compiles, but when I run it, it throws the following error:

terminate called after throwing an instance of 'chaiscript::exception::eval_error'
  what():  Error: "Unable to find appropriate'=' operator." With parameters: (St3mapISsS_ISsSsSt4lessISsESaISt4pairIKSsSsEEES1_SaIS2_IS3_S6_EEE, const Map)

c++filt confirms that this weird type name is the type I wrote in the lambda signature.

Curious fact: when I comment the body of the lambda, the example runs with no problem.

Reading other threads, I saw that there is no direct conversion for container types using boxed_cast, so I imagine this is the reason why my code fails.

Given all this explanation, I have two questions:

  • Is there any simple way of converting a Map of Maps as this one I need? I do not expect a single line to solve my problem (I read about of the compilation / runtime cost of enabling container conversions), but at least I’d like to convert Boxed_Values in the most localized way possible (e.g. within that lambda).

  • Wouldn’t be possible to create a better error for this case? Perhaps warning the user of its invalid / unsupported conversion through a static_assert or so. I’m not quite sure if there is any standard type trait that would make this verification easy, but at least it would readly show what I believe it’s the right error I’m having in this example.


#2

Hi,

Is there any kind of solution for this problem? Maps of Maps are an essential feature for me, but I can’t see any way to convert a Boxed_Value to a Map, and then convert it to another std::map type?

Thanks


#3

The native C++ type for a Map object is std::map<std::string, Boxed_Value>, so your current code could become something like:

chai.add(fun([] (std::map<std::string, std::map<std::string, std::string>> &conv,
                   const std::map<std::string, Boxed_Value> &orig) {
    for (const auto &pair : orig) {
      for (const auto &deep_pair : boxed_cast<const std::map<std::string, Boxed_Value> &>(pair.second)) {
        conf[pair.first][deep_pair.first] = boxed_cast<std::string>(deep_pair.second);
      }
  }), "=");

The best solution would be to add in another type conversion helper like the one we have for vectors here:

It wouldn’t be difficult, but I don’t have the time at the moment to implement it myself at the moment.


#4

Thanks for your time, @lefticus

I tried something very similar to your first code snippet, but for some reason it doesn’t work. This is the error I receive when I try to run it:

erminate called after throwing an instance of 'chaiscript::exception::eval_error'
  what():  Error: "Unable to find appropriate'=' operator." With parameters: (St3mapINSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEES_IS5_S5_St4lessIS5_ESaISt4pairIKS5_S5_EEES7_SaIS8_IS9_SC_EEE, const Map)

As I wrote in my first post, c++filt translates that this mangled name translates to std::map<std::string, std::map<std::string, std::string>>, so it makes no sense that chaiscript can’t find the right type.

Now about the second solution, interestingly I found out vector_conversion a couple days ago and tried to implement exactly what you suggested. Here it is my map_conversion :

template<typename To>
  Type_Conversion map_conversion()
  {
    auto func = [](const Boxed_Value &t_bv) -> Boxed_Value {
      const std::map<std::string, Boxed_Value> &from_map = detail::Cast_Helper<const std::map<std::string, Boxed_Value> &>::cast(t_bv, nullptr);

      To map;

      for (const std::pair<std::string, Boxed_Value> &pair : from_map) {
        map[pair.first]
          = detail::Cast_Helper<typename To::mapped_type>::cast(pair.second, nullptr);
      }

      return Boxed_Value(std::move(map));
    };

    return chaiscript::make_shared<detail::Type_Conversion_Base, detail::Type_Conversion_Impl<decltype(func)>>(user_type<std::map<std::string, Boxed_Value>>(), user_type<To>(), func);
  }

But I think I don’t know how to use it properly. When could I apply it? Just use it with boxed_cast, in my tests, didn’t work. Looking here:

It seems I can use it with eval, but how about other places?

Thanks again for the attention!


#5

I really wish I had more time to spend on this right now, but I won’t probably until Monday.

That said, I have a hunch that your issue is how you are using boxed_cast in your test. Specifically:

// a call like this does not have access to the registered conversions
boxed_cast<std::map<std::string, std::string>>(obj);
// a call like this *does*
chai.boxed_cast<std::map<std::string, std::string>>(obj);

If you can make a fork with a branch on github with your version of map_conversion plus some expected tests similar to the vector_conversion that you pointed at, that would help me be able to help you much faster.

-Jason


#6

@lefticus,

At the end, I realized boxed_cast was working. Your implementation, by the way, worked perfectly. The problem was in the example I was using: it had different types in values of Map, so boxed_value was failing to make its cast… At the time, I couldn’t create the tests for the new feature. Recently, I saw you added this helper to the last version of ChaiSript, and it was very useful for my implementation. Thanks again for the effort of creating this awesome and useful language.

Thanks for the help and for the effort of building a useful language as ChaiScript!

Renato