The Ceylon language takes a different approach to reading and writing files than Java. Consider the following methods in Java:
File::exists
: Tests whether the file or directory denoted by this abstract pathname exists.File::isFile
: Tests whether the file denoted by this abstract pathname is a normal file.File::isDirectory
: Tests whether the file denoted by this abstract pathname is a directory.Files::isSymbolicLink
: Tests whether a file is a symbolic link.
What makes this wonky is that you can have an instance of File
, but can still attempt to write to it without testing whether it’s actually a file, directory, or even whether it exists in the first place. This can lead to various incarnations of IOException
being thrown.
Rather than handle these abstractions with methods, Ceylon implements them as types in the ceylon.file
module. The hierarchy is (note the highlighted LeafNodes
):
Resource
: Represents a file, link, or directory located at a certain path, or the absence of a file or directory at that path.Nil
: Represents the absence of any existing file or directory at a certain path in a hierarchical file system.ExistingResource
: A resource that actually exists—that is, one that is notNil
.
I find that this produces more intuitive code than Java. Let’s see these types in action.
How to Open and Read a File in Ceylon
Reading a file is quite straight forward. There are two ways to do it: the first applies a callback function to each line of the file, while the second returns the lines of the file as an array. The third way listed below is really just the internal implementation of the first way.
import ceylon.file {
parsePath,
File,
Resource,
forEachLine,
lines
}
void readFile(String filePath) {
Resource resource = parsePath(filePath).resource;
// Resource has 4 subtypes: File | Directory | Link | Nil
// We have to resolve the type.
if (is File resource) {
// STRATEGY 1
forEachLine(resource, (String line) {
// do something with each line of the file.
});
// STRATEGY 2
String[] allLines = lines(resource);
// do something with each line of the file.
// STRATEGY 3 (this is just what STRATEGY 1 is doing internally)
try (reader = resource.Reader()) {
while (exists line = reader.readLine()) {
// do something with each line of the file.
}
}
}
else {
// resource has type Directory | Link | Nil
// This use case may need to handled in some way.
}
}
How to Write to a File in Ceylon
Similarly, writing to a file is straightforward. A file can either be overwritten or appended to, as shown below.
import ceylon.file {
createFileIfNil,
File,
Nil,
parsePath,
Resource
}
void writeFile(String filePath) {
Resource resource = parsePath(filePath).resource;
// Resource has 4 subtypes: File | Directory | Link | Nil
// We have to resolve the type.
if (is File|Nil resource) {
// Create the file if it doesn't exist,
// otherwise return the resource.
File file = createFileIfNil(resource);
// STRATEGY 1 (overwrite)
try (overwriter = file.Overwriter()) {
overwriter.writeLine("something");
}
// STRATEGY 2 (append)
try (appender = file.Appender()) {
appender.writeLine("something");
}
}
}
The “try-with-resources” and Exception handling
Ceylon has a similar concept to Java’s “try-with-resources” statement. In Java, java.lang.AutoCloseable
instances can be declared in a try
statement. At the end of the try
block, the close()
method is automatically called on those instances.
Ceylon has has the taken the same concept, but made it more general: Ceylon defines two types that can be declared in a try
header:
Destroyable
): instances are declared at the start of atry
block have thedestroy()
method invoked at the conclusion of thetry
block. An instance can only be used once. E.g.File.Reader
.Obtainable
: instances are initialized before atry
block. When in the resources list, theobtain()
method is invoked, and at the end of the block, therelease()
method is invoked. AnObtainable
may be used by multipletry
blocks. E.g. synchronization locks.
You’ll notice that there are no catch
blocks after the try
blocks. There’s almost almost no need form them because Ceylon’s Resource
subtypes eliminate the most common pitfalls that lead to thrown IOException
, FileNotFoundException
, etc. in Java when performing basic file operations. However, these exceptions can still be thrown in Ceylon if, for example, a file is deleted after Ceylon has created the Resource
instance but before the lines of the file are read. It’s also worth pointing out that Ceylon has done away with checked exceptions, so the compiler does not require catch
blocks.