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:
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.:
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:
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":
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:
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:
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:
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?
Inside functions, however, the code honors lexical scoping:
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:
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.
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:
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:
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:
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:
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:
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.
|