libmprompt icon indicating copy to clipboard operation
libmprompt copied to clipboard

Proposal: Refine the libmpeff's API with Metalang99

Open Hirrolot opened this issue 3 years ago • 0 comments

First of all, thank you for all your work on algebraic effects, especially libhandler and Koka -- for me, they have always been a strong source of inspiration and examples to follow.

In this issue, I propose using Metalang99 to clean up the API of libmpeff, as well as to improve its implementation.

Problem

Currently, the header file mpeff.h exposes the set of macros MPE_DECLARE_EFFECT(0-2) and MPE_DEFINE_EFFECT(0-7). Owing to the lack of iteration facilities of the C/C++ preprocessor, these are implemented by copy-pasting the same pattern with respect to a number of macro parameters. Needless to say that this approach leads to error-prone and hard-to-maintain code, and moreover, complicates the API: a user needs to specify the number of parameters like this: MPE_DEFINE_EFFECT5(...) and change the name as the number of parameters changes.

A similar problem is manual macro overloading on a number of parameters:

#define MPE_DECLARE_OP(effect, op) extern const struct mpe_optag_s MPE_OPTAG_DEF(effect, op);

#define MPE_DECLARE_OP0(effect, op, restype)                                                       \
    MPE_DECLARE_OP(effect, op)                                                                     \
    restype effect##_##op();

#define MPE_DECLARE_OP1(effect, op, restype, argtype)                                              \
    MPE_DECLARE_OP(effect, op)                                                                     \
    restype effect##_##op(argtype arg);

Besides MPE_DECLARE_OP, there are MPE_DECLARE_VOIDOP, MPE_DEFINE_OP, MPE_DEFINE_VOIDOP, MPE_WRAP_FUN, and MPE_WRAP_VOIDFUN.

Solution

We could rewrite these macros using Metalang99, a standard-conforming header-only library that augments the C99/C++11 preprocessor with extra metaprogramming facilities, including handy iteration patterns.

MPE_DECLARE_EFFECT

With the aid of Metalang99, MPE_DECLARE_EFFECT takes the following form:

#define MPE_DECLARE_EFFECT(...)                                                                    \
    extern const char *MPE_EFFECT(                                                                 \
        ML99_VARIADICS_GET(0)(__VA_ARGS__))[ML99_VARIADICS_COUNT(__VA_ARGS__) + 1];

That is it -- now you can specify an arbitrary amount of parameters (to be precise, up to 63) and it will work fine.

MPE_DEFINE_EFFECT

MPE_DEFINE_EFFECT would be slightly more complex:

Show the definition
#define MPE_DEFINE_EFFECT(...)                                                                     \
    ML99_IF(                                                                                       \
        ML99_VARIADICS_IS_SINGLE(__VA_ARGS__),                                                     \
        MPE_PRIV_DEFINE_EFFECT_0,                                                                  \
        MPE_PRIV_DEFINE_EFFECT_N)                                                                  \
    (__VA_ARGS__)

#define MPE_PRIV_DEFINE_EFFECT_0(effect) const char *MPE_EFFECT(effect)[2] = {#effect, NULL};

#define MPE_PRIV_DEFINE_EFFECT_N(effect, ...)                                                      \
    const char *MPE_EFFECT(effect)[ML99_VARIADICS_COUNT(__VA_ARGS__) + 2] = {                      \
        #effect,                                                                                   \
        ML99_EVAL(MPE_PRIV_opNameForEach(effect, __VA_ARGS__)) NULL};                              \
    ML99_EVAL(MPE_PRIV_defineEffectForEach(effect, __VA_ARGS__))

/*
 * #effect "/" #op1, ..., #effect "/" #opN,
 */
#define MPE_PRIV_opNameForEach(effect, ...)                                                        \
    ML99_variadicsForEach(ML99_appl(v(MPE_PRIV_opName), v(effect)), v(__VA_ARGS__))

#define MPE_PRIV_opName_IMPL(effect, op) v(#effect "/" #op, )

/*
 * const struct mpe_optag_s MPE_OPTAG_DEF(effect, op1) = { MPE_EFFECT(effect), 1 };
 * ...
 * const struct mpe_optag_s MPE_OPTAG_DEF(effect, opN) = { MPE_EFFECT(effect), N };
 */
#define MPE_PRIV_defineEffectForEach(effect, ...)                                                  \
    ML99_variadicsForEachI(ML99_appl(v(MPE_PRIV_defineEffect), v(effect)), v(__VA_ARGS__))

#define MPE_PRIV_defineEffect_IMPL(effect, op, i)                                                  \
    v(const struct mpe_optag_s MPE_OPTAG_DEF(effect, op) = {MPE_EFFECT(effect), i};)

#define MPE_PRIV_opName_ARITY       2
#define MPE_PRIV_defineEffect_ARITY 3

Macro overloading

MPE_DECLARE_OP and its friends could be defined as follows:

#define MPE_DECLARE_OP(effect, ...) ML99_OVERLOAD(MPE_PRIV_DECLARE_OP_, effect, __VA_ARGS__)

#define MPE_PRIV_DECLARE_OP_2(effect, op) extern const struct mpe_optag_s MPE_OPTAG_DEF(effect, op);

#define MPE_PRIV_DECLARE_OP_3(effect, op, restype)                                                 \
    MPE_PRIV_DECLARE_OP_2(effect, op)                                                              \
    restype effect##_##op();

#define MPE_PRIV_DECLARE_OP_4(effect, op, restype, argtype)                                        \
    MPE_PRIV_DECLARE_OP_2(effect, op)                                                              \
    restype effect##_##op(argtype arg);

In user code, just type MPE_DECLARE_OP(reader, ask, long) and it will overload automatically.

To see how it performs in practice, take a look at my branch. Just git clone https://github.com/Hirrolot/metalang99 to the root project directory and execute the usual build procedure.

Advantages

  • More convenient API.
  • Less error-prone, more maintainable implementation.

Disadvantages

  • A third-party dependency (though it is header-only).
  • -ftrack-macro-expansion=0 (GCC) or -fmacro-backtrace-limit=1 (Clang) should be specified because otherwise, compilation errors will go insane.

Final words

The aforementioned syntax is not a final result but just an example of how can we refine the API. In theory, the syntax could be even more concise. I will be willing to refactor the source code more if needed and can help with anything. Hopefully, this should not be tough, and the semantics should remain completely the same.

Metalang99 is already stable v1.x.y and it is very unlikely that it will significantly change in near future. It has been tested on Datatype99 and Interface99 with successful results.

I believe libmprompt has a bright future, please do not abandon it.

Hirrolot avatar Jul 01 '21 22:07 Hirrolot