File I/O in Ceylon

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 not Nil.
      • File: Represents a file in a hierarchical file system.
      • Directory: Represents a directory in a hierarchical file system.
      • Link: Represents a symbolic link.

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 a try block have the destroy() method invoked at the conclusion of the try block. An instance can only be used once. E.g. File.Reader.
  • Obtainable: instances are initialized before a try block. When in the resources list, the obtain() method is invoked, and at the end of the block, the release() method is invoked. An Obtainable may be used by multiple try 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.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s