Update 2017-03-04: Gradle no longer has these problems.
One of the most important features of the Maven family of build tools, of which
Gradle is a member, is the ability to automatically download dependencies and
configure the Java classpath. The latter often is the more important part of
the feature — unlike virtually every other module-based environment, the JVM is
bafflingly incapable of locating dependencies itself (eg, from a standardised
location as in C).
Gradle’s implementation, however, is rather problematic. Virtually every task
undertaken by Gradle — including simply invoking the javac
compiler —
involves spawning a child Gradle process, whose classpath includes that of the
user code in question. This is perfectly fine as long as the user code’s
dependencies are disjoint from Gradle’s; had gradle been written without any
non-JRE dependencies, it wouldn’t be an issue at all.
But Gradle isn’t at all light-weight — its memory footprint dwarfs that of
Windows XP — and it has almost as many dependencies as one might suspect.
The private
access modifier is arguably the most important in the context of
software engineering in Java, and more so in Groovy where everything is
public
(sort of) by default. By declaring
something private
, the programmer can be certain that no code outside the
current lexical scope can see the member; thus, there will never be any
external dependencies on its presence or behaviour, allowing it to be easily
changed or removed without widespread consequences. Of course, it could be
expected of Groovy to merely parse and discard the private
keyword,
substituting it with the “implicit-public” access modifier instead.
Fortunately, it doesn’t do that.
It does something much worse.
Groovy has an entire three different types of type-casts that can occur. The
first two are inherited from Java, albeit with substantially more permissive
semantics.
One of the most obvious ways with which Groovy tries to “reduce” boiler-plate
is to make all members (both variables and methods) of any class public by
default, as opposed to Java in which members are package-private by
default. While this is already a questionable design decision at best, an
interesting facet of the implementation is that implicitly public member
variables are private, though another Groovy feature masks this detail when
operating within Groovy.
We’ll start with a Groovy class that just has some members of varying types and
access modifiers. You can compile these files yourself by dropping them into
the base project.
In the past two
weeks, we’ve looked at how horribly
Groovy handles something as simple as function call arguments. There is yet one
more horror Groovy brings to the table: its conception of named arguments.
Named arguments (also known as “keyword arguments”, or just “kwargs”, in some
contexts) allow the programmer to specify arguments to a function by name,
rather than by position, enhancing readability and in many cases writability,
as the meaning of each argument is clarified at use site. It also has the
advantage of allowing some arguments to be specified without specifying the
preceding ones, in the case of arguments which also have defaults. Examples of
languages which support this concept are Python and OCaml.
As mentioned last week, one of the main reasons Java has method overloading was
to emulate optional arguments, albeit verbosely. In the below example, our
multiple definitions of doSomething
allow it to be called with any prefix of
its argument list, the other parameters “defaulting” to something that the
implementor considered reasonable.
Java permits the programmer to define more than one function or method of the
same name, which can be differentiated based on argument count and types; this
is called “overloading”. While most modern languages consider it a fairly bad
thing, it allows Java to partially overcome two serious deficiencies:
Java does not have default arguments. With overloading, you can define
versions of a function with fewer arguments, which simply call the real
version with the missing arguments filled in.
Java gives no way to generically perform identical operations across
disparate types, especially when it comes to arrays. Overloading allows the
same function to be copy-pasted with different type declarations, or to do
forwarding with conversions, in order to achieve such operations.
Java 1.5 introduced into the language a concept called generics. In
environments that do generics correctly, a generic type parameter specifies the
type of elements contained within an abstract data type. C++’s templates have
this effect — it is fundamentally impossible for a list<string>
to ever
contain anything other than string
s.
For the sake of backwards compatibility, Java instead handles generics with
type erasure and implicit casting. Type erasure means that the Java compiler
determines the least general type that the ADT can possibly have (eg, Object
for List
, Comparable
for TreeMap
, etc) according to its generic
declaration, and then internally uses that type, and provides extra information
in the generated .class
files so that the compiler knows what generics were
originally there. Whenever a value with a more specific generic type is used,
the compiler implicitly casts it back down to the type that “should” be there.
In Java, a default constructor is simply one without arguments. If you make a
non-abstract class with no explicit constructor, the compiler gives your class
a do-nothing default default constructor. This happens in Groovy as
well. This is not the subject of this post. Also consider that the title of the
post did not say “every Groovy class” — that would be a far less disturbing
topic.
At first glance, Groovy’s scope rules are much like Java’s. A variable name
will refer to a local variable, a variable from a containing scope, or the
superclass or -interface of a class scope, or possibly from static
imports. Except in contrived cases, the system is fairly easy to reason about,
and in any case the compiler will catch you if you do something wrong.
Groovy, of course, had to take a decent system and try to make it more
“dynamic”. In some ways, the scoping system is more like Python, especially in
that variable existence is determined partially at run-time. Python’s rules,
however, are fairly simple, and make that system basically work.