Partial application and function composition

Metalang99 supports partial application -- a technique that allows applying arguments to functions separately, not all-at-once. Consider this:

#define F_IMPL(x, y, z) v(x + y + z)
#define F_ARITY         3

// 1 + 2 + 3
ML99_EVAL(ML99_appl(ML99_appl(ML99_appl(v(F), v(1)), v(2)), v(3)))

Alternatively, you can write ML99_appl2(ML99_appl(F, v(1)), v(2), v(3)) or ML99_appl3(F, v(1), v(2), v(3)). It works as follows: first, F is applied to v(1), then its arity specifier F_ARITY (e.g., the number of parameters it accepts) is decremented; the same happens with v(2) and v(3). The point here is that each application is already a valid Metalang99 term: you can pass it to higher-order metafunctions, which is quite handy with lists:

// 4 5 6
ML99_LIST_EVAL(ML99_listMap(ML99_appl(v(ML99_add), v(3)), ML99_list(v(1, 2, 3))))
  • ML99_appl(v(ML99_add), v(3)) is a term representing a function that accepts a single natural number and increments it by 3.

  • ML99_listMap maps each item in our list with this function.

  • ML99_LIST_EVAL evaluates the list and pastes all resulting items sequentially.

Without partial application, the situation becomes rather clumsy:

#define ADD_3_IMPL(x) ML99_add(v(x), v(3))
#define ADD_3_ARITY   1

// 4 5 6
ML99_LIST_EVAL(ML99_listMap(v(ADD_3), ML99_list(v(1, 2, 3))))

Partial application is especially useful when you want to capture an environment into a closure. With ML99_appl, it becomes remarkably convenient:

#define F_IMPL(x, y) v(x##_##y)
#define F_ARITY      2

// abc_1 abc_2 abc_3
ML99_EVAL(ML99_variadicsForEach(ML99_appl(v(F), v(abc)), v(1, 2, 3)))

Read more about partial application here.

Another neat functional facility is function composition. Put simply, ML99_compose(f, g) returns a new metafunction that is identical to first invoking g with user-provided arguments, and then invoking f with the result of invoking g. Take a look at the following macro:

#define TRANSFORM(...)                                                                             \
    ML99_EVAL(ML99_variadicsForEach(ML99_compose(v(ML99_braced), v(ML99_untuple)), v(__VA_ARGS__)))

// { int x; }
// { const char *x, y; }
TRANSFORM((int x;), (const char *x, y;))

Here, ML99_braced is composed with ML99_untuple:

  • ML99_untuple removes brackets from provided arguments: (1, 2, 3) => 1, 2, 3.

  • ML99_braced wraps arguments into curly braces: 1, 2, 3 => { 1, 2, 3 }.

  • Overall, ML99_compose(v(ML99_braced), v(ML99_untuple)) results in (1, 2, 3) => { 1, 2, 3}.

Then this function composition is supplied to ML99_variadicsForEach to achieve the final result.

Partial application and function composition make metafunctions more reusable, which is the essence of functional programming. All higher-order metafunctions in the Metalang99 standard library use partial application under the hood, so you can pass to them both "raw" function-like macro identifiers such as v(F) and partially applied functions as well. Any Metalang99-compliant macro with an arity specifier can be partially applied, and the operation is very cheap.

Last updated