|
|
In the film Hudson Hawk [yes, I know], Richard E. Grant can be heard to
assert that "happiness comes from the fulfilment of /goals/!" Now this
isn't a film that you should ever take too seriously. But this quote
does seem to have an air of truth about it.
It's roughly 10 years now since I seriously wrote any Java code. I often
write psuedocode using Java syntax, but it's a very long time since I
actually compiled any Java with the serious intention of /running/ it.
So I just spent the past month or so coding in Java.
The result is Logic Box, as you've all seen. I'm still not completely
happy with it, but there's definitely a sense of achievement to be
gained from actually producing a /finished/ program that does something
[vaguely approaching] useful.
On the one hand, I could have written this program in Haskell in about
half an hour. Haskell already /has/ sophisticated parsing libraries
which are trivial to use. The structural inspections are all 1-liners.
The data transformations are similarly trivial. And, of course, I've
written this program several in Haskell several times before, so I
already know the best way to factor it.
On the other hand, the Java program is a ~200KB JAR file which runs
/everywhere/. A Haskell "hello world" program compiles to 1MB on
Windows, and runs only on Windows. More to the point, my Java program
has a GUI. Sure, you can do that in Haskell, but it's a hell of a lot
more work.
For a start, you need to install GTK (the GTK developers keep moving the
damned download, and it's hard to figure out exactly which file(s) you
need), and manually add it to your search path. Oh, and I hope you
weren't using any /other/ applications that need GTK. Then you need to
get the Haskell GTK bindings to compile, which is quite a long,
convoluted dance. Oh, and Glade [the UI painted] no longer seems to be
part of the standard GTK bundle, so you need to get that separately. It
also depends on several other GTK packages which aren't really bundled
for Windows properly, so you have to fiddle with .pkg files to try to
guess what the correct settings are...
By contrast, once I have NetBeans installed, I can throw a
non-functional Java GUI together in /seconds/. Adding code to make it do
something is also fairly trivial. You /do/ have to deal with the extreme
brain-deadness of the Java APIs. But if you can fight past that, you get
to have a API application which anybody can run without having to do
some convoluted dance with installing the entire GTK runtime. (Or,
alternatively, me figuring out exactly which DLLs are required and
sending complicated instructions on where to put them.)
So the languages - and more specifically, the ecosystems in which they
exist - have different strengths and weaknesses. In Haskell, I spend
most of my time fighting the fact that Windows isn't Linux, and that C
isn't Haskell. In Java, I spend most of my time fighting the absurd
APIs, and the general stupidity of the actual programming language.
Java is a very, very, stupid language, by the way. It's a long time
since I've really done any programming with anything that isn't Haskell.
I'd forgotten just how much of my time I end up wasting typing "public
static void Log(BufferedWriter out)" and similar nonsense. But more than
that, I'd forgotten just how much time is spent trying to find
workarounds for the stupid limitations of the language, the bugs, the
glitches, the stuff that doesn't work how it's supposed to, the stuff
that isn't documented, etc.
Java has generics now. It didn't, back when I used it. One of the things
I wanted to see is what the syntax is like, and how you use it. I've
since come to the conclusion that the answer is "you don't". The system
is just too weak and feeble. Because generics was hacked in as an
afterthought, it really, really doesn't work properly. You want to
create an array who's element type is determined at run-time? Oh, I'm
sorry, that's impossible. The language does not allow it. Have you
considered using an ArrayList instead?
There's lots of other stupidity too. Java has this retarded system where
everything is a class, *except* int, long, double and bool. [OK,
actually there's a few more...] These are primitive types, which
completely different semantics. For performance reasons.
You know what that means? It means that you /cannot/ use these types
with generics. Which means that you /cannot/ use them with any of the
standard container types. Unless you wrap them, to turn them into
objects, thus negating every single one of the performance benefits
which lead them to be non-object types in the first place. THE STUPID,
IT BURNS!! >_<
Interestingly, you /can/ make arrays out of them without boxing. Because
arrays don't use generics, they use a system equivalent to generics but
hard-wired into the language from day 1, which thus ACTUALLY WORKS
PROPERLY. If they had extended this to allow you to use it for your own
classes, we'd be golden. But that would break backwards compatibility,
so noooo...
You know what? Eiffel allows you to /choose/, on a class by class basis,
whether objects of this class should have reference semantics or value
semantics. C++ allows this in a variable by variable basis. Both of
these languages manage to treat reference objects and value objects in a
consistent, uniform way that works properly with generics. But no, not
Java. :-P
Let me just back up a bit and summarise what I just said: Java sucks.
Badly. The language is complex, bloated, weak and flabby. It's a problem
that seemingly *all* mainstream languages share to some degree or
another. Which is crushingly disappointing, because mainstream languages
let you actually *do stuff*. Useful stuff. Like have GUIs, or access
multimedia data, or connect to databases, and so on.
Unfortunately, it seems that only obscure languages like Haskell are
actually well designed. But such languages suffer from it being
impossible or at least unreasonably difficult to *do* anything that
involves talking to the outside world. It's really, really frustrating
that there isn't a language that does both.
Having said all that, I /was/ eventually able to rewrite Logic Box in
Java, and it /does/ work. It's the first time I've tried to implement
Logic Box with interactive stepping. Because, let's face it, from a CLI
that would just be too horrible to be useful. But from a GUI, it's a
plausible thing to want to do.
(I did once implement Lambda Box as a CGI script, so that I could have
an interactive UI without losing my mind. It's a sad day when generating
HTML and sending it over HTTP is /easier/ than writing a native GUI...)
The actual kernel of Logic Box is not difficult to write. Just tedious.
Of course, writing a program that solves predicates as much simpler than
writing a program that shows you what it's actually doing while it does
it. In true iterative-prototyping style, I tried several different
approaches.
Initially I had an Expression class with a Unify() method, and a
Predicate class with a Solve() method. But eventually, I decided on
having a separate Solver object for each kind of predicate, and having
expressions and predicates as immutable objects, consequently with far
fewer methods.
Initially unification used a Unify() method defined in Expression and
overridden by each type of Expression class. But today it's a huge
tangle of nested if blocks using the "instanceof" operator instead. A
retrograde step, perhaps, but it works and it means that all the
unification logic is in one place instead of fifteen places that need to
cooperate intimately.
The actual solution process itself is a state machine. Initially each
Solver contained a set of variables holding the necessary data for every
possible state, most of which starts out as null or similar invalid
values. Then, in the Step() method, a complicated tangle of logic
analyses these variables and attempts to reverse-engineer the current
state by seeing what variables have and haven't been set.
This isn't a great idea. In particular, every time I add a new variable,
or change the order of states or something, the state detection code
tends to break horribly, and I spend forever chasing obscure bugs where
half the code behaves as if we're in one state, and half in another
state. The solution, obviously, is to encode the current state
/explicitly/, rather then trying to "guess" it from implicit information.
The solution cries out for first-class functions, but Java has no such
thing. Ideally, we'd have a Solver that holds all the data, and a
pointer to the Step() function. When the Step() function executes, it
updates the data in the Solver, and then updates the function pointer to
point to the next Step() function. But we can't do that, because Java
does not have functions.
Instead, every state that the state machine can be in becomes an entire
class. A private nested class (because nobody else needs to see it), but
a class none the sell. Oh, but there's some brokenness with inner
classes and disambiguating between local variables, inner class fields
and outer class fields. I wasted a lot of time sorting that one out.
I'm still not 100% happy with the result. I kept the data in the Solver
object rather than the Step object so that I could implement the GUI
display code just once. But it turns out that different states ought to
display differently, which leads to the Step object needing to "talk to"
the Solver object rather a lot, and the display code becoming very
complicated. It seems that the correct thing to do is just move
/everything/ related to a state into the actual State object.
Also, currently you can advance the simulation forwards only. I can't
tell you the number of times I've clicked the button three times and
then been like "oh, wait, go back!" But you cannot go back. This is the
beauty of destructive updates; the old state has been lost, and that's
that. I think with some work, it should be possible to allow going
backwards as well as forwards.
I /also/ think the GUI display needs some serious work. There are too
many redundant steps. The GUI displays so much information by default
that you get lost in a sea of data and it's difficult to understand
what's really /happening/. I think the display needs to be more focussed.
(Regarding redundant steps: Consider the predicate "x = 1". This has the
following steps...
1. LHS == RHS?
2. Bind x.
3. Simplify solution.
4. LHS = RHS?
5. LHS == RHS. Predicate solved!
6. Sending solution...
7. Done.
That's 7 times you need to click the Step button to solve this trivial
little predicate. Surely we can do better...)
Parsing was fun too. And by "fun" I of course mean "not fun at all". :-P
During testing, it rapidly became clear that writing half a page of Java
code every time I wanted to test a different aspect of the solver was
just not good enough. So a parser had to be developed early on.
I imagine that somebody somewhere has probably invented either a parser
library or a parser generator for Java. But I coded mine by hand. The
/lexing/ is fairly easy. (Once you get around Java's gigantic APIs to
the bits you actually want.) The /parsing/, not so much.
Initially I tried to implement parsing in Java the way you'd implement
parsing in Haskell. DON'T DO THIS! It doesn't work at all! First of all,
a monadic parser really /requires/ first-class functions. Java can
sort-of simulate these, but the result is horrifyingly complex and
inefficient. And Java's generics really, really don't work properly, so
trying to make the parser's result type a generic parameter is an
exercise in futility.
I am not well-versed in parser theory. I have no idea what the hell the
difference is between LL1, LR, LALR, top-down parsing, bottom-up
parsing, recursive descent, or whatever. I am just a humble computer
science (*cough* information technology) graduate. :-P
What I do know is that I started out with a system of "levels". Each
level takes a container of terms, and groups some of them together into
larger terms. The input to level 1 consists of single tokens. Level 1
turns anything expression-like into expressions, but leaves all other
tokens alone. Level 2 therefore receives a mixture of tokens and full
expressions. (This requires a Java analogue of Haskell's "Either" type.)
Level 3 tries to group pairs of expressions into equality predicates.
Level 4 thus receives a mixture of tokens and predicates, with no
expressions remaining.
The good news is that this frameworks makes it easy to dump the result
of a given level out onto the screen to check everything is parsing
right. The bad news is... there's quite a lot of boilerplate code, and
it's hard to get things like bracketing to work. As soon as you see an
open bracket, you need to jump to another level in the hierarchy.
The final, /working/ implementation is different. There's a function for
parsing each construct. Null is returned on EOF, otherwise an exception
is thrown if the required construct is not present. The functions for
higher precedence operators call the functions for lower precedence
operators to parse them. At the very bottom is a function which parses
an equality /or/ a bracketed predicate of any type.
Again, things get tricky when you get to AND and OR. You can tell
whether a predicate is bracketed or not by looking at the first token.
You can statically tell where equality predicates are supposed to be
situated. Exists always starts with "~". And so on. But for AND and OR,
you need to "look ahead". For this, I have to buffer tokens, so they can
be "peeked" multiple times without advancing to the next token.
Eventually I got it to work. (I'm surprised nobody has yet found a way
to break my parser. I assume nobody has bothered trying...) Only the
most recent prototype actually includes proper error reporting. All
earlier versions simply throw an exception and die if anything
unexpected ever happens. The current parser attempts to indicate /what/
actually went wrong. This is a surprisingly non-trivial task!
Later I had to go back and hack in the ability to parse named predicate
definitions. (Originally only the actual predicate in the input box was
parsed.) And to load and save predicate libraries from file. In the
process, I once again had to battle against Java's ridiculous I/O APIs.
But this, too, now works - more or less. (I really ought to add a way to
have comments though...)
Building the GUI was a somewhat frustrating exercise. The UI painter
makes it easy enough to draw what you want. Except that the UI painter
uses the "Windows" look and feel, while the generated code defaults to
the "Nimbus" look and feel, and AFAIK there is /no way/ to change this.
I actually deleted the look and feel code, so now it defaults to "Java".
This is why Logic Box looks so ugly. (In particular, the tool bar
doesn't look like a tool bar, and the separators between the buttons are
invisible.)
It took some doing to figure out how to wire up the buttons to some
executable code. NetBeans was asking me to select predefined Action
objects or some mumbo-jumbo like that. (I looked it up; apparently this
is the Swing API's way of getting around the lack of first-class
functions. So you can have a button and a menu item that do the same
thing, and you don't have to code it twice.) It turns out you can just
double-click the button, and NetBeans generates the stub method for you.
Figuring out how to work a tree was harder. As you'd expect. It seems
it's impossible to modify an existing tree. So every time you click
Step, the entire tree is deleted and rebuilt from scratch. (Probably
explains part of why Logic Box is so slow.) I also don't understand why
you cannot simply say "here is a tree". Instead, you have to have a
series of DefaultMutableTreeNode objects, then you need to create a
DefaultTreeModel object (which you cannot do until /after/ the tree
nodes are built), and finally you can pass this to the JTree object
which is the actual GUI component.
You would /think/ that DefaultMutableTreeNode would have an attribute or
a method or something which would let you select whether it is initially
expanded or collapsed. You would be wrong. Instead, you have to go to
the JTree object, pass it the TreePath to the DefaultMutableTreeNode
that you want to expand or collapse, and call either expandPath() or
collapsePath(). (No, there isn't an expandPath() method that takes a
boolean. You have to write an if-statement yourself. Every single time.)
You have to refer to the node you want to expand or collapse by path.
You cannot simply use a pointer to it. Which means you can only expand
or collapse the node once the tree is completely built (because until
then, a path doesn't exist). Which means you have to do /two/ passes
over the input tree to construct the output tree.
You would /think/ that DefaultMutableTreeNode.getPath() would return a
TreePath. You would be wrong. It returns a TreeNode[]. So what you
/actually/ have to do is
TreePath path = new TreePath(node.getPath());
Isn't that silly?
On top of that, presumably in an effort to be helpful, expandPath()
expands the selected node /and all parent nodes/. Which means to make
the expansion state of each node match what the input tree actually
requested, you have to scan the input tree bottom-up rather than
top-down. Which is just weird...
I also love how when you create a DefaultMutableTreeNode, you can select
whether this node can have children or not. You would /think/ this
dictates whether the graphic shows a folder icon or a file icon. You
would be wrong. The number of actual children dictates that. The setting
here just controls whether an exception is thrown when you try to add
children. >_<
In short, tree-building is a pain. OTOH, it turns out that you can use
HTML in the node titles. (If the title text begins "<html>".) And by
"HTML", I mean "some SGML derivative vaguely resembling HTML 2". I
quickly discovered that it really, really does not work well. For
example, if you set the background colour of some text, and inside that
span you set the foreground colour, that /cancels/ the background
colour! There is no style cascading here; if you want that, you have to
reimplement it by hand.
If you look at the "about" or "quick reference" pages, you will also see
that the general rendering quality is really quite low. (At least in
these panes, you can use real HTML with real CSS!) But hey, what do you
expect if you try to embed an entire web rendering algorithm into a GUI
library as an afterthought?
In summary, most of my time was spent wrestling with weaknesses of the
Java language, and the overly complex APIs you have to use with it. Many
of the bugs in the actual kernel are due to workarounds I had to do
because the natural way to encode the solution is impossible. It's been
a frustrating experience.
Then again, if I posted a Zip file containing a random Windows
executable and half a dozen DLLs and said "run this!", I doubt anybody
would. And I'm sure I'd be receiving a lot more reports about how broken
it is. Written in Java, this stuff actually works.
I'm still not completely happy with the GUI. It works fine for the
utterly trivial predicates I used during testing. However, trying to
write, debug and run "real" programs in it, I find it to be inadequate.
- The tree view is too complicated. It needs less stuff displayed by
default, and probably more highlighting to indicate what part is
currently running.
- The solvers have too many redundant steps.
- The "solve" and "more" buttons run 1,000 steps, but it turns out
sometimes that locks the entire GUI up for as much as 30 seconds. There
ought to be some kind of progress display, the work should be in a
separate thread to avoid stalling the GUI, and there ought to be an
abort button. Also, perhaps the process should be limited by wall-time
rather than number of steps. (Wall-time seems oddly variable.)
- There ought to be a way to put comments in source code files.
- It really irritates me that the load and save requesters default to My
Documents rather than the current folder. There must be some obscure
hack to fix that...
Whether I will fix these issues or move on to another project remains to
be seen. Originally my goal was to write the program in Java (which is
object-oriented), and then have a go at translating it to C++. (Without
the GUI, because I have no idea how to do GUI programming in C++, and it
sounds highly non-trivial.) But actually, it looks like it's going to be
impossible, because it fundamentally /requires/ abstract classes, which
are hard in C++. So probably another Java project then...
Post a reply to this message
|
|