openlang

Most of my macros turned into const and static

Nim makes macros easy to write. That's the problem.

Before reaching for macro, check the cheaper tools. A const is evaluated at compile time by the same VM that runs macros, so a lookup table built by an ordinary proc is just const table = buildTable(). A static: block runs code during compilation and generates nothing. Checks on config, sizes, string formats. Ordinary procs, called early.

Then templates. Plain substitution, no AST walking. Covers most tiny DSL wishes.

What's left for a real macro is syntax the other three can't express. Even then the macro only rearranges nodes. Logic that doesn't need NimNode lives in ordinary procs, and those I can test at runtime like anything else.

后续讨论 5

顺着彼此的依据读下去
  1. 固定链接

    Same advice holds in Common Lisp, and there the reason is redefinition. A macro that expands into a call to an ordinary function keeps its expansion small, and the function can be redefined and tested without touching any caller. If the logic sits inside the expander instead, every site that used the macro holds whatever the expander produced when that site was compiled. Changing the logic then means finding and recompiling those sites, and a reader of the program has no record of which ones they are.

  2. 固定链接

    Half agree. I live in C++, and there the template is both the macro and the generic type, so "keep it small" isn't really on offer. What you can do is move work out of it. Most old metaprogramming is now constexpr functions, which are just functions, loops and ifs included, evaluated at compile time when the context asks. consteval if you want to insist.

    Picking an overload by what a type can do got concepts. template <std::integral T> fails at the call site with a note about the unmet constraint instead of three screens of substitution failure from deep inside a library.

  3. 固定链接

    Agda is home turf for me, and there's no compile-time sublanguage there to keep small, which sounds great until you pay for it. Type checking normalizes terms, so compile-time evaluation is just evaluation, and anything total can run there. The price is that totality gets checked. The termination checker wants recursion on structurally smaller arguments, and a perfectly reasonable function that recurses on n / 2 is rejected until you rewrite it with well-founded recursion or a fuel argument. Honestly that rewrite can come out longer than the function.

    Agda does have macros, through reflection, and I almost never want one.

  4. 固定链接

    Worth counting what const table = buildTable() costs. The proc doesn't run once. It runs in the compile-time VM on every build of that module, and the VM is an interpreter, so a loop that finishes in a blink compiled can take a good while there. A table of a few hundred entries, nobody notices. A few million and you hit the VM's loop limit first, the error that tells you to raise --maxLoopIterationsVM. At that size I'd rather generate the table once into a file and pull it in with staticRead, or build it at startup and pay once per process.

同一个问题的其他角度

  1. foldl composes fine and leaks anyway

    foldl (+) 0 over a long list is the smallest space leak I know, and it's a composition problem more than an arithmetic one. E…

    回复 6最近活动 haskell
  2. Empty reductions return the identity

    Reduce over an empty vector. APL answers. Each primitive carries its identity. Zero for plus. One for times and for and. ⌈/⍬…

    回复 3最近活动 apl
  3. Returning from a Common Lisp handler means declining

    Neat abbreviation. I'd still like to see what it's agreeing to. The wrapper macro installs a handler around a parser, and the…

    回复 4最近活动 common-lisp