Some more 'use cases' for the closures proposal (see previous entries). One poster child for closures is resource management, so lets see how that works out.
withLock
Lock lock = ...; withLock (lock) { // do something which required locking }
This operation locks the lock, calls the closure, then unlocks the lock. This equates to the try..finally block:
Lock lk = ...; lk.lock(); try { // do something which required locking } finally { lk.unlock(); }
withFileReader
File file = ...; withFileReader (FileReader in : file) { // read data from the file }
This operation opens the file, creates the reader, calls the closure, then closes the file. Again this equates to a try..finally block and eliminates problems by forgetting to close the resource.
Similar methods would exist for Writer, and streams.
eachLine
File file = ...; eachLine (String line : file) { // called once for each line in the file }
This operation opens the file, creates the reader, reads the file line by line calling the closure once per line, then closes the file. This equates to quite a significant body of code that is essentially cut and pasted in many applications. And again it eliminates problems by forgetting to close the resource.
Summary
Although I haven't shown it, additional examples could be written for databases, or any other connection where the resource needs explicit closure after the task is complete. These tasks all seem a lot cleaner using the proposed closure syntax than at present.
eachLine needs a way of specifying the encoding - one of the biggest mistakes in the API currently is that FileReader and FileWriter provide no way of specifying character encoding; instead using the platform default.
ReplyDeleteI still think an expression-form of this would be good, e.g., a simple grep-like API:
List[String] allLinesContainingFish=eachLine(String line: "ASCII",file)
{
line.indexOf("fish")!=-1
}
Of course this can be implemented as a statement form, but that's boilerplate - creating the list yourself, the if statement, adding to it:
List[String] lines=...;
eachLine(String line: "ASCII",file)
{
if (line.indexOf("fish")!=-1)
lines.add(line);
}
Not a huge amount bigger, but less expressive.
@Ricky
ReplyDeleteStill closure aren't the answer to everything. Using "eachLine" for what you're proposing isn't such a good idea. More generally you could do it like this:
List<String> allLinesContainingFish = removeIfNot(new LineReader(file, "ASCII"), {String s=>s.contains("fish")});
Where LineReader is an Iterator/Iterable that reads a file line by line and this kind of removeIfNot will take an iterator/iterable and build a list from it.
Starting from there you may provide something like this:
List<String> allLinesContainingFish = grep(file, "ASCII", {String s=>s.contains("fish")});
Which is implemented like so:
static String grep(File f, String encoding, {String=>boolean} valid) {
return removeIfNot(new LineReader(f, encoding), valid);
}
In general I don't think "eachLine" is such a great addition:
for(String line: new LineReader(file)) { ... }
does the job well enough.
It makes sense only if it was defined for the reader itself, for example:
withReader(Reader r: file) {
r.eachLine { String s =>
// ...
}
}
As this really abstracts things away. Then, of course, this should also be a method on file:
file.eachLine { String s => ... }
Using Rickys example:
List<String> linesWithFish = file.grep({String s=>s.contains("fish")});
Being able to use eachLine inline would help in composing statements, e.g.:
ReplyDeleteSystem.out.println(file.grep("fish").uniq().join(","));
However, the above is impractical in Java, because adding methods to objects is non-trivial.
Scala supports a mixin feature, whereby an object can be kept as small as possible, and other methods can be added to it, at compile time.
E.g.,:
public static boolean testForFishiness(Object object)
{
return object.toString().indexOf("fish")!=-1;
}
FishTester.testForFishiness(object); //Java
object.testForFishiness(); //Scala
You have to set up a 'view' of an object; the object itself doesn't gain a method, it just appears to, and only for the code that subscribes to that view. I think that's a better way than what I've heard of for Ruby et al, in terms of mixins.
I've been keeping my objects as small as possible; ideally a single method on each (hence they end up being analogous to functions), with static helper classes. Static imports help with this, but you still need to know where to get the helper methods from. I use a convention, Thing has ThingUtility in the same package, but it'd be better if I could make an alias, or a view, so that Thing is viewed as Thing plus all the methods of ThingUtility.
Where am I going with this?
Patrick said: "Then, of course, this should also be a method on file:
file.eachLine"
I disagree; it should be in a utility class, as eachLine is not an inherent ability of a file - its implementation won't really vary between different implementations or instances of File.
It's an operation *on* the file, but it's convenient to make it appear as an operation by the file.
Don't think that there is much of a chance that we'll get mixins in Java in the nearer future. ;)
ReplyDeleteAnyway, you're right, eachLine doesn't really belong to the File-Object, but its the only case in which eachLine would clearify the intention of the code cleaner than a simple enhanced-for-loop.
> Being able to use eachLine inline would help in composing statements, e.g.:
What do you mean? That statement has nothing to do with an inlined eachLine or something like that.
Anyway, the biggest question is: What is eachLine supposed to return? It's an iteration over each line of a file. What you want (in general) is a removeIfNot that knows how to work with files:
List[String] filteredLines = removeIfNot(file, {String line=>line.contains("fish")});
Or maybe you'd like to read each line of the file and store the number of characters within it:
List[Integer] lineLengths = map(file, {String line=>line.length()});
Therefore you'd need a map that knows how to iterate a file-object (or wrap the File into an Iterable/Iterator).
There is no need for eachLine with a return value, and limited use as a control structure, as it wouldn't be much more than we could have currently, though the implementation might be simpler.
eachLine is supposed to return some form of sequence of Strings. This could be an Iterable, for example.
ReplyDeleteLet's rename removeIfNot to grep (and invert its logic).
String nonBlankLines=join(",",grep(file,"ASCII":String line) line.trim().length()!=0);
I swapped the order of what goes before and after the colon, and think that this syntax is attractive. I also think it's non-ambiguous, but I'm not certain.
Wrapping line.trim().length()!=0 in {} might help with parsing.
It might be better not to make grep know about files, just about Iterable[String]s.
Iterable[String] nonBlank=grep(eachLine(file,"ASCII"):String line) line.trim().length()!=0);
Of course, regexes might be better for this case anyway.
The statement-form eachLine given as a use case by Stephen is merely a specialisation of the eachLine that I gave, that forces the handling of the lines to be done imperatively; it doesn't facilitate composition; or at least, not as well as the expression form.