Some macro systems can create variables in a loop for you. You could make this macro,
(define-all i 5 0)
;; creates i1 i2 i3 i4 i5 initialized to 0
That's somewhat impossible with functions. The closest you get is either an array/dict with only runtime error checking, or an external codegen program.
I wrote a post [0] about how to do this in Racket. The macro generates ORM code based given a SQLite DB. Aka the compiler queries SQLite and generates table-column functions automatically.
More potential benefits are: Better static error messages (can implement a type system using macros, example here[1]), and controlling execution order (can add lazy computation semantics).
no one is proposing macros as a paradigm of programming. they simply give programmers expressive powers not afforded by use of regular functions. in a sense you can compare macros to c++ templating. know what you are doing and use sparingly
Consider how ergonomic testing is thanks to macros: https://docs.julialang.org/en/v1/stdlib/Test/
Here's an example of passing quasi-json to a plotting function: https://www.queryverse.org/VegaLite.jl/stable/userguide/vlpl... . This lets you essentially transliterate a VegaLite spec into Julia without needing to translate it into Julia.
Finally, macros that operator on dataframes let you write code that looks kind of like SQL, and is much more pleasant than working with functions: https://dataframes.juliadata.org/stable/man/querying_framewo...