Hacker News new | ask | show | jobs
by kazinator 250 days ago
The main problem that threading macros solve is the lack of implicit partial application. If you farm that problem to partial-applicative combinators, you can do it:

  This is the TXR Lisp interactive listener of TXR 302.
  Quit with :quit or Ctrl-D on an empty line. Ctrl-X ? for cheatsheet.
  1> (defun bind1 (fn a1) (lambda (a2) [fn a1 a2]))
  bind1
  2> (defun bind2 (fn a2) (lambda (a1) [fn a1 a2]))
  bind2
We define bind1 and bind2 for binding the left or right argument of a binary function producing a unary function, then:

  3> [chain [bind1 * 2] succ]
  #<intrinsic fun: 0 param + variadic>
  4> [*3 10]
  21
The only wart is you have that explicit bind1.

You can write functions that notice they have fewer arguments than ostensibly required and partially apply themselves. Obviously the standard * can't do that because it's usefully variadic already.

Anyway, in the implementation of TXR, I've done this kind of thing in C, just with function calls: no macros. E.g. in eval.c, certain functions are prepared that the quasiquote expander uses:

static val consp_f, second_f, list_form_p_f, quote_form_p_f; static val xform_listed_quote_f;

  static void qquote_init(void)
  {
    val eq_to_list_f = pa_12_1(eq_f, list_s);
    val eq_to_quote_f = pa_12_1(eq_f, quote_s);
    val cons_f = func_n2(cons);

    protect(&consp_f, &second_f, &list_form_p_f,
            &quote_form_p_f, &xform_listed_quote_f, convert(val *, 0));

    eq_to_list_f = pa_12_1(eq_f, list_s);
    consp_f = func_n1(consp);
    second_f = func_n1(second);
    list_form_p_f = andf(consp_f,
                         chain(car_f, eq_to_list_f, nao),
                         nao);
    quote_form_p_f = andf(consp_f,
                          chain(cdr_f, consp_f, nao),
                          chain(cdr_f, cdr_f, null_f, nao),
                          chain(car_f, eq_to_quote_f, nao),
                          nao);
    xform_listed_quote_f = iffi(andf(consp_f,
                                     chain(car_f, eq_to_list_f, nao),
                                     chain(cdr_f, consp_f, nao),
                                     chain(cdr_f, cdr_f, null_f, nao),
                                     chain(cdr_f, car_f, consp_f, nao),
                                     chain(cdr_f, car_f, car_f, eq_to_quote_f, nao),
                                     nao),
                                chain(cdr_f, car_f, cdr_f,
                                      pa_12_1(cons_f, nil),
                                      pa_12_2(cons_f, quote_s),
                                      nao),
                                nil);
  }
"nao" means "not an object": it's a sentinel value used internally in the runt-time for various purposes, the most common of them being the termination of variadic argument lists.

andf is an and combinator: it returns a function which passes its argument(s) to each function in turn; if anything returns nil, it stops calling functions and returns nil. Otherwise it returns the value of the last function.

The pa_this_that functions are partial applicator combinators, generalizations of bind1 and bind2. E.g. pa_12_1 means take a fucntion with arguments 1 2, returning a function which just takes 1 (so 2 is bound: this is like bind2). A bunch of these exist, and more coud be added if needed: pa_1234_1, pa_1234_34 pa_123_1, pa_123_2, pa_123_3, pa_12_1 and pa_12_2.