4

I'm playing with the idea of learning Scheme but I have a few misgivings.

From what I understand Lisp makes heavy use of macros that allow programmers to drastically change the language itself. I understand why that's useful, but how does this affect collaborative projects? How much confusion can this cause if other developers implement obscure macros, with or without documentation? Are there well defined guidelines for avoiding this confusion or are there language features that help avoid it?

I ask because my current job is C++ and we have to use an in house framework. Our senior developer has tried to extend C++ to make it as Java-ish as possible. We have to spatter our code with obscure macros to make everything a smartpointer (in house of course, not the standard library kind) and homebrew exceptions etc. As a result, everything we write turns into a heaping mess and when these magic macros break we have no idea how to fix them. Obviously job security comes into play, but let's forget about politics and assume everyone is here for the right reason.

From what I've read, the C preprocessor is very primitive compared to Lisp. So would a situation like this be 1000 times worse in Lisp or would it be better somehow?

  • 2
    I can't answer your actual question but I just wanted to point out that Lisp macros are a totally different beast than C's (and, in general, than text-manipulating macros). What I've read is that because Lisp has barely no syntax, what you see is the actual AST ("data is code is data" in Lisp); macros can manipulate this syntax tree. So Lisp macros replace the use of a lexer/compiler: you can dynamically implement stuff like 'while' or 'for' (or anything you find as featured keywords in other languages) as macros rather than embedding them in the language like C++ (or Java or Python). – Nadir Sampaoli Sep 28 '13 at 9:26
  • 3
    Technically, the absence of syntax is not a necessary condition for syntax-manipulating macros (although it helps). For example, Nemerle and Scala have those. – fjarri Sep 28 '13 at 9:39
  • 2
    Macros should be used for defining embedded domain-specific languages, which are obvious, simple, self-documenting (and, of course, well-specified). This way code will only become more readable and easier to maintain. – SK-logic Sep 28 '13 at 13:10
  • +1 and much-needed LOL for the pain I didn't realize I was still carrying @ "Obviously job security comes into play, but let's forget about politics and assume everyone is here for the right reason." – Erik Reppen Sep 28 '13 at 18:44
5

Having worked on a few Lisp projects (Mostly Scheme, some CL) here are my thoughts.

With macros you can make code hell to maintain. But this is only when you don't document. Frankly I think the same can be said for any language feature. When was the last time you read 500k lines of OO ravioli and went "Gee there was no docs, but this code is so understandable anyways".

The best way to make sure everything stays readable is simply to enforce documentation. In the projects I've worked on the documentation had to have something like

;; sommac, Used to do foo.
;; Translation:
;;   (sommac (a b c)
;;       d e f)
;; to
;;    (loop for a from b to c
;;          do (begin d e f))

The translation isn't exactly what the macro will do. Sometimes they were in pseudo-code. But it was a simple effective way to make sure everyone knew what was going on.

  • Ravioli code is ravioli code. You can write OO that is not ravioli. – Robert Harvey Mar 10 '14 at 22:40
  • 1
    @RobertHarvey Very true, that comment was to illustrate that you can write terrible code in any paradigm – Daniel Gratzer Mar 10 '14 at 22:43
3

Since you have a C background it is best to first look at lisp and C macros, and see what properties they share and which they are not sharing.

In C, macros are compile-time-rules for almost textual replacement of code parts by the preprocessor. C macros have a different syntax than the C programming language, they are executed by the preprocessor and emit C code that is compiled by the C compiler. So it is a preceding step to program compilation.

Lisp is a little different here. In Lisp, every code is a tree-like (directed, acyclic graph) datastructure that is written in S-Expressions (think of S-Expressions like compact XML).

(case a
   ((10) "a is ten")
   (else "foo"))

This datastructure consists of nested list objects that offer a simple API for accessing its elements. The great thing about lisp is, that the same data structures used in a lisp program for storing lists is also used during the compilation of the Lisp code. Therefore one can write small "compiler" extensions in Lisp code that transform branches of the abstract syntax tree into other representations. Again: You use the same language that you use for programming an algorithm to transform the code.

For a moment, I would downplay the differences between a procedure and a macro call. The first real difference between a procedure and a macro is about the time when they are executed not about the nature of their abstraction in the first place.

The overall question is: How can we keep a codebase from becoming an obscure meta-langauge that is not accessible to colleagues?

Keeping programs readable

Readability of programs is a complex topic.

  • First of all it comes down to the experience and expectancy of the reader. Code that looks foreign is harder to understand for a programmer than code he/she is familiar with.

  • Then there is the problem of code-scope. When reading code we are quite local and cannot look at all parts of the source at the same time. Therefore code that is compact should be easier to read than related code that is distributed widely over the source file.

  • However, good abstraction helps code readability since it groups operations into logical chunks. A function composes these abstracted operations is more readable than an equivalent function that concretely implements everything on its own.

  • Good abstraction means separation of concerns. A function that does several unrelated things at a time is harder to understand since you have to disentangle the functionalities within the procedure.

    Consider a function write_file_and_show_results might be a bad function. Especially if it does not compose one write_file and one show_results function but directly opens the file, starts writing to it, shows results, writes some more....

The virtue of lisp is, that it is really good for implementing these good abstractions. This is often compared to "writing an own language for the problem". Yet it is hardly different than programming in other languages, Lisp just has more drastic means for extending syntax.

Commonly if people argue against macro use or abstraction, they point out, that a direct, concrete implementation is more readable than one that involves a few abstractions. In practice, it mostly pays off to introduce some abstraction that compacts the rest of the code. It saves LoC, maintainability is increased since you only have to perform changes once than in several parts of the problem

Architectural design: Bottom up

Paul Graham has written a nice essay on how to work when writing Lisp code. http://www.paulgraham.com/progbot.html

Macro Hygiene

There is one huge problem associated with macros: Hygiene.

Take the following example of a C program to illustrate it.

#define max_defect(a,b) ((a) > (b) ? (a) : (b))

The issue at hand is, that an invocation of max_defect(i++, 3) will increase i twice. Typically this problem is solved by introducing another binding of a and b (something like _a) and evaluate the parameters to it. Then, the bound results are used to perform the actual code.

However, what happens if you then use _a as a parameter to the maximum-determining function? You have a name conflict at hand. In Lisp this problem has been solved by introducing gensyms. Generated symbols that are unique in the program.

However since this still leaves the possibility for errors, other ways to define macros have been developed, so called hygienic macros. Scheme does offer hygienic macros. They always guarantee correct lexical scoping.

Developing a common language

When presenting the idea of a "modifiable" programming language, people sometimes fear, that this might lead to a programmer building up "his/her own world" that is inaccessible to the outsider. While this is certainly possible, as long as you are not working in total isolation it will not be the immediate consequence. A lot of macros are already existing that encapsulate a certain abstraction, their naming schemes can be adapted, and there are also surprisingly readable macro constructs, such as the loop macro.

Generally the lisp communities have slightly different traditions in naming things than other programming communities. make-whatever for creating an object, with-RESOURCE for RAII like resource management, etc. Getting used to this is not an issue when reading established lisp codes and will enable you to write consisten lisp code.

Disclaimer: I have mostly worked with Scheme and Clojure, not very much with Common Lisp. Clojure is probably the most readable dialect for people who have some experience with Python and/or ruby.

0

Lisp macros can be used well or badly.

Simple use of macros can be similar in effect to cpp - replacing something simple looking with related boilerplate for example. It can make things prettier without changing much if used well, or it can make things awful by doing something unexpected.

The entire lisp language and environment is available to lisp macros. Sophisticated use of lisp macros can implement whole language constructs ("loop" for example is just a lisp macro), or perform complex analysis or build complex data structures; effectively becoming part of the compiler.

Not the answer you're looking for? Browse other questions tagged or ask your own question.