Can high order function replace macro

The version of macro

(define-syntax (hyphen-define/ok1 stx)
    (syntax-case stx ()
      [(_ a b (args ...) body0 body ...)
       (syntax-case (datum->syntax #'a
                                   (string->symbol (format "~a-~a"
                                                           (syntax->datum #'a)
                                                           (syntax->datum #'b))))
                    ()
         [name #'(define (name args ...)
                   body0 body ...)])]))
 (hyphen-define/ok1 foo bar () #t)
 (foo-bar)

**The version of high order function **

(define (hyphen_define_ok2 a b . args)
  (define name (string-append a "-" b))
  (define (inner_func . args)
    #t)
  inner_func)

(define foo_bar (hyphen_define_ok2 "foo" "bar"))
(display (foo_bar))

why should I choose the macro rather that high order function

Hi, @WSWZhh.

Hmm, I don't think I understand the equivalence here (sorry, I can be slow!).

The macro is creating a hyphenated identifier, which can be used to refer to the execution of the particular body provided, but the higher-order function does not seem to do anything with the hyphenated name. Indeed, you have to "manually" provide an identifier, e.g. foo_bar, and the args in the inner_func do not refer to the same args from the outer definition.

Macros, from my limited mental model, are programs that write programs, so they expand the content of the macro into something else, or something more complex, which you would like to avoid having to specify by hand, or about which you would like to obtain certain guarantees, which manual specification might not avoid, or introduce.

In any event, nothing "magical" is going on with the macro, it simply allows us to deal with the code reflectively--i.e. the syntax--as opposed to the "data" realized by the syntax.

My first "aha!" moment with macros, was having to specify a cache for procedures, which would be very cumbersome to deal with manually for each procedure of which you would like to cache the results.

See, for example, this code by M. Butterick, which does exactly this.

Sorry, there is a slight error in the code.
I am too addicted to the macro and see it as a silver bullet,
In many cases, the function may be better

I see; it is rather a question of the sins of the macro, as opposed to its virtues.

Of course, there are probably many people more qualified than I to expound on this, but the stereotypical reply about the topic, is that macros are "opaque", in the sense that they appear to be just like any other expression, but secretly do things which the programmer might not expect or forget about (haha, sorry, future me).

At the end of the day, the macros are going to expand into normal code. So, a function created from a (custom) macro, and a function created using lambda, are both just functions. The macro is more powerful in what you can reflect upon, but this can (mostly) be done by hand with sufficient dependency injection (by which I mean "threading things through code.")

For me, it is a matter of taste, and some conscientiousness if others are to use your code.

See this gist for a probably terrible macro I am using for playing around with the Collatz conjecture. The idea for the macro came about from using the fabulous plot library, which allows for parameters to be specified either by explicit parameterization, or, by keyword arguments to a particular procedure.

Everything the macro does, can be done just the same by hand, but ye gods would I hate it to write out the keywords in the code for each procedure manually.

Each macro is a trade-off. As long as you feel that the benefits outweigh the downsides, I would say, go forth and macro.

Keep in mind it's not always either/or. Often it's best to use both:

  • Most of the work is done by a plain old runtime function.
  • A macro supplies something extra that can only be done at compile time (like some "syntactic sugar", or a binding form), expanding into code that calls the plain old function.

This division of labor can be good because:

  • often it is easier to understand and debug a plain old runtime function
  • complicated macros can be even harder to understand than complicated functions
  • a macro could expand into excessively repeated/"unrolled" code (e.g. repeating the contents of a function, vs. calling it repeatedly)

So I think it's healthy to be "suspicious" or "cautious" about overusing macros -- to think about how to limit them to doing only things that only they can do.

Sometimes that will be some pattern that's obvious from the start: "Oh, I should define a call-with-xxx function and a with-xxx macro that lets people avoid the lambda".

Sometimes you won't realize until the macro has gotten complicated, that you could/should refactor out parts of it into runtime functions. (Sometimes you'll realize this abstractly; sometimes you'll see it OMG from looking at the expansion.)


Those are a few quick thoughts from me. I'm glossing over entire subjects like macros that expand to other macros. The Racket core devs and overall community have probably more collective experience than anyone, in this area; so you'll probably get some better ideas.

7 Likes

| greghendershott
July 31 |

  • | - |

Keep in mind it's not always either/or. Often it's best to use both:

  • Most of the work is done by a plain old runtime function.
  • A macro supplies something extra that can only be done at compile time (like some "syntactic sugar", or a binding form), expanding into code that calls the plain old function.

Possibly a good example of this is command-line vs. parse-command-line. The command-line syntax is nicer to write and use at the cost of being static; with parse-command-line you can do fairly dynamic things.

For me, macros are about notation, especially repetitive patterns. As another example, while working on 763af9c (add editable summons to server, 2023-06-05), I noticed I was extracting certain values from a request repeatedly. I created a notation to allow me to define functions that extract those values from the request in the body (define/summon) which largely simplified working on the request handlers.

Of course macros shine for other purposes, too. Creating compile-time guarantees, like building Typed Racket or (much) smaller macros that can fail at compile time rather than runtime, is one such purpose.

Classic lisp wisdom is not to use a macro where a function will do. In the name of better notation, I sometimes ignore this wisdom in what others might call extreme ways.

I try to avoid bending hygiene too much, however, as the magical introduction of names can be hard to reason about.

5 Likes