Articles

Three Rules for Programming Language Syntax?

I’m always pondering the question: what makes good programming language syntax? One thing occuring to me is that many languages often ignore the [[Human-computer interaction|HCI]] aspect.  For me, it’s a given that the purpose of a programming language is to simplify the programmer’s life, not the other way around.

So, I thought of a few simple rules:

  1. Syntax should explain. The purpose of syntax is to help explain a program to a human.  In particular, structure in the program should be made explicit through syntax.
  2. Syntax should be concise. The converse to (1) is that syntax should always add to the explanation.  Syntax which does not add value simply obscures the explanation.
  3. Syntax should conform. The are many things people learn from everyday life, and we should not force them to unlearn these things.  Doing so can confuse the explanation, especially for non-experts.

I’m sure there are plenty of others you can think of.  I pick on these because I see them being broken constantly.  Let’s illustrate with some of my pet peeves:

Colons in Type Declarations?

There are plenty of programming languages which use colons in type declarations ([[ML (programming language)|ML is a classic example]]).  It goes something like this:

fun f (0 : int) : int = ...

For me, this breaks rule (2) because those colons do not add value.  The C-family of languages is evidence that we can happily live without them.  That’s not to say that C-like declaration syntax is the “one true way”; only that it demonstrates we don’t need those colons!

Function Call Braces: To Be or Not To Be?

Some programming languages require braces around the arguments to a function call, whilst others (e.g. [[Haskell (programming language)|Haskell]]) don’t.  Consider this:


(f (g h) 1 2) y 3

What can you tell about the invocation structure from this line?  Not much, unless you happen to know from memory exactly what arguments each function takes. This violates rule (3) since functions are normally expressed with braces in mathematics.

Now, how about this:


f(g(h),1,2)(y,3)

Personally, I prefer this style.  However, it would be fair to say that it violates rule (2) since the comma’s are not strictly necessary.  I suppose, in fact, we have a contradiction between (2) and (3) in this case, since commas are generally used to deliniate lists in written English.

Why Don’t we Teach Polish Notation at School?

(answer: because we don’t)

Some programming languages require mathematical expressions be expressed in [[Polish notation]] (a.k.a. prefix notation).  For example, in [[Lisp (programming language)|Lisp]], we write (+ 1 2) to mean 1+2. This violates rule (3), since most people have learned mathematics at school where the usual convention applies.

Similarly in [[Smalltalk|SmallTalk]], the expression 2+3*4 is equivalent to (2+3)*4 rather than 2+(3*4), as might be expected.  Again, this violates rule (3) and is particularly insidious because it’s rather subtle.

The point here is not to say that e.g. one notation is fundamentally better than the other.  Just that, we learn one in school, so we should stick with that.

Conclusion

Well, if you made it this far, great!  Hopefully there was some food for thought, and maybe you can suggest some other examples…

