Hacker News new | past | comments | ask | show | jobs | submit login
Comparative Macrology (wilfred.me.uk)
44 points by luu on Sept 15, 2014 | hide | past | web | favorite | 11 comments



F-expressions are ancient (1960's era Lisp). newLisp dug them out from a Lisp landfill of discarded cruft; they do not chronologically belong in 1991. Tcl has them too, incidentally. That is to say, Tcl allows for user-defined interpretive operators that are very much like fexprs. Bash's eval can almost do something like this; an eval sees the surrounding scope. What's missing is the sugar to hide the eval.

"Text based fexpr":

    gen_swap_code()
    {
       echo "local tmp=\$$1;"
       echo "$1=\$$2;"
       echo "$2=\$tmp;"
    }

    fun()
    {
       local a=3
       local b=4
       eval $(gen_swap_code a b)  # sugar needed here
       echo $a $b
    }

    $ fun
    4 3


I've used Tcl and Scheme fairly extensively so it's interesting to compare them. Scheme has highly developed macros, and many implementations have non-hygienic macros along with syntax-rules. Most of the coders I know say it's rare to use anything other than the hygienic system, which is pretty easy to learn and quite powerful.

Tcl doesn't have comparable macros, though some writers say in Tcl coding, the whole thing is like writing a bunch of macros anyway, but I'm not sure I buy that.

I'm also not entirely clear what you meant by "user-defined interpretive operators ...". Current Tcl versions provide many facilities for specifying semantics of expressions (proc, ensembles, interpreters, lambdas, etc.) and the ability to redefine most any operator or built-in function if one so desires.

The philosophy of "it means what I say it means" can lead to strange constructions, but that is a basis for describing Tcl as "macro-like" in its abundant, highly flexible features. The many ways it reflects Lisp/Scheme-like traits is truly part of its appeal.


Take a look at the vau-calculus work and the Kernel implementation. The author resurrected fexprs and studied their theory in depth.


About the Common Lisp section:

Using fresh symbols is not enough. GENSYM creates uninterned symbols - those are not interned in a package. Thus referencing them in user code is not possible.

Shadowing 'SETF' in Common Lisp would be non-conforming. SBCL for example detects it and gives an error. The ANSI CL Standard, Section 11.1.2.1.2. http://www.lispworks.com/documentation/HyperSpec/Body/11_aba... Still for user code in macro expansions it can be a problem.

If the SWAP macro really should work with places in a semantic compatible way, then it needs to do more. Compare with the built-in ROTATEF macro. It makes sure that place subforms are only evaluated once.

    ? (let ((a (vector 1 2)))
        (swap (aref a (print 0)) (aref a (print 1)))
        a)

    0 
    0 
    1 
    1 
    #(2 1)
Note that it prints 0 and 1 twice.

The built-in ROTATEF does it better: it prints them only once. This shows that the subforms are only evaluated once.

    ? (let ((a (vector 1 2)))
        (rotatef (aref a (print 0)) (aref a (print 1)))
        a)

    0 
    1 
    #(2 1)

The EACH-IT macro has a bug: the body needs to be put into a PROGN. Otherwise the LOOP macro might think that parts of the body are LOOP clauses.

    (defmacro each-it (list &rest body)
      `(loop for it in ,list
             do (progn ,@body)))
For the purposes as an example, DOTIMES would be more idiomatic, since it provides the basic stuff like declarations and goto tags. It is also the traditional iteration macro for lists.

An alternative to PROGN would be LOCALLY, which also allows declarations. Thus one could declare IT to be of a certain type, for example.


The hygienic C macro is broken. It breaks if your variable happens to already be named "tmp": swap(x, tmp). This would work however:

  #define SWAP(x, y) {        \
      typeof (x) x##y = x; \
      x = y;              \
      y = x##y;            \
  }
Also, you can be tricky and "co-recurse" in macros, which is explained here: https://github.com/pfultz2/Cloak/wiki/C-Preprocessor-tricks,...

Regarding each-it, I think its pretty doable in C. Right off the top of my head, this seems to work:

   #define eachIt(myArray) typeof(myArray[0]) it; for (int i = 0; i < sizeof(myArray)/sizeof(myArray[0]) && (it = myArray[i], 1); ++i)
This can now be used just as in the example:

   struct test_ { int one; double two; };
   typedef test_ test;

   test array[17];

   eachIt(array) {
     it.one = 1;
     it.two = 0.2;
   }
However, I believe(?) that it depends on the version of the C standard whether this is hygienic or not. When I compile in GCC it certainly works fine because the i is counted as internal to the for loops scope, but I'm not sure that's always the case. There is also the separate question of whether "it" should be hygienic or not.


There is an interesting claim about Rust:

> However, it prevents you from writing generic macros that use any l-value – we can’t write swap!(x[0], x[1]) as we could in Common Lisp.

As long as `macro_rules!` has existed (I could be wrong about before 0.8 though), you can wrote a macro that takes any expression, and will error from the assignment when the expression is not an lvalue:

    macro_rules! swap {
        ($x:expr, $y:expr) => ({
            let tmp = $x;
            $x = $y;
            $y = tmp;
        })
    }


I posted an explanation of how Dylan's macro system would handle this elsewhere (including http://www.reddit.com/r/programming/comments/2gesag/comparat...).

Dylan was one of the original languages to have an infix-syntax with a Lisp / Scheme style macro system. (It is fairly similar to Scheme's syntax rules, but there's an implementation of something more powerful that is used within the compiler but isn't currently available to users.)

The examples that he is demonstrating in the other languages would like this in Dylan:

    define macro swap!
      { swap! (?place1:expression, ?place2:expression) }
      =>
      { let value = ?place1;
        ?place1 := ?place2;
        ?place2 := value; }
    end;
and:

    define macro each-it
      { each-it (?collection:expression)
          ?:body
        end }
      =>
      { for (?=it in ?collection)
         ?body
        end };
    end;
Dylan's macros are hygienic by default, and as can be seen in the ``each-it`` macro, ``?=it`` is a way to violate hygiene without much difficulty, when needed.

My full post as linked above contains links to documentation and other minor details.


At the risk of stating obvious - typeof is a gcc-ism, not a part of the C standard.


Thanks, that saved me some keystrokes.

Here's the GCC manual page: https://gcc.gnu.org/onlinedocs/gcc/Typeof.html, note that you can go "Up" to the main Extensions section where you'll find the sub-page for typeof.

Also, the OP does the usual "everything is a function in C" error, which is weird in a text about macros in programming languages. The typeof extension, like sizeof, is not a function, it's a unary prefix operator. But nobody seems to care about that for (the standard) sizeof operator, so I guess I shouldn't be surprised.

Also, of course the local temporary variable should be marked as const.


Tcl (1990) doesn't directly have macros, but with a calling convention that resembles fexprs it doesn't need them:

    proc swap {_a _b} {
        upvar 1 $_a a
        upvar 1 $_b b
        foreach a $b b $a {}
    }
Foreach is a builtin, but is trivially defined as:

    proc each {ls script} {
        for {set i 0} {$i<[llength $ls]} {incr i} {
            uplevel 1 $script
        }
    }
Perhaps more interesting:

    proc until {cond script} {
        tailcall while "!($cond)" $script
    }


There is also a somewhat newish approach in addition to the venerable ones mentioned: a Katahdin-style syntax extensions, built upon the natural PEG extensibility.




Applications are open for YC Winter 2020

Guidelines | FAQ | Support | API | Security | Lists | Bookmarklet | Legal | Apply to YC | Contact

Search: