Lisp evangelists that miss the point about syntax
confusionist.tumblr.com
confusionist.tumblr.com
You won't get a language war from me, but there's a huge difference in "feel" between those two major language classes, and strangely, I kinda like both of them. Sure I can do some impressive reasoning in the functional realm, but even in the procedural realm I can do iron-clad reasoning and correctness-preserving transforms that (I hope) would make Dijkstra proud.
The Fortran family of languages was based on the hardware's capabilities, with theoretical considerations like expressiveness as an afterthought.
We are fortunate to have reached an age where we have CPU cycles and RAM to spare so that languages that were not designed with performance-related restrictions in mind are becoming practical for a wide variety of applications.
Consequently the whole purpose of Fexl is to be a thin functional "layer" on top of C. That way as a C programmer I can escape to functions to avoid all the gnarly horrors of memory management and such, and as a Fexl programmer I can escape to C to embrace the hard-edged bits and bytes. That way if you asked me whether I was writing the thing in C or in Fexl, I couldn't really give you a straight answer. ;)
Right off the bat I can tell you that Fexl's combinators aim to be written in readable C.
I suppose the simplest combinator to show would be "I" (the identity function). In Fexl I have a file named "type_I.c" which says:
#include "node.h"
#include "type_I.h"
static struct node *reduce(struct node *parent)
{
return parent->RIGHT;
}
static struct type type = { reduce, clear_pair };
struct node *make_I(void)
{
return make_pair(&type,0,0);
}
(Quick note: all node values are reference counted. Cycles are impossible, and unnecessary because of the wonderful Y combinator.)Speaking of Y combinator, here is "type_Y.c":
#include "node.h"
#include "type_app.h"
#include "type_Y.h"
static struct node *reduce(struct node *parent)
{
return make_app(parent->RIGHT,parent);
}
static struct type type = { reduce, clear_pair };
struct node *make_Y(void)
{
return make_pair(&type,0,0);
}
Those are easy. The most gnarly one is the S (fusion) combinator ... which ... oh well I might as well paste it in: #include "node.h"
#include "type_app.h"
#include "type_S.h"
static struct node *reduce(struct node *parent)
{
if (0 == parent->LEFT->LEFT || 0 == parent->LEFT->LEFT->LEFT)
{
parent->type = parent->LEFT->type;
return parent;
}
struct node *x = parent->LEFT->LEFT->RIGHT;
struct node *y = parent->LEFT->RIGHT;
struct node *z = parent->RIGHT;
struct node *fun = make_app(x,z);
struct node *arg = make_app(y,z);
hold(fun);
hold(arg);
parent->type->clear(parent);
parent->LEFT = fun;
parent->RIGHT = arg;
return fun->type->reduce(parent);
}
static struct type type = { reduce, clear_pair };
struct node *make_S(void)
{
return make_pair(&type,0,0);
}
OK now I'm pushing the limits of long-windedness, but I just wanted to feed you some information right away while I work on some other things. #include "node.h"
#include "type_app.h"
static struct node *reduce(struct node *parent)
{
struct node *curr = parent->LEFT;
while (curr->type == parent->type)
{
struct node *self = curr->LEFT;
struct node *next = self->type->reduce(curr);
if (next != curr)
{
hold(next);
drop(curr);
curr = next;
}
}
parent->LEFT = curr;
return curr->type->reduce(parent);
}
static struct type type = { reduce, clear_pair };
struct node *make_app(struct node *f, struct node *x)
{
return make_pair(&type, f, x);
}
That "while" loop you see there is essentially the only loop in the entire core of Fexl. (Obviously "plug-ins" can do their own loops.)That loop keeps reducing the parent node as long as it's still of type_app. Then the loop ends and it reduces whatever the parent node has become as a result of the evaluation loop.
Dunno what all this says about me though :-)
This just indicates the author is clueless about the substantive use of a DSL.
The only way to implement this is to vastly reduce the scope of that "intend." Basically equivalent to, "all you really need is Blub."
80% would be reasonable. But the last 20% is going to encompass some powerful stuff.
It's obvious the author hasn't a Clue about that.
[1] Of course you can "do it all" in any Turing-complete language, but theoretical possibilities are often much less interesting than practical impossibilities. In Python or Java, creating new syntax is so hard it might as well not be possible.
The point of the article was that creating a sufficiently powerful language, that doesn't offer easy ways[1] of extending the syntax, does not warrant the criticism that the language has 'too much syntax'.
Evidently, you are as inexperienced with DSLs as the author.
Of course you can "do it all" in any Turing-complete language, but theoretical possibilities are often much less interesting than practical impossibilities. In Python or Java, creating new syntax is so hard it might as well not be possible.
Insufficient knowledge is probably why you mistook the sense of the statement you are replying to by 180 degrees.
Exercise for the student -- find the contradiction here:
You consider 'sufficiently powerful' and "doesn't offer easy ways of extending the syntax" to be contradictory. Does that mean you consider only Lisps 'sufficiently powerful'? Insufficient knowledge is probably why you mistook the
sense of the statement you are replying to by 180 degrees.
Do you mean extending the syntax of Python or Java is easy? Otherwise I'm not sure what the knowledge is you think I'm lacking.Syntax extension (which in the case of macros is not about syntax at all, but about controlling when evaluation happens, something else that people do not understand about macros) is not about inventing a new, stupider way of writing for loops. It is about clearly expressing domain concepts.
For an article about "missing the point," the author manages to fail to understand both how syntax extension works and how it's used.
Because then I've got a syntax that works for 99% of the use cases you've already thought of, and that doesn't overlap very well with the use cases I have or that either of us will come up with in the future.
So why not create syntax that catches 99% of the cases for which you intend the language to be used and abolish the ability to create further syntax?
Although it's possible to create new syntax in Common Lisp via reader macros -- I don't think lispers recommend to extend the syntax. But when you miss a language feature, instead of fighting against the language, you could implement it. I don't remember exactly who said it in one the SICP video lecture, Harold Abelson or Gerald Sussman, but he said something like
Lisp is not good at solving a particular problem. What Lisp is good at is extending the language to solve a class of problems.
The problem with syntax is that it's difficult to use correctly with macros. It's no wonder that most introduction books to C warns against the pitfalls of using macros. Even the simple ones could bite you if you're not careful. Consider:
#define DOUBLE(x) (2*x) /* should be (2*(x)) */
#define MAX(a, b) ((a) < (b) ? (b): (a))
Even the last one is not immune against multiple evaluation problems
if you try to evaluate MAX(a++, b). In Lisp you could avoid this by
using gensym and local bindings. That being said, C macros and Lisp macros are
completely different beasts. And I recommend reading ``On Lisp'' for
advanced use of Lisp macros.The cost of Lisp is that the syntax is all the same. So a conditional looks the same as an assignment statement, looks the same as a function call, looks the same as a plus operator.
Lisp syntax is bad, because many humans find it easier to scan source code if different constructs use different syntax (whether that is just because it is what they are used to, I don't know). But Lisp syntax is also good, because it allows macros.
You could get round this problem by having two levels of a language, one with C-like syntax which compiles to Lisp-like syntax (which may itself be compiled). So this:
if (a>b) c := foo(d);
Might compile to this: (if (> a b) (:= c (foo d)))S-expressions are not INTERNAL to Lisp, only external. Thus s-expressions are defined on characters. Comparing s-expressions to strings makes no sense.
The representation of code is a different issue. You could read an s-expression and represent it internally as a string.
'if' has the syntax
if test-form then-form [else-form]
'cond' has the syntax cond {clause}* => result*
clause::= (test-form form*)
'case' has the syntax case keyform {normal-clause}* [otherwise-clause] => result*
normal-clause::= (keys form*)
otherwise-clause::= ({otherwise | t} form*)
LOOP has a full page of syntax.
http://www.lispworks.com/documentation/HyperSpec/Body/m_loop...What you think has 'no' syntax, is the syntax for s-expressions. S expressions are relatively trivial, but not every s-expression is a valid Lisp form. S-expressions are a syntax for DATA.
The syntax of Lisp, the programming language, is described on top of s-expressions. Syntax is concerned with the structure of valid expressions of a language. So you have an additional layer of syntax which describes, on top of s-expressions, the syntax of Lisp.
There is some basic syntax:
* data like numbers, strings are valid Lisp programs
* function calls are valid Lisp programs. Function calls have a non-trivial syntax with rest, optional and keyword args, lambda functions, ...
* special forms like CATCH, FLET, IF, QUOTE, SETQ, ... all have their defined syntax
* macro calls, here the macro has to implement the syntax specified in the standard. users can do with their macros what they want.
For example
(if (foo-p) (this) (that) (these))
is a syntax error, though it is a valid S-expression.Similar
(loop for i below 10 (print i))
is also wrong. Correct would be for example (loop for i below 10 do (print i))
So, what you may want is to remove the data syntax (s-expressions) from Lisp and replace it with some other layer of syntax that is not based on prefix parentheses-heavy s-expressions.There are lots of languages that have different syntactical approaches. Lisp has its own, which makes it slightly unusual (the syntax is used to support the code-is-data paradigm), less popular, but also makes sure that it will find its users - those users who want a language with a flexible syntax (one that can be extended by the developer) on top of a simple data syntax.
Aside from that, some lisps have tendencies (although not requirements) that symbols with common uses have special characters to stand out more. For example, foo for dynamic variables, +foo+ for globals, foo! for destructive operations (in scheme), foo? for predicates (scheme + clojure), and = for assignment (in arc). The choice of what to emphasize is different, but it was a conscious choice on the part of the language creators.
If infix was actually "natural", there'd be only one set of precedence/associativity rules and there wouldn't be bugs due to folks getting it wrong.
With very few exceptions, folks use parentheses to try to protect themselves from their ignorance of their language's precedence rules. (C++ has over 10 levels of precedence.) Even then,they occasionally get it wrong.
And, it's worse than that on teams because different folks have different knowledge (and illusions) of the precedence rules.
However, many important developments in math and science ran counter to common sense. In math, people with common-sense arguments resisted numbers which seemed "irrational", "imaginary" and "negative." (Supposedly, there were times when you could die over them.) Euclidean geometry seemed so obvious that people were scared to discuss non-euclidean ones. In science, even Newton had some self-ridicule about spooky action at a distance, like gravity:
"That one body may act upon another at a distance through a vacuum without the mediation of anything else, by and through which their action and force may be conveyed from one another, is to me so great an Absurdity that, I believe, no Man who has in philosophic matters a competent Faculty of thinking could ever fall into it."
http://en.wikipedia.org/wiki/Newton%27s_law_of_universal_gra...