23 comments to Three Rules for Programming Language Syntax?

  • Not to be rude or anything, but your Haskell example is not legal in the language. You need to use braces in Haskell to sometimes disambiguate nested function calls, so your example should look something like this:

    f (g h) 1 2 (y 3)

    That is, assuming that g and y are functions, and that (g h), 1, 2, and (y 3) are used as parameters to the function f. It’s hard to tell what intent you had from your more readable example, since y and 3 are not enclosed in the parameter list of f (I assumed you just typo’d there.)

  • Hey, thanks for pointer … my haskell is rusty! Y is not a function, it’s a constant. so I think it should be: (f (g h) 1 2) y 3. I’ll update.

    Dave

  • It depends. Do you want (g h), 1, 2, y and 3 to all be passed into the function f? If so write it as

    f (g h) 1 2 y 3

    for it to be legal haskell. Using your braced language, it should be rewritten as

    f ( g(h), 1, 2, y, 3)

    So they all get passed into f. Is this what you intended?

  • Not quite. The function f returns a function, and y 3 are parameters to that. So, I think it’s fixed now. But, that turns the tables quite a bit, and now I realise the C-like syntax is the one violating rule (2)!!

  • Tim

    Haskell’s function calls don’t look like ordinary function calls because they’re not. Currying simplifies the syntax a great deal (as you’ve acknowledged) and adds a lot of power to the language.

    Your example can be simplified to f (g h) 1 2 y 3. It means the same thing. That’s something the normal notation can’t do, and is part of what makes the language a joy to use. Although it might take some time to get a grasp on these concepts, this doesn’t necessarily make the syntax bad. If it’s the concepts that are confusing then there’s no point in sugaring them up. That will just lead to more confusion later (and break rule 2).

  • Hey Tim,

    So, how is it they’re the same?

  • Tim

    Because function invocation binds in the other direction in Haskell.

    f a b c means the same as (((f a) b) c), as opposed to the parens-less calls of Ruby and Coffeescript (which use commas to delimit arguments). Just because you’ve ‘finished’ calling a function doesn’t mean it needs to be wrapped up in parentheses to keep operating on it.

  • Ah, I see. Interesting.

    So, in my example, f (g h) 1 2 is the same as (((f (g h) 1) 2) and this works out because of currying. Then, why do I need braces around g h?

    Suppose h is an int constant, and g has type int->int. If f has type int->int->int->int, then doesn’t f g also have type int->int->int->int ?

  • Sebastian

    You need braces around “g h” to group them. If you dropped the braces then the first argument to f would have to be of type “(Int->Int)”, not merely “Int” (so f g would have type (Int->Int)->Int, which is different from Int->Int->Int which is equivalent to Int->(Int->Int)). In other words, a function that takes an int and produces an int, is not he same as just an int. Therfore you need to group “g h” so determine precedence so that you can pass in the result of applying g to h, rather than passing in both g and h to f.

  • Tim

    f g won’t compile: the first argument of f must be an Int.

    Int -> Int -> Int is the same as Int -> (Int -> Int). The type of g is Int -> Int, at no point in the type of f does this appear. If the type of f were (Int -> Int) -> Int, then f g would be valid and would evaluate to an Int.

    You need the parens around (g h) to distinguish that h is being passed to g, not to f. It groups the call to g into a single expression to make it the first argument of the call to f.

  • Sebastian and Tim,

    I guess that’s what I figured, but it actually seems slightly non-uniform to me, with respect to currying.

  • Sebastian

    Also, the reason you sometimes need to add “redundant” symbols is to make the syntax less ambiguous. This is helpful for compilers and humans alike. It sucks that “F X(Y);” in C++ can be either a variable declaration or a function prototype depending on context. Having a colon to indicate to the parser, and human, that “here comes the type” can make it much easier to see at a glance what’s going on.

  • Hey Sebastian,

    But, that’s exactly what I don’t like: compromising syntax to simplify the compiler. As a compiler writer, I certainly understand the temptation … but I need we always need give priority to the humans :)

  • Sebastian

    It’s not non-uniform, it’s just normal grouping like you’d do in any other mathematics context with non-associative operations (in this case, function application).

  • Hmmm, I disagree. Currying is about delaying computation. That is, taking an argument and not evaluating the function, but producing partially evaluated result. So, it makes sense to me that in f g h 1 2, you can “curry” on g producing a new kind of function which, by coincidence, accepts the same arguments.

    A more general example:

    f : T1 -> T2
    g : T3 -> T1

    then (f g) : T3 -> T2

    … it’s function composition!

  • Tim

    And you can do that if you really want.

    (f . g) h 1 2

    You just need to compose it first to produce the right function. (Note that you still need parens because application binds tighter than any operator).

  • Sebastian

    Currying isn’t about delaying computation, it’s about partially applying funcitons. Regardless, it’s irrelevant to the discussion of syntax.

    *Any* operation that is non-associative would require parenthesis to specify order. In this case, either you add a separator between every argument, or parenthesis around every application, or you require people to explicitly group applications when they’re passed as an argument to other functions.

    Also, my argument about colons for types isn’t about simplifying it for compilers, but to simplify it for readers of the code, as opposed to writers. I don’t care that the C++ syntax is context-dependent and ambiguous for the sake of the compiler reader, I care because it slows me down when I try to read C++ code. Adding a few carefully chosen symbols to make things easier to parse (by human readers, and compilers) is perfectly sensible.

  • I have found that Haskell and ML’s function call syntax helps make unorthodox usages of higher order functions a bit more intuitive. Particularly in Haskell, where (say) a function such as distance :: Point -> Point -> Length is probably perceived by the writer as taking two arguments and returning the distance, it’s equally useful in situations such as map (ditsance origin) interestingPoints where it’s being treated as a function that generates a distance-from-x function for some x, in this case, the origin.

    I think it’s good to have generalized principles like you have to guide language development, but every language designer must inevitably come down on the side of personal aesthetics at some point. For example, one of the major syntactic differentiators between Ruby and Python is that Ruby obeys the uniform access principle and Python does not. The argument for this principle is that you don’t want clients of your object to care whether a query is implemented as a property or a function; you should be able to switch implementations in the future without breaking your object’s contract (the origin of this, AFAIK, is Bertrand Meyer’s Object-Oriented Software Construction). One ramification of this is that in Ruby it is not possible to pass a function by name to another function; you must instead create a closure with a block. Ruby happens to include other syntactic support that makes this easy or automatic, but it’s a contrast to Python, where names always refer to some object and not some computation, and function calls are usually discernable from property access by the presence or absence of parentheses. There are now ways of working around this for the purpose of providing methods that are invoked at property accesses instead of reading properties on objects, but this was a late addition to the language and is about as clean and easy as passing a function by name to another function in Ruby.

    The point I’m making is that neither of these answers is “correct” in absolute terms. I could argue that Ruby’s method is concise and informative to humans in the general case, but I could also argue that Python’s decision conforms (saying ‘foo’ when you have a function defined by that name refers to the function, not the result of invoking it). Good languages will certainly fulfill these guidelines, but they will do so only within their self-defined context, and comparison across languages is (in general) fruitless.

  • James

    We’re taught all kinds of things at school – overbars, different sized parentheses, single Greek
    characters as variable names – many of which aren’t great in programming languages (well other than Fortress).

    Not sure if there’s any research on this (would be relatively easy to check) but I think best practice is probably to parenthesize if there is any room for ambiguity. That’s why Self, for example, has no operator precedence – if you’ve more than one, parenthesize.

    (and you’re still misspelling Smalltalk)

  • Hi James,

    I never got taught greek letters at school (well, other whan pi)!

    As for research, well there’s that paper on Perl versus Random Syntax.

    As for Smalltalk, I just follow Wikipedia … why do they do it that way?

  • Art Protin

    I do not accept your wording (and thrust) of your first principle. As I See it, it is essential for a programming language to be unambiguously precise, although I would see that as the 0th rule, and a rule that must never be compromised. Factoring that out of your first rule, that leaves it to require clarity to the human about that precision.
    The discussion in the comments about the Haskell example borders on an indictment of Haskell, for the better the language the less room there is for such misunderstandings. The value of a programming language derives entirely from the production of a corpus of properly functioning programs, and clarity of expression is key to the writing, debugging, and maintaining of programs. The precision of a program should be equally obvious to a human reader as it is to the computer.

    That paper you cite claiming things about “fandom syntax” lies, for all they have done is encipher the keywords, leaving totally unchanged the syntactic rules for the structure of programs. Please, never confuse the “syntactic sugar” and lexemes for the real syntax of a language. (I will argue that the indentation rules of Python are not part of the real syntax.)

  • it is essential for a programming language to be unambiguously precise

    That’s a given, and is not specifically about syntax.

    The precision of a program should be equally obvious to a human reader as it is to the computer.

    No, I definitely disagree on this. It should be much more obvious to a human than the computer :) That is, I want to make the compiler work harder for the benefit of the human.

    That paper you cite claiming things about “Random syntax” lies

    That statement is far too strong for me. Certainly, as we discussed in the reading group, there are a few problems with the paper. My point is only that it represents an attempt to do research in this space.

  • Aivar

    Maybe my brain is not “natural” anymore, but I think that what’s difficult to parse for computer is usually also difficult to parse for human.

    About requiring parens for application — this overloads the meaning of parens, they’re not only for grouping anymore. I think all kinds of overloading usually complicates matters (but on the other hand, it may be beneficial for expert language users).

    Now, if you choose juxtaposition for application, then you need to separate type and name with something (eg. colon). You gain some, you lose some.

Leave a Reply

 

 

 

You can use these HTML tags

<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>