Thursday, February 21, 2008

Syntax experiments

Now that metalua 0.4 has been released, I took a bit of time to play with syntax. I'm usually reluctant to superficial syntax tweaking: 90% of the time, it breaks source compatibility for a marginal or even negative increase in readability. However, it's impossible to identify the remaining 10% interesting cases unless you give them a fair shot. Hence I introduced two experimental extensions:
  • xloop the extended loop syntax;
  • xmatch the extended pattern matching syntax

xloop

The original idea behind xloop stemmed from Common Lisp's infamous loop macro. This is an incredibly powerful and arcane macro. It's quite fun to hack it, but it's also a surefire way to get unmaintainable code in most team coding conditions. So I reduced its scope so dramatically that, well, it's got almost nothing left from the original loop :) But I've got what I believe is a sound base, on which I'll be able to gradually add new features to try. The central idea is that a Lua loop consists of a loop header, followed by a body "do ... end". What I'm going to provide is the ability to chain several headers, so that you can write:
for i=0,9 for j=0,90,10 do
  print (i+j)
end
instead of:
for i=0, 9 do
   for j=0, 90, 10 do
      print (i+j)
   end
end
The headers can be:
  • for, both in its numeric and enumerator versions. These are composed by nesting the loops, as shown above.
  • while, which gives a condition not to exit the loop.
  • until, which gives a condition to exit the loop.
  • if, which allows to zap an iteration of the loop without breaking it.
while and until header elements act as conditionial break statements, whereas if acts as a conditional continue. If a break appears in the loop's body, all the (implicit) nested loops are broken at once. Some things that might turn out to be useful idioms include "for i=1,100 if not prime[i] do ... end", or "for i=1, 1000 while not canceled do ... end", "for i=1,10 for j=1,10 if i~=j do ... end"... This might look not-so-interesting, but I plan to go on playing with it, in case something truly interesting emerges. I think that introducing some fold and map operators, as alternatives or complements to the loop's body, might be interesting (they would be roughly equivalent to directives such as collect or sum for Lisp's loop macro). There's a point where too many directives will hurt readability, and you can only tell after you've tried, but that's why I'm not merging these extensions with metalua's more mature stuff: they're virtually guaranteed to be at least partially rolled back.

xmatch

Structural pattern matching is already a metalua extension. However, it's the one I used and tuned the most extensively, and it's directly inspired by established functional programming languages, so it's fairly mature. The painfully repetitive idioms I've met are:
  • functions which solely consist of a match statement;
  • contrived uses of match statements when I meant it to return an expression.
Moreover, patterns are only usable in match statements, where several alternative patterns might apply. In some cases, we know that a given pattern will match, we only want to use it as a way to destructurate a piece of data, in order to easily create assignments.
match functions
instead of:
function f(a, b, c)
  match a, b, c with
  | x1, y1, z1 -> foo()
  | x2, y2, z2 -> bar()
  end
end
you can write:
match function f
| x1, y1, z1 -> foo()
| x2, y2, z2 -> bar()
end
This is very similar to Lua's "function f()... end" as a shortcut for "f=function()...end". Still in Lua's spirit, if you want to create a local match function, as in "local function f(...) match ... end end", you can use "local match function f ... end".
anonymous match functions
Of course, you can also declare anonymous functions, this is not Python after all :)
f = match function
| x if x%2==0 -> return 'even'
| _ -> return 'odd'
end
expression match
You are now allowed to put a match...with where an expression is expected. In that case, the conditional blocks are replaced by conditional expressions: print(match x with 1 -> 'one' | _ -> many). Notice that the end keyword in Lua terminates statement blocks, not expressions, and therefore isn't expected by the match expression.
Destructuring bind
This could be summed up as a single-case match statement which escapes its scope. Suppose that you know that x contains something of the form `If{ cond, { stat1, stat2, ... } } and you want to get the value of cond and the first statement of the body. You can write the full code:
assert(x.tag=='If')
cond = x[1]
stat = x[2][1]
Or you can use a bind: bind `If{ cond, { stat, ... } } = x. There is also a local bind, which declare the variables as new locals instead of merely assigning them: local bind `If{ cond, { stat, ... } } = x. Of course, if the pattern doesn't match, you get a runtime error.

No comments: