Stevey's JVM Language Soko-Shootout: Pnuts

(This article is one of a series of articles on JVM languages.)

Chances are pretty good that you haven't heard of the Pnuts language. I wasn't aware of it until well into my JVM-language evaluation.

Pnuts is low-key; it doesn't have much marketing hype. I was pretty surprised to find that it's mature, expressive, and resulted in the most concise implementation by some 15% to 30%, depending on how you count (lines/chars/tokens). And it was one of the fastest, without even compiling to bytecode (which you can do.)

So what's it like? Here's the whirlwind tour:

  • Dynamically typed. You don't declare the types of your variables. It does have some facilities for typecasting and coercion. Syntactically it looks a fair bit like JavaScript with optional semicolons.

  • Functional. Pnuts has solid support for first-class functions. There's even an extension module that gives you a large-ish subset of the Haskell prelude, although I didn't use it in my evaluation, since it's not part of the standard Pnuts distribution.

  • Fast. Even interpreted, the language is very fast. Compiled to bytecode, it appears to be one of the fastest JVM languages out there.

  • Stable and well-supported. I found only one minor bug in the language as I was writing my app. I reported it, and it was fixed by the next day, with new binaries uploaded to the website for all platforms. Very cool.

  • Java-friendly. Pnuts is written just for the JVM, and it has hundreds of shortcuts for Java functions. Pnuts also some fancy declarative UI-building tools, including an excellent generalized layout manager.

  • Well-documented. The documentation includes a detailed User Guide, a FAQ, a language reference that includes the full grammar, an API guide, a guide to the modules (both standard and the extension modules), and other goodies. It's all nicely organized, although it would be nice if it were searchable.

Pnuts comes with a debugger, although I never needed to use it. Pnuts also comes with an interactive shell, which was quite useful for experimentation. Even better, you get into the shell simply by typing "pnuts" without a filename. No separate commands needed, and no mucking around with getting shell scripts working on Cygwin. Pnuts comes with Windows executable binaries.

Despite some bumps, I really enjoyed working with PNuts, and I think it has tremendous potential. It's a new language just for the JVM, so it's evolving quickly, and isn't weighed down with a lot of legacy baggage. I learned the language in just a few hours; its syntax and semantics should be very familiar to Java programmers. That's a big plus for a JVM scripting language.

Language Strengths

Pnuts is fairly expressive. It's less succinct than Ruby or Python, but definitely more succinct than JavaScript. This section covers a few things I like about Pnuts.

1) First-class functions. In Pnuts, you can pass functions by name or by value as function literals, e.g.:

    bind(root, "keyPressed",   function(e) handleKeypress(e))
    bind(root, "mousePressed", function(e) handleMouse(e))
    bind(root, "mouseDragged", function(e) handleMouse(e))
Pnuts is smart enough to match up the argument lists, so this works too:
    bind(root, "keyPressed",   handleKeypress)
    bind(root, "mousePressed", handleMouse)
    bind(root, "mouseDragged", handleMouse)
Anonymous functions aren't limited to a single expression, as they are in Python.

You can declare nested functions that create closures. Pnuts has a decent selection of higher-order functions, although somewhat strangely they decided not to include the "functional" module as part of the standard distribution.

Pnuts provides a variety of useful shortcuts for declaring and passing functions. Take the following pair, for instance:

function stopAnimation() animating = false
function startAnimation() animating ? false : (animating = true)

This is about as succinct as you can make these functions, regardless of the language (although it would be nice if Pnuts allowed you to make a shortcut for the word "function", which is a bit cumbersome.)

The first function, stopAnimation(), just resets a boolean variable. Still, it's unusually concise because you don't need braces or a semicolon; the newline terminates the function. You only need the braces if your function body is more than one statement or expression. This is consistent with the way if-statements and for-statements work in C-like languages; it's nice that Pnuts permits the same shortcut for function definitions.

There's more going on in stopAnimation(). The function acts like a test-and-set instruction, so it has both a side-effect and a meaningful return value. If the animating flag is set, then we return false, indicating that we couldn't start the animation because it's already going. Otherwise, we assign the variable and return true to indicate that we started it.

Note that this is all done in a single expression. The ternary operator helps a lot, but most languages that offer a ternary operator don't allow it to appear where a statement is expected. They'd want you to put in a return or if keyword in there to make it an official statement. Dumb. Pnuts is showing its colors as a functional language here: there's no distinction between statements and expressions, and everything returns a value.

2) Inline variable declarations (for lack of a better word). I haven't seen this feature before. To illustrate, look at line 5 below:

1. /// Undo support.  Just snapshot board and a few globals => easiest.
2. UNDO_VARS = ["dudeX","dudeY","dudeDir","moves","allMoves","pushes","allPushes"]
3. 
4. function saveUndoInfo() {
5.     copyGrid(ctx.s_board, board=String[ctx.s_width][ctx.s_height])
6.     undo = map(); undo["board"] = board
7.     project(UNDO_VARS, function(n) undo[n] = ctx["s_"+n])
8.     ctx.s_undoInfo = undo; ctx.s_undoMenu.enabled = true
9. }
The second parameter is board=String[ctx.s_width][ctx.s_height]. This declares the variable in the scope of the function and gives it an initial value, then passes it as a parameter to copyGrid. The variable is still in scope for the rest of the function. Very convenient.

This feature is particularly useful when using the Pnuts layout manager, to give names to the UI widgets as you create them. It's also useful for creating a variable to collect the results of the map function, e.g. in the following code, where the last parameter creates an accumulator called "grid":

    // make all lines the same width, and replace all floors with OOB for now
    fmap(function(s) sprintf("%-*s", w, s).replace(" ", "_"), rows, grid=list())
3) Array and hash literals -- these have been a staple of scripting languages since the days of ancient, byzantine, obfuscated languages like Awk and Perl. Language design has evolved quite a bit since then, but a few of their ideas have carried forward, and this is one of them.

In Pnuts, hashtable literals follow the syntax that's become standard: {key => value, key2 => value2, ...}. Lists follow the [a, b, c] syntax that most languages use these days, and Pnuts also provides a list() function that allows you to specify whether it's a LinkedList, Vector, ArrayList, or primitive array, passing the literal data to the function. Very convenient.

Unfortunately Pnuts suffers from some serious problems and inconsistency in this area, so it's far more annoying than it should be; I'll discuss these below.

4) Lots of sugar -- Pnuts has quite a few syntactic shortcuts. For instance, it offers a Java 5-style "smart" for-loop, one that's even smarter than the Java version because it accepts generators, iterators, and strings in addition to the standard collections.

5) Operators galore -- Pnuts supports pretty much all the operators you'd expect if you're coming from a Java background, including the bitwise operators (some of which I use in the implementation), shifts, ternary operators, and indexing.

Pnuts also has a few extra operators not present in Java, including being able to index strings with [], a (..) range operator, list slicing, and array concatenation. As in nearly all scripting languages, using "==" to compare strings compares their contents, not their pointers, which is almost always what you want. (Unfortunately, Pnuts doesn't seem to provide an easy way to compare strings for instance identity.)

6) Library functions -- Pnuts has a pretty large collection of standard modules, all of which you can include by using "pnuts.tools". It gives you built-in functions for doing a lot of the most useful things from the Java APIs, plus some nice extras such as a readLines() function.

7) Layout Manager -- Pnuts has a custom layout manager (which you can use directly in Java without Pnuts, if you like) called PnutsLayout. It works like an HTML table, so you can declare the number of columns and/or rows, then add components specifying their rowspan or colspan (among other things).

In addition to the html-like layout manager, Pnuts has layout() and menubar() functions that take object trees and pass them as arguments to the appropriate layouts. Here's the Sokoban menu bar using this approach:

    ui = [["Game",
           ["Restart Level",  restartLevel, ks('R')],
           ["Save and Quit",  saveAndQuit,  ks('Q')]],
          ["Edit",
           ["Undo Push",      undoPush,       ks('U')],
           ["Record Macro",   startRecording, ks('M')],
           ["Stop Recording", stopRecording,  ks('S')],
           ["Replay Macro",   replayMacro,    ks('Y')]],
          ["Help",
           ["How to Play",         function(e) showHelp("help"),  ks('T')],
           ["About Pnuts Sokoban", function(e) showHelp("about"), ks('A')]]]

    bar = menubar(f, ui)
The layout() function is similar, allowing you to build up UIs declaratively using any combination of objects and layout managers.

The Sokoban UI really only has five objects: four widgets in the top row (each consisting of a two labels), and the board. Because the four display-widget groups are just copy/paste code except for the names, I generated them with the Pnuts mapFunction() function, which I renamed to fmap(), since the traditional name is map(), but that's taken in Pnuts already. So generating the Sokoban UI is really short:

function makeSokoFrame() {
    f = JFrame()
    f.locationByPlatform = true
    f.defaultCloseOperation = WindowConstants.EXIT_ON_CLOSE
    bind(f, "keyPressed", handleKeypress)

    makeMenuBar(f);
    fmap(function(label, name) [BorderLayout, [],
				["West", JLabel(label + ": ")],
				["Center", ctx["s_"+name+"Display"]=JLabel()]],
	     {"Moves" => "moves", "Pushes" => "pushes",
	      "All Moves" => "allMoves", "All Pushes" => "allPushes"},
	 args=list())
    args.add([makeSokoPanel(), "colspan=4,expand=xy"])
    layout(f.contentPane, [PnutsLayout, "cols=4"] + args.toArray())

    ctx.s_sokoFrame = f
    updateFrameTitle(); updateDisplays()
    f.pack(); f.visible = true
}
All that's missing from the function above are the implementations of makeMenuBar() and makeSokoPanel(), both of which are equally short. So generating the game UI in Pnuts wound up being only about half as much code as it was in the other JVM languages.

PnutsLayout seems pretty flexible, and it's certainly easy to use. In fact, I rewrote two big sections of the UI to use the heirarchical layout stuff (after just having read about it), and both times it worked perfectly -- I was so surprised both times that I had to go back and add in deliberate UI mistakes, to make sure it had really picked up my changes. It's one of those features that "just works".

It looks like Pnuts has a huge number of other shortcuts, including support for servlets and web templates, image manipulation, the java.nio package, JDBC, JDO, XML, the Java Security APIs, and many others. I didn't get a chance to test all of these modules in my little app, but the ones I did test always worked perfectly and were usually well-designed.

None of these features may seem like much, but when you add them all up, you've got an expressive, fast, stable, fun language that's perfectly suited for the JVM.

Weaknesses

Pnuts has lots of strengths -- it's a great scripting language. However, it also has a handful of maddening annoyances; I'll cover them in this section. The following section has wish-list items that are simply missing from the language.

1) No Classes

The blatantly obvious weakness is that Pnuts doesn't have "first-class" classes. You can create subclasses of Java classes, sort of -- here's an example from Sokoban:

function makeSokoPanel() {
    ctx.s_sokoPanel = root = new JPanel() {
        minimumSize()   { preferredSize() }
        preferredSize() { Dimension(pixels(MAX_COLS), pixels(MAX_ROWS)) }
        update(g) { paint(g) }
        paint(g) { ctx.s_finished ? drawFinaleScreen(g) : drawBoard(g) }
    }
    bind(root, "keyPressed",   handleKeypress)
    bind(root, "mousePressed", handleMouse)
    bind(root, "mouseDragged", handleMouse)
    root
}
Here we've created a JPanel() instance (which we assign simultaneously to ctx.s_sokoPanel and the alias "root"), and we've defined four methods on this instance: getMinimumSize, getPreferredSize, update and paint. It does what I need it to do -- creates a panel that will call my drawBoard() functions and respond to mouse and keyboard events.

But it's creating what amounts to an anonymous class, which means we can't create subclasses of that class, at least not easily. There doesn't appear to be syntax for it, at least as of this writing, and you have to use some special library calls to assemble classes that can be subclassed (if it can be done at all).

You can do everything I needed to do to implement Sokoban, which puts Pnuts way ahead of most of the 190-odd JVM languages out there. But until Pnuts makes it trivially easy to define classes and interfaces usable by Java code, Pnuts will probably never really take off, as it'll be mostly relegated to scripting, prototyping, and unit testing.

2) Top-level variable scope problems. Although Pnuts does a far better job of handling lexical scoping than Groovy or Jython, it still has problems with top-level script variables. For instance, what do you think the following code should print?

    a = 0
    function foo() { a = 5 }
    foo()
    println(a)
I think most people would agree that it should print 5. However, it prints 0, because Pnuts screwed up the lexical scoping for top-level (and only top-level) variables. If you assign to a variable in a function, the name resolution doesn't look at the top level, and it thinks you're creating a local variable rather than assigning to the global one.

Inside functions, however, the code honors lexical scoping:

    function bar() {
	a = 0
	function foo() { a = 5 }
	foo()
	println(a)
    }
    bar()
This prints 5.

It's pretty lame when a scripting language has problems assigning to script-level variables. It's almost always the result of a really bad language-design decision (namely, not requiring you to declare local variables), and an astounding number of language designers fall into this trap.

Pnuts, Jython, JRuby and Groovy all have this problem. The problem has recently appeared in Kawa as well, for Java threads (but not Scheme threads). Only Rhino and Nice handle lexical scoping perfectly for both internal and top-level variables. (It's moot in Java and NetRexx, neither of which allow "top-level" variables outside of a class definition. NetRexx still manages to screw up its class scoping, though.)

Fortunately (and almost as if by accident), PNuts provides a workaround for the lexical scoping problem. Each Pnuts script has a Context object that behaves very much like a hashtable -- a namespace, anyway. You can fetch it with getContext(), and you can add properties to it using bean-prop syntax or hash-access syntax:

> ctx = getContext()
pnuts.lang.Context@186c6b2
> ctx.a = 2
2
> ctx.a
2
> ctx["a"]
2
> ctx["a"] = 3
3
> ctx.a
3
I was able to use this to "fake" top-level variables. That's why all the variables in my Pnuts Sokoban are prefixed with "ctx." It's not the worst workaround in the world. But frankly I'd rather not have to use it. Lexical scoping is lexical scoping -- you should always be able to determine the nested lexical scopes by looking at the code nesting. No exceptions.

3) Array/Collection impedance mismatches

Pnuts tries hard to give you a consistent syntax for collections, including Java primitive arrays. Unfortunately, it falls short of this goal, and the results can be pretty bizarre.

Here are a few examples. There are many more, but this should give you a taste of what I mean.

> a = []
[]
> a += [[1, 2]]
[[1, 2]]
> a += [[3, 4]]
[[1, 2], [3, 4]]
> a.class
java.lang.Object[] class
> a[0]
[1, 2]
> println("So far, so good.")
So far, so good.

So we can create Object arrays with the [] operator, and we can concatenate other Object arrays to them with +=. Very convenient.

Now let's try the same thing with an int array:

> a = int[]
int[] class
> a += [[1, 2]]
java.lang.IllegalArgumentException : class [I, [Ljava.lang.Object;@cdfc9c
What? Oh, we're off on the wrong foot because of a misunderstanding. It printed "int[] class" when we created the variable, but if you look closer, we've actually created a Class literal:

> a = int[]
int[] class
> a.class
java.lang.Class class
Fair enough, although the syntax is unfortunately very similar to an array declaration. Can't be helped, though. Let's try it with a real int array now:

> a = (int[])[]
[]
> a += (int[])[1, 2]
[1, 2]
> a += (int[])[3, 4]
[1, 2, 3, 4]
Oops. Array-concatenation isn't the same as append; it actually concatenates the arrays, sort of like string concatenation. Fine. Let's try doing what we did with the Object[] example above:

> a = (int[])[]
[]
> a += [[1, 2]]
java.lang.IllegalArgumentException : argument type mismatch
Wait! We just did exactly the same thing above, at the start of the "so far, so good" example. It works for Object arrays; why doesn't it work for int arrays? Maybe it needs a different syntax; let's try a few others:

> a += [(int[])[1, 2]]
java.lang.IllegalArgumentException : argument type mismatch
> a += (int[][])[[1, 2]]
java.lang.IllegalArgumentException : argument type mismatch
> a += (int[][])[(int[])[1, 2]]
java.lang.IllegalArgumentException : argument type mismatch
How do you do it? I'm not sure. All of the attempts above look reasonable, and none worked. To demonstrate just how reasonable I think they are, consider that all of them are legal Pnuts syntax:

> b = [[1, 2]]
[[1, 2]]
> b = [(int[])[1, 2]]
[[1, 2]]
> b = (int[][])[[1, 2]]
[[1, 2]]
> b = (int[][])[(int[])[1, 2]]
[[1, 2]]

In other words, every single one of my attempts should have worked.

If there's a magical syntactic incantation to make this work, I couldn't find it. But the point is that it shouldn't need any special syntax. My very first try:

a += [[1, 2]]
should have worked, just as it did for the Object[] scenario.

This is enough to drive you completely nuts when you're programming. It's maddening. You're trying to think about your application domain, and you wind up chasing down what's either a bug in the language or an inexplicably hostile syntax. Either way, you're not happy.

So the primitive-array support in Pnuts pretty much sucks. But what about Collections like java.util.List? Let's take a look:

> a = list()
[]
> a += [[1, 2]]
java.lang.IllegalArgumentException : [], [Ljava.lang.Object;@13bad12
> a.add([[1, 2]])
true

First annoyance: lists can't use the concatenation operator. I don't see why not; it's not as if the syntax conflicts with anything else. It just barfs. Instead, you have to use the add() function.

Next, printing of arrays and collections is weird and unpredictable. Consider this example:

> > a = [1, 2, 3, 4]
[1, 2, 3, 4]
> a.class
java.lang.Object[] class
> a = (int[])[1, 2, 3, 4]
[1, 2, 3, 4]
> a.class
int[] class
> a = list()
[]
> a.add([[1, 2]])
true
> a
[[Ljava.lang.Object;@19106c7]
> a[0]
[[1, 2]]

Lists evidently know how to print themselves, and arrays know how to print themselves -- but when an array is an element of a list, it mysteriously forgets how to print itself. Lame.

This is all just scratching the surface. Pnuts has a lot of support for integrating collections into the syntax and the library functions. But in any given situation, you can almost certainly expect different behavior depending on whether you're using a primitive array, an Object array, or a Collection. And that just plain sucks.

Pnuts has a lot going for it, so it's a shame that something you use so often (arrays and collections) has so much friction. It's enough to keep me from really loving the language, and I suspect other folks will have the same issues.

Wish List

I could wish for a hundred additions, but I've chosen the handful that I think would matter the most. The first one is a gimme.

1) Built-in "functional" module. This is a no-brainer. It already exists, and it's very powerful (e.g. it allows infinite lazy lists), but you have to download a separate jar file. That's pointless. It should be included in the standard distribution.

2) Fix arrays and collections. I'd like to see more work done on making arrays and collections work interchangeably, with the same syntax and semantics, and automatic conversions between them.

3) Automatic argument destructuring. In the simplest case, it would be very nice to have parallel assignment, e.g. "x, y = y, x", or "x, y, z = 1, 2, 3".

I'd also like to be able to say things like this:

for ([x, y] : pointList) ...
This assumes you have a list of 2-element arrays; I want each 2-element array to bind to the variables x and y, on each iteration. Jython and Nice both provide ways to say this.

This feature would also be very useful in function declarations:

function foo(x, y) x + y
a = [1, 2]
foo(a)
Destructuring binds are well-studied and reasonably well-understood, provided you don't get too far into the pattern-matching research space. Many other languages have at least some support for this feature, including several of the JVM languages in my evaluation. It would go a long way towards giving Pnuts more expressive power.

4) String support. Strings are just so important, and almost every language gets them wrong in at least one major way.

Pnuts gets a few things right. It has multi-line string literals, and you can index and slice strings:

> "foobar"[1..2]
"oo"
> "foobar"[1..]
"oobar"
> "foobar"[1..-1]
""
Oops! Pnuts messed up that last one. Everyone knows that a negative upper-bound on an array slice means to index backwards from the end. Perl may have crawled or shambled straight out of H.P. Lovecraft's worst nightmare, but it got some things right.

I can live without built-in syntactic support for regular expressions, but I detest having to double-escape all the metacharacters in a regexp string. Pnuts, thank goodness, provides a way to do what it calls "verbatim" strings, delimited using backquotes:

> "foo   bar".replaceAll(`o*\s+`, "")
"fbar"
This is very important for helping make your regular expressions at least somewhat readable. Pnuts scores major points for providing raw-string syntax.

Most importantly, though, Pnuts lacks a decent string library. Strings are sequences, so essentially every function you can use on an array or list should be available (in some form) for strings as well. Pnuts doesn't have higher-order functions for strings, and it's also lacking basic but often important string functions like trim-left, trim-right, and reverse.

5) Emacs Mode. C'mon guys. Pnuts is the only JVM language in my evaluation (out of 10 total) that doesn't have Emacs support. I was able to get by (barely) using javascript-mode, but it required putting semicolons after every statement, then removing them after all the coding was done. (Which took all of ten seconds in Emacs -- just try doing that in Eclipse...)

6) Extensibility. When it comes right down to it, Pnuts doesn't provide any extensibility or language-customization functionality. There's no macro system, no preprocessor, no template system, no aliasing, no operator overloading, no metaclasses or metaprogramming... absolutely nothing that allows you to change or extend the language to suit your needs.

This, more than anything else, is what keeps Pnuts from being a world-class language.

As a trivial example, "function" is by far the most common language keyword in my Pnuts Sokoban implementation. It occurs more frequently than "if" by a factor of 2 (nearly 100 times total). As you can probably imagine, I kept wishing for a way to shorten it, but there simply isn't a way. The language can't even do simple keyword aliasing, let alone more sophisticated things like macros.

Summary

Pnuts was very pleasant to work with, with the notable exception of its arrays and collections. It's more expressive than JavaScript, so it was generally nicer to work with than Rhino. Pnuts is certainly far more pleasant to work with than Groovy, for reasons that I'll outline in my Groovy article.

I like Pnuts, but not as much as Jython, JRuby and Kawa. It's just not as expressive as they are. I'd say Pnuts is about equal with the Nice language. In fact, aside from the static typing, Pnuts and Nice have a surprising amount in common. Both are good languages with very good performance.

Pnuts is fast, stable, mostly bug-free, and well-documented. Any bugs you find will be fixed quickly, and I've no doubt the language and libraries will continue to improve over time.

If you're looking for an expressive, dynamically-typed JVM scripting language, I think Pnuts is your best option today, particularly if performance matters a lot. Although Pnuts isn't as expressive as Jython, Kawa or JRuby, it's a lot faster than they are. And I'd choose Pnuts over Rhino, NetRexx or Groovy any day of the week.

Visit the Pnuts home page.