The inside scoop on what's happening with Tapestry ... from the creator of the Apache Tapestry framework. Plus all the normal, random thoughts on coding and technology.
Tapestry Training -- From The Source
Let me help you get your team up to speed in Tapestry ... fast. Visit howardlewisship.com for details on training, mentoring and support!
Here's a quicky I just put together for a client to generate IE conditional comments. This isn't a feature supported by Tapestry's JavaScriptSupport (for libraries; it is support for CSS files).
Fortunately, this is something that comes together in Tapestry in almost no code:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
And that's the whole point ... the URLs for the referenced JavaScript libraries are now Tapestry assets, and will have access to Tapestry's asset pipeline as unique resources, including GZip compression and minimization.
It is always important to me that Tapestry not get in the way: frameworks that lock you down and prevent you from accomplishing your goals should not be used. Tapestry has a long history, and there are certainly many areas that could have been implemented differently, or simply not implemented at all, but it's nice that most edge cases, like this one, have a simple solution.
Perhaps no subject in software development is as important to me as feedback, which to my mind, is the language, library, or framework providing information back to the developer when things go wrong. A language, library, or framework that provides poor feedback is creating barriers to its own adoption.
There's a possibly apocryphal legend about early FORTRAN compilers. Back in the day (thankfully, even earlier than my personal
back in the day), you would code up your FORTRAN code on punch cards and deliver them to an operator. At some convenient time (for the operator), the compiler would be started and fed the stack of cards. Assuming the cards had not been bent, folded, spindled, or mutilated ... and that the program itself contained no errors, you would get a compiled program somewhere. If anything failed, you would simply get the error message "FAIL". Nothing else.
That's bad feedback.
It's important to feel that you are in control of your own tools; that's part of the love/hate relationship so many developers have with their IDEs: the IDEs certainly enable a lot of productivity ... but what you always end up remembering is when the IDE gets in the way of some really critical, really difficult stuff. Or when it crashes mysteriously at a critical moment. Or when it just
won't dosomething and won't tell you why not.
Likewise, any tool (or language, library, or framework) that provides bad feedback,
especially when things go wrong, is building a perception that it is hard to use overall. We're a fickle bunch ... we'll be off to find the Next Big Thing instead.
Which brings us to my favorite programming language, Clojure. When things are going right in Clojure they are going
very, veryright. However, when things go wrong in Clojure it can be very painful.
First of all, Clojure's basic mechanism for any kind of error is to simply throw an exception. Since Clojure is a Lisp, the difference between compile time and runtime is not so easily distinguished ... a running Clojure program is often loading source and compiling code as well as running the application.
And what happens to exceptions? They are thrown in one piece of code and, more often than not, caught, wrapped, and re-thrown in containing stack frames. Ultimately, this whole stack of exceptions gets spewed out to the console.
This isn't quite bad as that FORTRAN compiler; however, even on my big 30" cinema display, a Clojure compilation failure always sends me scrolling backwards in my terminal window to see the actual error and (with more than a pinch of luck) the file name and line number containing the error.
It's much worse when it comes to runtime failures in my own code; it is likely that the exceptions will be nested even deeper than the number of levels intituted by the Clojure compiler and REPL. Beyond that is the issue of laziness ... program execution goes a little topsy-turvey from your code due to laziness, as what code is executing is typically driven by what particular bit of lazily-computed data is needed at any particular instant. That takes a little getting used to for new Clojure developers.
And so, deep in your code, you passed a string when a keyword was expected, or you passed a map when a seq was expected, or any number of other similar situations ... and now you are faced with an exception and its stack trace.
Here's the ugly not-so-secret of Clojure: it's a light and tasty frosting on top of a cake of sawdust and rusty gears: Java. Every Clojure function must be converted into a Java class in order to execute within the JVM. Java places somewhat arbitrary limits on the class and method names: Where Clojure loves dashes in function names, Java doesn't allow them. Likewise, Clojure loves all kind of other punctuation, with functions named "+" or "string?", that Java forbids as well.
Clojure
manglesthe Clojure namespace and function names to fit into the Java world, and nothing in the exception reporting chain does anything to make the result any prettier. Here's an example:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
All the information is available ... but you have to work to get at it: You need to mentally de-mangle the Java class names back to Clojure namespaces and function names. You have to ignore a lot of stack frames that are really the infrastructure supporting Clojure on top of Java. You have to assemble the meaningful stack trace (the last and deepest one) from the parts it extends off of the prior stack traces. You have to ignore the numeric ids tacked on to the end of names to help ensure uniqueness.
This is not great feedback.
What if we could provide just a little bit better feedback? What if we could do a lot of that name-demangling automatically, and present the same data in a more helpful way? That's what I've been working on as part of the io.aviso:pretty library for Clojure.
Pretty can be used to format that same exception just a little bit nicer:
The exact format is still in-flux because it is currently so wide; fortunate, the interesting stuff is always to the left!
This same approach appears in the related io.aviso:twixt library, modified to take advantage of how much more richly data can be presented in a web page:
However, this is just the start. Providing truly useful feedback is more than just putting a patch on exception reporting. It requires attention to detail throughout the tool. This has been the case for Apache Tapestry, where a significant amount of the framework is about detecting and reporting errors in a useful way.
Twixt has a set of utilities to address this as well; a way of tracking what the application is doing so that exceptions can be reported. The end result is an "operations trace" that can nest to an arbitrary depth and is used to reconstruct how the application arrived at the point of failure.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This extra layer of code can pay big dividends when things go wrong; the nesting of operations can be critical to understanding the underlying problem; the operations trace combines the very local scope of the failure with a larger scope that explains why the failed code was invoked in the first place.
So I'm very excited by the possibilities, but to provide truly universal, useful feedback in Clojure may not be something that can be effectively added in from the outside: it has to be a priority for the core Clojure developers, and bred into the fabric of the Clojure compiler and runtime. That's something I'd love to see ... or help with.
I'll be spending about three weeks in Gainesville, Florida this coming February (2014). For the wife and kids, its about visiting the grandparents, hiking, fishing, and playing. For me, its mostly about work, sigh. In any case, I'm told that Gainesville has gotten quite the start up culture going recently ... I'd love an opportunity to spread the word about Tapestry, Clojure, or AngularJS (or Spock, or Geb, or any other presentation I have in my bag of tricks). Think of this as a chance to put your "brown bag lunch" on steroids for a day.
Just get in contact with me if you are interested; see my contact information on my home page.
Clojure can simulate named parameters, or a mix of positional and named parameters.
This is really old news, dating back to Clojure 1.2 if evidence serves, but I always have trouble finding this exact syntax, and it is not fully obvious.
Say you want to accept a certain number of optional parameters with names, and perhaps, defaults. It turns out you can combine rest parameters (the ones that come after a &) with map destructuring.
In the simple case, you don't care what the possible options are, and you don't have any defaults.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
The keys and values you pass to this function, say (named-parameters :foo 1 :bar 2), are collected together as symbol params.
If you don't provide an even number of values (that is, the same number of keys and values), you'll get a reasonable exception, such as java.lang.IllegalArgumentException: No value supplied for key: :bar
Easy-peasey ... but you need to extract values from the params map to use them inside the function, e.g.: (:foo params). It would be nicer to have them as symbols, just like with normal positional parameters. This is also easy, by leveraging more of the features of map destructuring:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
The :keys identifies the keywords expected in the map; it works backwards from the symbol name, foo, to the expected keyword, :foo.
There's also a :syms (for when the keys
are expected to be symbols) and :strs (for when the keys are expected to be strings).
The :or identifies default values for each symbol.
The end result is that we can rely on defaults from :or or provide our own values when invoking the function:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
And since this is Clojure, you can combine all of these things together quite easily ... some positional parameters, some named, some identified by keywords, others identified by symbols.
I've been working through the Tapestry issue list over the last couple of days; I'm closing a lot of duplicate and invalid bugs, and fixing some low-hanging fruit. Still, the number of fixed bugs already is impressive:
Bug
[TAP5-800] - Server side error during provideCompletions event for Autocompleter mixin is not reported on the client side properly
[TAP5-860] - JavaScript errors when adding validations to checkboxes
[TAP5-986] - A request can fail with an NPE in some cases, when a Tapestry page is acting as the servlet container error page
[TAP5-1193] - tapestry.js prevents using the back/forward browser cache
[TAP5-1200] - Nested Ajax calls result in a call to $T(null)
[TAP5-1569] - Grid should implement the ClientElement interface
[TAP5-1601] - Sometime a method that references a field with a conduit will not be instrumented, resulting in an NPE accessing the field itself
[TAP5-1668] - JavaDoc for @Parameter.value() should be clearer about the empty string
[TAP5-1691] - AssetPathConstructorImpl should URL-encode the application version
[TAP5-1704] - Localizing the "Today" and "None" labels in the core datefield component
[TAP5-1729] - Sometimes YUICompressor can fail with java.util.EmptyStackException
[TAP5-1734] - Race condition loading JavaScript libraries with ProgressiveDisplay
[TAP5-1735] - Most packages lack package-level javadocs
[TAP5-1742] - AfterRender() in Loop component should not short circuit
[TAP5-1762] - Some components do not have include a description of their parameters in their JavaDoc pages
[TAP5-1765] - PerThread scope is not honored when service is created using autobuild
[TAP5-1768] - @ActivationRequestParameter does not encode to be URL friendly
[TAP5-1770] - PageTester causes StringIndexOutOfBoundsException for any page request path with query parameter
[TAP5-1773] - FormFieldFocus mixin passes control name, not client id, to JavaScriptSupport.autofocus()
[TAP5-1779] - Tapestry allows directory listing of assets via client browser
[TAP5-1784] - Extra comma in tapestry-messages_de.js causes Internet Explorer to fail to work
[TAP5-1785] - Exceptions while compressing JavaScript are not fully reported
[TAP5-1787] - TextField should be usable with HTML5 type values (such as "number", "email", etc.)
[TAP5-1788] - Service id 'environment' has already been defined by org.apache.tapestry5.services.TapestryModule with Spring 3.1
[TAP5-1791] - On some JDKs, the complex regular expression used by ComponentEventLinkEncoderImpl will cause a stack overflow
[TAP5-1798] - Grid and BeanDisplay should ignore properties that are actually static fields
[TAP5-1822] - LinkSecurity should be public, not internal, as it is used with the public Link interface
[TAP5-1825] - Incorrect order of parameter in localization messages within ValidationMessages Italian lang
[TAP5-1831] - DelegatingInjectionResources does not pass generic type information to its first delegate
[TAP5-1836] - "LocalhostOnly" WhitelistAnalyzer check "0:0:0:0:0:0:0:1%0" ip address instead "0:0:0:0:0:0:0:1"
[TAP5-1844] - Datefield not fires onchange event on changes
[TAP5-1848] - tapestry-jpa ignores persistence provider from persistence.xml
[TAP5-1854] - AjaxComponentEventRequestHandler doesn't handle the case where a response has already be returned, and may append an empty JSON Object to the response
[TAP5-1860] - Access to protected component fields does not always reflect in subclasses
[TAP5-1868] - SRSCachingInterceptor returns compressed version of asset for all clients once it was compressed for some client
[TAP5-1870] - javascript added while in the render phase of a component from an ajax request is never executed
[TAP5-1873] - JavaScript execution exception is not logged
[TAP5-1880] - GZip compression should be disabled if the request is over http 1.0
[TAP5-1881] - TypeCoercion from Number to Boolean returns false for any number that is an even multiple of 256
[TAP5-1887] - Client-side JavaScript error if console.info/debug/... is available but not a function
[TAP5-1890] - PlaceholderBlock should implement RenderCommand
[TAP5-2034] - When the application operates with a context path, asset URLs are incorrectly formed
[TAP5-2037] - ValidationTracker/Flash persistence race condition with AJAX
[TAP5-2040] - TapestryAppInitializer should have access to system properties; this is a reversion in 5.4
[TAP5-2041] - Links within subheadings are invisible on Javadoc pages
[TAP5-2045] - Set default CSS class for Label component to be "control-label", but allow overrides
[TAP5-2047] - ElementWrapper#find jQuery implementation is broken if there is no match
[TAP5-2049] - Tapestry should provide locking semantics for attributes stored in the session, to prevent multiple simultaneous requests (due to Ajax) from conflicting
[TAP5-2052] - tapestry-ioc has a compile dependency on tapestry-test
[TAP5-2053] - Use of 'transient' property in alert options breaks JavaScript processors
[TAP5-2054] - `dom.js` implementations break when minified
[TAP5-1008] - The order in which zones are added to the MultiZoneUpdate should be honored on the client side, to allow nested zones to be updated all in a single request
[TAP5-1268] - Have Tapestry's core library contribute to the global message catalog; move all validation messages and component catalogs to the single file
[TAP5-1394] - Adding method to Cookies api that lets you set path, domain, and maxAge at once.
[TAP5-1405] - XHR requests should be easily callable from javascript and not rely on a zone
[TAP5-1560] - Remove internal dependancies on deprecated features
[TAP5-1570] - Zone elements in the client should trigger events so that the application can react to changes
[TAP5-1748] - Alerts component should render informal parameters
[TAP5-1756] - Let the asset path prefix be configurable
[TAP5-1775] - Improve javascript performance while creating zone events
[TAP5-1781] - Improve javascript load time of pages improving onDomLoadedCallback
[TAP5-1801] - Component fields should not need to be private, merely non-public
[TAP5-1805] - Selectable Tree component look and feel is lacking in user affordances
[TAP5-1808] - Change Form to (by default) immediately render markup when there are validation errors, to avoid creating the session
[TAP5-1816] - Add CSS rule for DIV.t-exception-container to default.css that sets a very high z-index
[TAP5-1824] - New translations for Norwegian Bokmål
[TAP5-1827] - KaptchaField should have a parameter to allow it to operate as a visible text field rather than a password field
[TAP5-1832] - Tapestry could do an even better job of filtering unnecessary stack frames from the exception report
We're getting very close to a beta release of Tapestry 5.4. I've spent the last couple of weeks converting an existing client's very large application (120+ pages, tons of components, lots of client-side JavaScript) from 5.3 to the current 5.4 alpha, and it's been a lot of work ... but also a lot of reward.
Before the switch, the application was already using jQuery, Bootstrap, and a limited amount of Backbone; under Tapestry 5.3, that means that Prototype and Scriptaculous were also in the mix ... and there were quite a few conflicts between Prototype and jQuery.
It's not just that Prototype and jQuery both want to use window.$ as their point of entry; the conflicts can be deeper and more subtle. For example, I had a nasty time with a Boostrap modal dialog that didn't dismiss correctly. After a lot of debugging, I found out that jQuery treats a method on an element as an event handler for the event with the matching name; Bootstrap was triggering a hide event, and jQuery was invoking the hide() Element method added by Prototype. That kind of thing has been my life under the mixed stack.
Tapestry 5.4 manages this better; and with the new abstraction layer it is possible to turn off Prototype completely and use just jQuery. That removes these conflicts ... and also speeds things up, but reducing the amount of JavaScript code downloaded to the client.
The transition from 5.3 to 5.4 was a chance to review and improve all that code. Here's a few observations.
The application has some very complex client-side forms; well beyond the abilities of Tapestry to manage using the FormInjector and FormFragment components. Instead, we use Backbone is a kind of hybrid mode, where the Backbone Model or Collection is persisted to a hidden field, so that the data collected or edited is transmitted to the server as part of an over-all form submission.
Of course, this means creating a lot of content, including form control elements, on the fly. One of the main problem was integrating Tapestry's client-side validation for the newly created fields.
Under 5.3, this required examining and often hacking (or monkey patching) the Tapestry client-side code, which create FormEventManager and FieldEventManager objects for each form or form control element ... and there was lots of hackery to tap into the form's submission cycle to perform validations.
Under 5.4 it is much easier, when creating the new fields, we can specify the desired validations via attributes:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
The data-validation attribute indicates the field participates in validation; it will have events triggered on it when the enclosing form submits. The t5/core/validation module supplies the code that handles fields with data-optionality="required" and ensures that a value is provided, displaying the data-required-message if the field is left blank.
That's the pattern throughout 5.4; data- attributes are used to identify where special behavior is desired, and a well documented system of events is used handle the processing of the behavior.
Very little on the server side changed with the upgrade to 5.4; but revisiting and rewriting all that JavaScript was more than enough work!
In addition, it was an opportunity to convert all the code to CoffeeScript. CoffeeScript is my preferred way of writing client-side code: it is more concise and readable than JavaScript, but the compiled output is still quite readable. It also has great features like the block strings (from the example code above) which largely eliminates the need for a separate template engine.
Working in CoffeeScript and Tapestry so much led to some quick evolution of the CoffeeScript support built into Tapestry (in the optional tapestry-wro4j module). Initially, any change to any CoffeeScript file forced all CoffeeScript files to be recompiled; when using the sluggish Rhino JavaScript engine to run the CoffeeScript compiler, we could see 10 - 20 seconds per source file!
I made some improvements, adding a special development-mode in-memory cache that would only recompile the CoffeeScript if the underlying source file content had changed. Since application restarts are rare in Tapestry, this was sufficient. I eventually added a file-system cache so that compilation could be avoided, even over restarts of the application.
I also started using Less instead of CSS. Less is a meta-language that compiles down to CSS, much like the relationship between CoffeeScript and JavaScript ... but I think even more dramatically. Compilation is pretty fast, based on the Less4J library.
I found that the out-of-the-box support for Less4J supplied by WRO4J was insufficient: it didn't do a great job with @import; a change to an imported source file didn't force a recompilation. I've addressed that with a custom (for Tapestry) wrapper that properly tracks dependencies.
So where does that leave us? There's still a huge amount of work to do before Tapestry 5.4 is ready for release, but it's mostly fixing bugs and other rough edges. As too often happens, the reality of earning a living have made me postpone some of my ideas for a later release.
I think that Tapestry is aging, if not gracefully, then at least comfortably, into a growing age where rich, single-page applications built with Backbone, AngularJS, or something else are the norm, and not the exception. I'm the first to admit that Tapestry was designed for a simpler time when Ajax was seasoning, not the meat-and-potatoes of the application. There's a lot of baggage in there, particularly related to forms and form controls. Yet, even so, there's some amazingly useful parts to Tapestry that apply equally well to modern applications:
Asset Pipeline
Tapestry's asset pipeline does much of the work normally associated with a command-line build step, but does it at runtime, with the benefit of being live for development. Tapestry can not only compile files from one type to another (e.g., CoffeeScript to JavaScript), but can also aggregate many small files into a single combined file and pass it through a minimizer. Further, Tapestry builds URLs that incorporate a content checksum. This checksum means that we can have the browser aggressively cache the file (using both E-Tags and a far-future expires header) as any change to the file will change the checksum as well, resulting in what looks to the client like an entirely new resource. Further, Tapestry caches both the straight-up file, and the GZip compressed version of the file. All of this means that Tapestry provides not just great throughput by processing requests quickly, but also great performance by reducing the number of requests from clients as well.
Asynchronous Module Definition (AMD) Support
Tapestry 5.4 introduces direct support for AMD via RequireJS. At the minimum, this means that JavaScript can be loaded in parallel ... but the real benefit is how AMD encourages you to organize your code in a manageable way. In addition, Tapestry ensures that your modules can be JavaScript or CoffeeScript, and provides a simple way on the server to override any built-in modules provided by Tapestry, should the need arise. Better yet, Tapestry has a server-side API for invoking client-side functions exported by AMD modules; these is a very clean and very useful way to knit together server-side and client-side logic.
Superior Feedback and Exception Reporting
When working on other stacks, the thing I miss the most now is Tapestry's exception reporting. This is cleanly integrated in Tapestry, where a server side failure will result in a detailed HTML report on the client. For Ajax requests, this report is presented in a floating <iframe>. The wealth of information available from the server side and reported directly in the client makes developing complex applications with involved server-side interaction much easier.
As Tapestry 5.4 transitions to beta, and to an eventual final release, I'm still targeted on the future ... and I'm focused on making Tapestry be a great choice for single-page apps as well as traditional applications. My head is bubbling with ideas to make this happen.
You've probably heard about "Not Invented Here" syndrome: the drive among developers to create something of their own, rather than just use an off-the-shelf library or component. It's almost universally painted as a bad thing, a sign of immaturity, or even arrogance.
But there's a flip side to this: every bit of code ever written contains within it tradeoffs: speed versus maintainability is a common tradeoff that everyone has seen. Perhaps the code is insufficiently flexible in the face of real-world requirements, but is really well tested for what it does cover. These choices reflect the developer's principles applied to the code. In fact, it is rare for it to be an easy give-and-take between two simple goals; more likely, there's lots of conflicting goals in the code, in the requirements, and in the developer's head. "Not Invented Here" can also mean "Not Reflecting My Principles".
Tapestry has it own set of guiding principals: Simplicity, Consistency, Efficiency, and Feedback ... and as a reusable framework, Feedback is very important. Feedback may be the most important principle when things go wrong. A framework that obscures problems, through bad feedback, is a framework that shouldn't be used.
Which brings us back to "Not Invented Here". Only in an impossibly perfect world would there be some ideal blob of code out there, ready to be reused, with zero impedance mismatch issues. In fact, when you bring in other people's code, you are forced to mesh your goals and principals with theirs. In my case, I'm adding support to Tapestry for converting Less files to CSS, using WRO4J (Web Resource Optimizer for Java). They've been working on WRO4J for several years, it makes sense to reuse their code, and it would be arrogant to think I could whip something better together under any kind of time constraints.
In fact, it's actually been pretty smooth sailing ... until we tripped across a violation of Tapestry's Feedback principle. As soon as I tested an error case, where the Less source file was not valid I hit bad feedback. Can you spot what's wrong with this exception report?
Oops! Looks like someone didn't get the memo about the importance of toString(). See, that Feedback principal is important to me, but for the majority of developers, useful feedback is too often an afterthought. I don't want to single out WRO4J here ... I'm pretty disdainful about feedback in nearly all software: open source or proprietary.
So what are our options here?
Write our own wrappers around the Less Processor, and throw out WRO4J
Beg the WRO4J guys to implement a real toString(), and wait for the next release
Fork WRO4J in the short term, and hope they'll take a patch in the long term
Patch around this reporting problem
Obviously, we should find a way to patch the reporting problem; we don't want to throw out the baby with the bath water. Fortunately, Tapestry provides the necessary hooks to override how it presents objects inside the exception report; it's all about providing a mapping from a Java type to a matching implementation of ObjectRenderer. Because of Tapestry's IoC container, this is actually quite straight forward:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
And with those changes, the exception is presented quite differently:
Well, those are actually the raw ANTLR parser errors, but at least that's enough to help you find location of the problem ... whereas, with the bad feedback, you would only know that there was an issue somewhere in your Less source file.
The other day, I was working on a little bit of code in Clojure, just touching up some exception reporting, when I was
suddenly struck by one of the fundamental reasons that Clojure is so enjoyable to code in. Clojure
is craftable: that is, in Clojure you have the option to craft at your code to make it more concise, easier
to read, and easier to maintain. That is not the case for all, or perhaps even most, programming languages.
In my case, I was constructing an error message where I needed to convert the keys of two maps into a comma-seperated
string (I don't like to say "you guessed wrong" without saying "here's what you could have said").
What I want my code to do is easily expressed as an informal recipe:
Extract all the keys from both maps
Remove any duplicates
Convert the keys to strings
Sort the strings into ascending order
Build and return one big string, by concatinating all the key strings, using ", " as a seperator
Return "<none>" if both maps are empty
If I was writing
this in Java, it would look something like this:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
There's enough looping and conditionals in this code (along with tip-toeing around Java Generics) that its easier to
look at its test specifiction (written in Spock) to see what it
is supposed to do:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
The first pass at a Clojure version is already simpler than the Java version ...
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
I couldn't resist using
the clojure.string/join function,
rather than building the string directly (which would be slightly tedious in Clojure). In many ways, this is a lot
like the Java version; we're using let to create local symbols for each step in the process in just the
same way that the Java version defines local variables for each step.
However, there's room for improvement here. Let's start to craft.
For example, let's assume that both maps being empty is rare, or at least, that the cost of sorting an empty list is
low (it is!). Our code becomes much more readable if we merge it into one big let:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Now we're getting somewhere. I think this version makes it much more clear what is going on that the prior Clojure
version, or the Java version.
However, if you've written enough code, you know one of the basic rules of all programming: names are
hard. Anything that frees you from having to come up with names is generally a Good Thing. In Java, we have
endless names: not just for methods and variables, but for classes and interfaces ... even packages. Long years of
coding Java has made me dread naming things, because names never quite encompass what a thing does, and often become
outdated as code evolves.
So, what names can we get rid of, and how? Well, if we look at the structure of our code, we can see that each step
creates a value that is passed to the next expression as the final parameter. So all-keys is passed as
the last parameter of the (map) expression, resulting in key-names, and
then key-names is passed as the last parameter of the (sort) expression. In fact, ignoring
the empty check for a moment, the sorted-names value is passed to the (s/join) expression as
the last parameter as well.
This is a very important concept in Clojure; you may have heard people trying to express that you code in Clojure in
terms of a "flow" of data through a series of expressions. We'll, you've just seen a very small example of this.
In fact, it is no simple coincidence that the last parameter is so important; this represents a careful and reasoned
alignment of the parameters of many different functions in clojure.core and elsewhere, to ensure that flow can
be passed as that final parameter, because it becomes central to the ability to combine functions and expressions
together with minimal fuss.
We can use the ->> macro
(pronounced "thread last") to
rebuild our flow without having to come up with names for each step:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
The ->> macro juggles our expressions into an appropriate order; without it we'd have to deeply
nest our expressions in an unreadable way: (sort (map str (set (concat (keys map1) (keys map2))))). Even
with a short flow of expressions, that's hard to parse and interpret, so ->> is an invaluable and
frequently used tool in the Clojure toolbox.
We can continue to craft; the first expression (that builds the set from the keys), can itself be broken apart into a
few smaller steps. This is really to get us ready to do something a bit more dramatic:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This is getting ever closer to our original recipe; you can more clearly see the extraction of keys from the maps
before building the set (which is only used to ensure key uniqueness), before continuing on to convert keys from objects
to strings, sort them, and combine the final result.
In fact, we're going to go beyond our original brief, and support any number of input maps, not just two:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
The mapcat function is like map,
but expects that each invocation will create a collection; mapcat concatinates all those collections together ... just
what we want to assemble a collection of all the keys of all the input maps.
At this point, we don't have much more to go ... but can we get rid of the sorted-names symbol? In fact,
we can: what if part of our flow replaced the empty list with a list containing just the string "<none>"? It
would look like this:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
... and that's about as far as I care to take it; a clean flow starting with the maps, and going through a series of
expressions to transform those input maps into a final result. But what's really important here is just how fast and
easy it is to start with an idea in Clojure and refine it from something clumsy (such as the initial too-much-like-Java
version) into something elegant and surgically precise, such as the final version.
That's simply not something you can do in less expressive languages such as Java. For example, Tapestry certainly
does quite a number of wonderful things, and supports some very concise and elegant code (especially
in green code) ...
but that is the result of organizing large amounts of code in service of specific goals. We're talking tons of
interfaces, a complete Inversion-Of-Control container, and runtime bytecode manipulation to support that level of
conciseness. That's the hallmark of a quite consequential framework.
That isn't crafting code; that's a big engineering effort. It isn't local and invisible, it tends to be
global and intrusive.
In Java, your only approach to simplifying code in one place is build up a lot of complexity somewhere else.
That is simply not the case in Clojure; by adopting, leveraging, and extending the wonderful patterns already present
in the language and its carefully designed standard library, you can reach a high level of readability. You are no
longer coding to make the compiler happy, you are in control, because the Clojure languge gives you the tools you need
to be in control. And that can be intoxicating.
The source code for this blog post is available on GitHub.
I had a very odd interchange with my friend Merlyn over lunch; he started talking about red code vs. green code (in the context of supporting both callbacks and promises inside NodeJS). At first I thought he was referring to code coverage of those lines of code ... the implication being that supporting multiple paths of execution may lead to laziness in testing every possible path (with some code going "red" in the code coverage report).
But that wasn't it at all: "red" code referred to framework code, "green" code referred to end-user code, leveraging the framework.
Odder still, Merlyn insisted that he first heard this term from ... me, a few years ago. That's what being in the baby-raising camp can do to you ... I know longer have any idea of what I've said or thought in the past. Actually, this isn't new for me ... I've always had a very vague memory for anything not code.
In any case, this red vs. green terminology is a useful concept ... certainly I move the earth in Tapestry's red code to make end-user's green code as simple as possible.
So, I want to do three things:
It's definitely worth reflecting on the fact that all code is not created equal, and that long-lived, reusable code ("red") will inevitably grow in complexity to a level that would not be acceptable in client ("green") code.
Let's promote this term, because it is so handy!
If I didn't create the term myself, let's track down the originator and thank them!
I've spent the last several months significantly reworking Tapestry 5's client-side JavaScript support, in an effort to move away form the tight binding to Prototype. After all of that refactoring, recoding, repositioning, and just-plain-hacking, I was able to do most of the job of introducing jQuery support in just a few hours, yesterday and today.
I'll be producing another preview release pretty soon, or you can get the lastest from Tapestry's master branch.
Currently, the code to switch over from using Prototype to jQuery looks like this (it will get simpler soon):
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
The first part overrides the provider to be "jquery" (the default provider is "prototype"). In Tapestry terms, the "infastructure framework" provides the APIs for DOM queries, DOM manipulation, and Ajax request handling. If you don't like jQuery, you can easily create your own provider for your favorite framework.
Part of 5.4 is trying to manage all these different compatibility issues at a slightly higher level than configuration symbols; that's the Compatibility service with its Traits. Those two traits disable Scriptaculous support (which isn't needed by jQuery, and won't work without Prototype), and disables support for Tapestry 5.3-style initializers.
Underneath the covers, the way the switch between Prototype and jQuery works is quite simple:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
The ModuleManager service is the code that handles requests for modules from the client. It has a configuration that is used to handle edge cases, such as treating traditional JavaScript libraries as if they were AMD modules.
The code above sets up a server-side override for the module t5/core/dom. All the Tapestry client-side modules use dom; none of them uses Prototype or jQuery directly (except for a couple that access Twitter Bootstrap functionality).
These contributions ensure that when the client-side requests the t5/core/dom module, what will be sent back will be either the Prototype-specific implementation, or the jQuery-specific implementation. Without this contribution, we'd see a 404 error when the dom module was requested. Instead, the client-side doesn't have any idea that dom is special.
The primary job of the dom module is to wrap DOM elements inside a new object, thereby providing a new API that allows various kinds of manipulation, as well as listening to events, or triggering them. The API is a bit of a mashup between Prototype and jQuery, but leans pretty heavily towards jQuery-style operations and naming. dom's secondary job is to support Ajax requests, plus adding event handlers to the document object.
In practice, this can be very concise and readable (partially, thanks to CoffeeScript):
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
In this listing (which supports some of the behavior of the Tapestry Zone component), dom holds the export from module t5/core/dom; it waits for the events.zone.update event to be triggered; the callback is invoked with this set to the wrapper around the element where the event was triggered.
The event here is also a wrapper; its a minimal mix of Prototype and jQuery: I like the memo property from Prototype, so that's present. The event handler triggers a pair of before and after events, and updates the content of the zone. Why the before and after events? By default they do nothing, but it would be simple to add handlers to perform some kind of animation when content is added.
In any case, because all Tapestry client-side modules code against this API, they don't know or care whether the page has loaded Prototype, jQuery, or something else. If you are writing Tapestry components for reuse, coding against the dom API will help ensure that your component will work correctly in all kinds of Tapestry applications. However, when coding an application,. you reserve the right to choose what the infrastructure framework should be: you should feel free to use the infrastructure framework directly ... unless you find the dom API to be easier.