Blog of Gavin King

Ceylon 1.3.2 is now available

Ceylon 1.3.2 is a significant minor release of the Ceylon language, with over 100 issues closed. This release introduces Ceylon assemblies, allows you to export Ceylon modules as Maven repositories, makes it even easier than before to have a mixed Java and Ceylon project in Maven, allows string interpolation in assertions, and introduces lazy initialization for attributes.

This release of Ceylon has been tested with a wide variety of Java libraries and frameworks, including:

Example code demonstrating the use of these frameworks is available.

Changes

Enhancements to the language and command-line distribution include:

  • Assemblies (.cas archives)
  • Generate Maven repository
  • Support for mixed Java and Ceylon module projects in Maven
  • String interpolation in assert messages
  • Lazy initialization for attributes marked late

The most notable issues representing those changes are:

  • #6712, #6927, #6929 assembly support — ceylon assemble and .cas archives
  • #6856, #6847 ceylon maven-export to assemble a Maven repo from list of Ceylon modules
  • #6872, #6853, #6875 syntax for specifying maven group/artifact ids and npm module names
  • #3692 string interpolation in assertion failure messages
  • #3544 attribute lazy initialization
  • #6721 allow late attributes in declaration section
  • #6804 much better return type for Iterable.sequence()
  • #6797 named constructors to create Java arrays with streams of elements
  • #6784, #6778 static methods for Integer, Float, and String
  • #2324 add annotations to java.lang for Java modifiers
  • #6735 add --exclude-module option to ceylon copy

Naturally, the release incorporates many more bugfixes and minor enhancements.

IDE Changes

Ceylon IDE 1.3.2 for IntelliJ and Eclipse resolves more than 60 issues, and adds several new features, including support for running as a fat-jar and an improved formatter in IntelliJ.

SDK Changes

Exactly 6 issues affecting the Ceylon SDK have been fixed. New 1.3.2 releases of the platform modules are available in Herd.

Migration

  • For the JVM, this release is backwards-compatible with all previous releases of Ceylon since 1.2.0.

  • For JavaScript, this release is backwards-compatible only with the previous two releases (1.2.2 and 1.3.0).

Ceylon 1.3.2 is backward-compatible with Ceylon 1.3.0, and so it's not necessary to recompile or change dependencies. However, upgrading to version 1.3.2 of any Ceylon platform module is recommended.

About Ceylon

Ceylon is a modern, modular, statically typed programming language for the Java and JavaScript virtual machines. The language features a flexible and very readable syntax, a unique and uncommonly elegant static type system, a powerful module architecture, and excellent tooling, including an awesome IDE supporting both IntelliJ IDEA and the Eclipse platform.

Ceylon enables the development of cross-platform modules that execute portably in both virtual machine environments. Alternatively, a Ceylon module may target one or the other platform, in which case it may interoperate with native code written for that platform.

In the box

This release includes:

  • a complete language specification that defines the syntax and semantics of Ceylon in language accessible to the professional developer,
  • a command line toolset including compilers for Java and JavaScript, a documentation compiler, a test runner, a WAR archive packager, a "fat" JAR packager, and support for executing modular programs on the JVM and Node.js,
  • a powerful module architecture for code organization, dependency management, and module isolation at runtime, which also supports interoperation with OSGi, Jigsaw, Maven, and npm, and
  • the language module, our minimal, cross-platform, foundation-level API.

Available separately:

Language

Ceylon is a highly understandable object-oriented language with static typing. The language features:

  • an emphasis upon readability and a strong bias toward omission or elimination of potentially-harmful or potentially-ambiguous constructs and toward highly disciplined use of static types,
  • an extremely powerful and uncommonly elegant type system combining subtype and parametric polymorphism with:
    • first-class union and intersection types,
    • both declaration-site and use-site variance, and
    • the use of principal types for local type inference and flow-sensitive typing,
  • a unique treatment of function and tuple types, enabling powerful abstractions, along with the most elegant approach to null of any modern language,
  • first-class constructs for defining modules and dependencies between modules,
  • a very flexible syntax including comprehensions and support for expressing tree-like structures,
  • fully-reified generic types, on both the JVM and JavaScript virtual machines, and a unique typesafe metamodel.

More information about these language features may be found in the feature list and quick introduction.

Community

The Ceylon community site, https://ceylon-lang.org, includes documentation, and information about getting involved.

You can follow @ceylonlang on Twitter.

Source code

The source code for Ceylon, its specification, and its website, is freely available from GitHub.

Information about Ceylon's open source licenses is available here.

Issues

Bugs and suggestions may be reported in GitHub's issue tracker.

Acknowledgement

As always, we're deeply grateful to the community volunteers who contributed a substantial part of the current Ceylon codebase, working in their own spare time. The following people have contributed to Ceylon:

Gavin King, Stéphane Épardaud, Tako Schotanus, Tom Bentley, David Festal, Enrique Zamudio, Bastien Jansen, Emmanuel Bernard, Aleš Justin, Tomáš Hradec, James Cobb, Ross Tate, Max Rydahl Andersen, Mladen Turk, Lucas Werkmeister, Roland Tepp, Diego Coronel, Matej Lazar, John Vasileff, Toby Crawley, Julien Viet, Loic Rouchon, Stephane Gallès, Ivo Kasiuk, Corbin Uselton, Paco Soberón, Michael Musgrove, Daniel Rochetti, Henning Burdack, Luke deGruchy, Rohit Mohan, Griffin DeJohn, Casey Dahlin, Gilles Duboscq, Tomasz Krakowiak, Alexander Altman, Alexander Zolotko, Alex Szczuczko, Andrés G. Aragoneses, Anh Nhan Nguyen, Brice Dutheil, Carlos Augusto Mar, Charles Gould, Chris Gregory, klinger, Martin Voelkle, Mr. Arkansas, Paŭlo Ebermann, Vorlent, Akber Choudhry, Renato Athaydes, Flavio Oliveri, Michael Brackx, Brent Douglas, Lukas Eder, Markus Rydh, Julien Ponge, Pete Muir, Nicolas Leroux, Brett Cannon, Geoffrey De Smet, Guillaume Lours, Gunnar Morling, Jeff Parsons, Jesse Sightler, Oleg Kulikov, Raimund Klein, Sergej Koščejev, Chris Marshall, Simon Thum, Maia Kozheva, Shelby, Aslak Knutsen, Fabien Meurisse, Sjur Bakka, Xavier Coulon, Ari Kast, Dan Allen, Deniz Türkoglu, F. Meurisse, Jean-Charles Roger, Johannes Lehmann, allentc, Nikolay Tsankov, Chris Horne, Gabriel Mirea, Georg Ragaller, Harald Wellmann, Oliver Gondža, Stephen Crawley, Byron Clark, Francisco Reverbel, Jonas Berlin, Luke Hutchison, Nikita Ostroumov, Santiago Rodriguez, Sean Flanigan, Schalk W. Cronjé.

Ceylon 1.3.1 is now available

Ceylon 1.3.1 is a significant minor release of the Ceylon language, with over 140 issues closed. This is the first release of Ceylon which supports interoperation with Java 8 lambdas and streams, with RxJava, and with Spring Boot. This release also introduces support for static members.

This release of Ceylon has been tested with a wide variety of Java libraries and frameworks, including:

Example code demonstrating the use of these frameworks is available.

Compared to previous releases of Ceylon, the use of Java frameworks based on reflection is now much more transparent, and integration with multi-module Maven-based platforms and frameworks is now much easier to configure.

The Ceylon tour has been extensively updated, especially the sections addressing interoperation with native Java and JavaScript, and with the module system.

Changes

Enhancements to the language and command-line distribution include:

  • static members in Ceylon classes
  • interoperation with Java 8 lambdas—ability to pass Ceylon functions to Java SAM types
  • local import statements
  • support for spread operators * and *. with java.lang.Iterable and Java arrays
  • literal tuples in cases of a switch
  • small Characters
  • new Maven interop mode --fully-export-maven-dependencies for projects depending on multi-module platforms like Spring Boot
  • support for POM-only Maven artifacts
  • new Java EE-friendly compiler mode, making it easy to use Java frameworks that depend on reflective direct access to fields
  • ability to pass Ceylon metamodel to Java methods accepting java.lang.Class
  • ability to pass Ceylon strings to Java methods accepting java.lang.CharSequence
  • improved treatment of null values originating in calls to native Java
  • several bugfixes to relating to interop with overloaded Java methods
  • new command line options: --java, --incremental, and --include-dependencies

Naturally, the release incorporates many more bugfixes and minor enhancements.

IDE Changes

Ceylon IDE 1.3.1 for IntelliJ and Eclipse resolves more than 110 issues, and adds support for running and debugging Ceylon programs on WildFly Swarm.

SDK Changes

Exactly 15 issues affecting the Ceylon SDK have been fixed, and the new platform modules ceylon.interop.spring and ceylon.interop.persistence were introduced. New 1.3.1 releases of the platform modules are available in Herd.

Migration

  • For the JVM, this release is backwards-compatible with all previous releases of Ceylon since 1.2.0.

  • For JavaScript, this release is backwards-compatible only with the previous two releases (1.2.2 and 1.3.0).

Ceylon 1.3.1 is backward-compatible with Ceylon 1.3.0, and so it's not necessary to recompile or change dependencies. However, upgrading to version 1.3.1 of any Ceylon platform module is recommended.

About Ceylon

Ceylon is a modern, modular, statically typed programming language for the Java and JavaScript virtual machines. The language features a flexible and very readable syntax, a unique and uncommonly elegant static type system, a powerful module architecture, and excellent tooling, including an awesome IDE supporting both IntelliJ IDEA and the Eclipse platform.

Ceylon enables the development of cross-platform modules that execute portably in both virtual machine environments. Alternatively, a Ceylon module may target one or the other platform, in which case it may interoperate with native code written for that platform.

In the box

This release includes:

  • a complete language specification that defines the syntax and semantics of Ceylon in language accessible to the professional developer,
  • a command line toolset including compilers for Java and JavaScript, a documentation compiler, a test runner, a WAR archive packager, a "fat" JAR packager, and support for executing modular programs on the JVM and Node.js,
  • a powerful module architecture for code organization, dependency management, and module isolation at runtime, which also supports interoperation with OSGi, Jigsaw, Maven, and npm, and
  • the language module, our minimal, cross-platform, foundation-level API.

Available separately:

Language

Ceylon is a highly understandable object-oriented language with static typing. The language features:

  • an emphasis upon readability and a strong bias toward omission or elimination of potentially-harmful or potentially-ambiguous constructs and toward highly disciplined use of static types,
  • an extremely powerful and uncommonly elegant type system combining subtype and parametric polymorphism with:
    • first-class union and intersection types,
    • both declaration-site and use-site variance, and
    • the use of principal types for local type inference and flow-sensitive typing,
  • a unique treatment of function and tuple types, enabling powerful abstractions, along with the most elegant approach to null of any modern language,
  • first-class constructs for defining modules and dependencies between modules,
  • a very flexible syntax including comprehensions and support for expressing tree-like structures,
  • fully-reified generic types, on both the JVM and JavaScript virtual machines, and a unique typesafe metamodel.

More information about these language features may be found in the feature list and quick introduction.

Community

The Ceylon community site, https://ceylon-lang.org, includes documentation, and information about getting involved.

You can follow @ceylonlang on Twitter.

Source code

The source code for Ceylon, its specification, and its website, is freely available from GitHub.

Information about Ceylon's open source licenses is available here.

Issues

Bugs and suggestions may be reported in GitHub's issue tracker.

Acknowledgement

As always, we're deeply grateful to the community volunteers who contributed a substantial part of the current Ceylon codebase, working in their own spare time. The following people have contributed to Ceylon:

Gavin King, Stéphane Épardaud, Tako Schotanus, Tom Bentley, David Festal, Enrique Zamudio, Bastien Jansen, Emmanuel Bernard, Aleš Justin, Tomáš Hradec, James Cobb, Ross Tate, Max Rydahl Andersen, Mladen Turk, Lucas Werkmeister, Roland Tepp, Diego Coronel, Matej Lazar, John Vasileff, Toby Crawley, Julien Viet, Loic Rouchon, Stephane Gallès, Ivo Kasiuk, Corbin Uselton, Paco Soberón, Michael Musgrove, Daniel Rochetti, Henning Burdack, Luke deGruchy, Rohit Mohan, Griffin DeJohn, Casey Dahlin, Gilles Duboscq, Tomasz Krakowiak, Alexander Altman, Alexander Zolotko, Alex Szczuczko, Andrés G. Aragoneses, Anh Nhan Nguyen, Brice Dutheil, Carlos Augusto Mar, Charles Gould, Chris Gregory, klinger, Martin Voelkle, Mr. Arkansas, Paŭlo Ebermann, Vorlent, Akber Choudhry, Renato Athaydes, Flavio Oliveri, Michael Brackx, Brent Douglas, Lukas Eder, Markus Rydh, Julien Ponge, Pete Muir, Nicolas Leroux, Brett Cannon, Geoffrey De Smet, Guillaume Lours, Gunnar Morling, Jeff Parsons, Jesse Sightler, Oleg Kulikov, Raimund Klein, Sergej Koščejev, Chris Marshall, Simon Thum, Maia Kozheva, Shelby, Aslak Knutsen, Fabien Meurisse, Sjur Bakka, Xavier Coulon, Ari Kast, Dan Allen, Deniz Türkoglu, F. Meurisse, Jean-Charles Roger, Johannes Lehmann, allentc, Nikolay Tsankov, Chris Horne, Gabriel Mirea, Georg Ragaller, Harald Wellmann, Oliver Gondža, Stephen Crawley, Byron Clark, Francisco Reverbel, Jonas Berlin, Luke Hutchison, Nikita Ostroumov, Santiago Rodriguez, Sean Flanigan, Schalk W. Cronjé.

Ceylon 1.3.0 is now available

Ceylon 1.3.0 is a major release of the Ceylon language, with over 330 issues closed. This is the first release of Ceylon which supports Android development, the Node Package Manager (npm), and Wildfly Swarm.

  • For the JVM, this release is backwards-compatible with all releases of Ceylon 1.2 (1.2.0 to 1.2.2).

  • For JavaScript, this release is backwards-compatible only with the previous release (1.2.2).

Ceylon IDE 1.3.0 is now available for the two leading Java development environments:

Ceylon IDE for IntelliJ was designed for high performance in large projects with many Java dependencies, and is currently the best-performing IDE for Ceylon.

Changes

Enhancements to the language and command-line distribution include:

Naturally, the release incorporates many more bugfixes, minor enhancements, and performance improvements.

Support for Docker

Docker images for Ceylon are now available, making it very easy to run Ceylon programs in a Docker container.

IDE Changes

Ceylon IDE for IntelliJ is a brand-new development tool for IntelliJ IDEA and Android Studio, featuring incremental error reporting, code completion, basic refactoring, many intention actions, sophisticated navigation, searching, type hierarchy and file structure, online documentation, full integration with Ceylon Herd and much, much more.

Ceylon IDE for IntelliJ is written mostly in Ceylon, and reuses the Ceylon IDE Common project, the core of Ceylon IDE for Eclipse, which was completely rewritten in Ceylon.

Almost 60 issues were fixed in Ceylon IDE for Eclipse, and code completion was redesigned around a non-blocking approach which is much more responsive in large projects. In addition, improvements to the typechecker have resulted in significantly lower memory usage.

SDK Changes

Exactly 40 issues affecting the Ceylon SDK have been fixed.

Migration from Ceylon 1.2.2

Ceylon 1.3.0 is backward-compatible with Ceylon 1.2.2, and so it's not necessary to recompile or change dependencies. However, upgrading to version 1.3.0 of any Ceylon platform module is recommended.

About Ceylon

Ceylon is a modern, modular, statically typed programming language for the Java and JavaScript virtual machines. The language features a flexible and very readable syntax, a unique and uncommonly elegant static type system, a powerful module architecture, and excellent tooling, including an awesome IDE supporting both IntelliJ IDEA and the Eclipse platform.

Ceylon enables the development of cross-platform modules that execute portably in both virtual machine environments. Alternatively, a Ceylon module may target one or the other platform, in which case it may interoperate with native code written for that platform.

In the box

This release includes:

  • a complete language specification that defines the syntax and semantics of Ceylon in language accessible to the professional developer,
  • a command line toolset including compilers for Java and JavaScript, a documentation compiler, a test runner, a WAR archive packager, a "fat" JAR packager, and support for executing modular programs on the JVM and Node.js,
  • a powerful module architecture for code organization, dependency management, and module isolation at runtime, which also supports interoperation with OSGi, Jigsaw, Maven, and npm, and
  • the language module, our minimal, cross-platform, foundation-level API.

Available separately:

  • updated versions of the platform modules that comprise the Ceylon SDK,
  • a code formatter as a plugin for the ceylon command,
  • a plugin for the ceylon command that supports compilation and execution for the Dart VM, and
  • two full-featured integrated development environments: for Eclipse and IntelliJ IDEA.

Language

Ceylon is a highly understandable object-oriented language with static typing. The language features:

  • an emphasis upon readability and a strong bias toward omission or elimination of potentially-harmful or potentially-ambiguous constructs and toward highly disciplined use of static types,
  • an extremely powerful and uncommonly elegant type system combining subtype and parametric polymorphism with:
    • first-class union and intersection types,
    • both declaration-site and use-site variance, and
    • the use of principal types for local type inference and flow-sensitive typing,
  • a unique treatment of function and tuple types, enabling powerful abstractions, along with the most elegant approach to null of any modern language,
  • first-class constructs for defining modules and dependencies between modules,
  • a very flexible syntax including comprehensions and support for expressing tree-like structures,
  • fully-reified generic types, on both the JVM and JavaScript virtual machines, and a unique typesafe metamodel.

More information about these language features may be found in the feature list and quick introduction.

Community

The Ceylon community site, https://ceylon-lang.org, includes documentation, and information about getting involved.

Source code

The source code for Ceylon, its specification, and its website, is freely available from GitHub.

Information about Ceylon's open source licenses is available here.

Issues

Bugs and suggestions may be reported in GitHub's issue tracker.

Acknowledgement

As always, we're deeply grateful to the community volunteers who contributed a substantial part of the current Ceylon codebase, working in their own spare time. The following people have contributed to Ceylon:

Gavin King, Stéphane Épardaud, Tako Schotanus, Tom Bentley, David Festal, Enrique Zamudio, Bastien Jansen, Emmanuel Bernard, Aleš Justin, Tomáš Hradec, James Cobb, Ross Tate, Max Rydahl Andersen, Mladen Turk, Lucas Werkmeister, Roland Tepp, Diego Coronel, Matej Lazar, John Vasileff, Toby Crawley, Julien Viet, Loic Rouchon, Stephane Gallès, Ivo Kasiuk, Corbin Uselton, Paco Soberón, Michael Musgrove, Daniel Rochetti, Henning Burdack, Luke deGruchy, Rohit Mohan, Griffin DeJohn, Casey Dahlin, Gilles Duboscq, Tomasz Krakowiak, Alexander Altman, Alexander Zolotko, Alex Szczuczko, Andrés G. Aragoneses, Anh Nhan Nguyen, Brice Dutheil, Carlos Augusto Mar, Charles Gould, Chris Gregory, klinger, Martin Voelkle, Mr. Arkansas, Paŭlo Ebermann, Vorlent, Akber Choudhry, Renato Athaydes, Flavio Oliveri, Michael Brackx, Brent Douglas, Lukas Eder, Markus Rydh, Julien Ponge, Pete Muir, Nicolas Leroux, Brett Cannon, Geoffrey De Smet, Guillaume Lours, Gunnar Morling, Jeff Parsons, Jesse Sightler, Oleg Kulikov, Raimund Klein, Sergej Koščejev, Chris Marshall, Simon Thum, Maia Kozheva, Shelby, Aslak Knutsen, Fabien Meurisse, Sjur Bakka, Xavier Coulon, Ari Kast, Dan Allen, Deniz Türkoglu, F. Meurisse, Jean-Charles Roger, Johannes Lehmann, allentc, Nikolay Tsankov, Chris Horne, Gabriel Mirea, Georg Ragaller, Harald Wellmann, Oliver Gondža, Stephen Crawley, Byron Clark, Francisco Reverbel, Jonas Berlin, Luke Hutchison, Nikita Ostroumov, Santiago Rodriguez, Sean Flanigan, Schalk W. Cronjé.

Object construction and validation

When porting Java code to Ceylon, I sometimes run into Java classes where the constructor mixes validation with initialization. Let's illustrate what I mean with a simple but very contrived example.

Some bad code

Consider this Java class. (Try not to write code like this at home, kiddies!)

public class Period {

    private final Date startDate;
    private final Date endDate;

    //returns null if the given String
    //does not represent a valid Date
    private Date parseDate(String date) {
       ...
    }

    public Period(String start, String end) {
        startDate = parseDate(start);
        endDate = parseDate(end);
    }

    public boolean isValid() {
        return startDate!=null && endDate!=null;
    }

    public Date getStartDate() {
        if (startDate==null) 
            throw new IllegalStateException();
        return startDate;
    }

    public Date getEndDate() {
        if (endDate==null)
            throw new IllegalStateException();
        return endDate;
    }
} 

Hey, I warned you it was going to be contrived. But it's really not uncommon to find stuff like this in real Java code.

The problem here is that even if validation of the input parameters (in the elided parseDate() method) fails, we still receive an instance of Period. But the Period we get isn't actually in a "valid" state. What do I mean by that, precisely?

Well, I would say that an object is in an invalid state if it can't respond meaningfully to its public operations. In this case, getStartDate() and getEndDate() can throw an IllegalStateException, which is a condition I would consider not "meaningful".

Another way to look at this is that what we have here is a failure of type safety in the design of Period. Unchecked exceptions represent a "hole" in the type system. So a more typesafe design for Period would be one which never uses unchecked exceptions—that doesn't throw IllegalStateException, in this case.

(Actually, in practice, in real code, I'm more likely to encounter a getStartDate() which doesn't check for null, and actually results in a NullPointerException further down the line, which is even worse.)

We can easily translate the above Period class to Ceylon:

shared class Period(String start, String end) {

    //returns null if the given String
    //does not represent a valid Date
    Date? parseDate(String date) => ... ;

    value maybeStartDate = parseDate(start);
    value maybeEndDate = parseDate(end);

    shared Boolean valid
        => maybeStartDate exists 
        && maybeEndDate exists;

    shared Date startDate {
        assert (exists maybeStartDate);
        return maybeStartDate;
    }

    shared Date endDate {
        assert (exists maybeEndDate);
        return maybeEndDate;
    }
} 

And, of course, this code suffers from the same problem as the original Java code. The two assertions are screaming at us that there is a problem with the typesafety of the code.

Making the Java code better

How could we improve this code in Java. Well, here's a case where Java's much-maligned checked exceptions would be a really reasonable solution! We could slightly change Period to throw a checked exception from its constructor:

public class Period {

    private final Date startDate;
    private final Date endDate;

    //throws if the given String
    //does not represent a valid Date
    private Date parseDate(String date)
            throws DateFormatException {
       ...
    }

    public Period(String start, String end) 
            throws DateFormatException {
        startDate = parseDate(start);
        endDate = parseDate(end);
    }

    public Date getStartDate() {
        return startDate;
    }

    public Date getEndDate() {
        return endDate;
    }
} 

Now, with this solution, we can never get a Period in an invalid state, and the code which instantiates Period is obligated by the compiler to handle the case of invalid input by catching the DateFormatException somewhere.

try {
    Period p = new Period(start, end);
    ...
}
catch (DateFormatException dfe) {
    ...
}

This is a good and excellent and righteous use of checked exceptions, and it's unfortunate that I only rarely find Java code which uses checked exceptions like this.

Making the Ceylon code better

What about Ceylon? Ceylon doesn't have checked exceptions, so we'll have to look for a different solution. Typically, in cases where Java would call for use of a function that throws a checked exception, Ceylon would call for the use of a function that returns a union type. Since the initializer of a class can't return any type other than the class itself, we'll need to extract some of the mixed initialization/validation logic into a factory function.

//returns DateFormatError if the given 
//String does not represent a valid Date
Date|DateFormatError parseDate(String date) => ... ;

shared Period|DateFormatError parsePeriod
        (String start, String end) {
    value startDate = parseDate(start);
    if (is DateFormatError startDate) {
        return startDate;
    }
    value endDate = parseDate(end);
    if (is DateFormatError endDate)  {
        return endDate;
    }
    return Period(startDate, endDate);
}

shared class Period(startDate, endDate) {
    shared Date startDate;
    shared Date endDate;
} 

The caller is forced by the type system to deal with DateFormatError:

value p = parsePeriod(start, end);
if (is DateFormatError p) {
    ...
}
else {
    ...
}

Or, if we didn't care about the actual problem with the given date format (probable, given that the initial code we were working from lost that information), we could just use Null instead of DateFormatError:

//returns null if the given String 
//does not represent a valid Date
Date? parseDate(String date) => ... ;

shared Period? parsePeriod(String start, String end)
    => if (exists startDate = parseDate(start), 
           exists endDate = parseDate(end))
       then Period(startDate, endDate)
       else null;

shared class Period(startDate, endDate) {
    shared Date startDate;
    shared Date endDate;
} 

At least arguably, the approach of using a factory function is superior, since in general it obtains better separation between validation logic and object initialization. This is especially useful in Ceylon, where the compiler enforces some quite heavy-handed restrictions on object initialization logic in order to guarantee that all fields of the object are assigned exactly once.

Summary

In conclusion:

  • Try to separate validation from initialization, wherever reasonable.
  • Validation logic doesn't usually belong in constructors (especially not in Ceylon).
  • Don't create objects in "invalid" states.
  • An "invalid" state can sometimes be detected by looking for failures of typesafety.
  • In Java, a constructor or factory function that throws a checked exception is a reasonable alternative.
  • In Ceylon, a factory function that returns a union type is a reasonable alternative.

Modelling failure in Ceylon

In all programming languages, we need to deal with operations than can "fail":

  • a pure function might fail to produce a result, or
  • an impure function might fail to produce its desired side-effect (create a new file, or whatever).

In neither case can we just blindly continue with the rest of the computation. In the first case, the result of the function might be the input to some other function. In the second case, subsequent operations might assume that the side-effect occurred (that the file now exists, or whatever).

Thus, it's clear that there must be some way for an operation to signal failure to the calling code. There are two broad mechanisms provided by programming languages for signalling failure:

  1. Failure may be indicated via return values: an error code, null, a union type, or a sum (enumerated) type, for example, Option/Maybe or Either.
  2. Failures may be signalled and handled within some sort of exception or "panic" facility.

Most modern programming languages support both of these mechanisms, though of course the details vary. In particular, languages offer varying degrees of typesafety.

  • In languages with proper support for sum types or union types, return values may be used to model failure in a very robust and typesafe way.
  • In languages with some sort of effect typing, for example, Java-style checked exceptions, the exceptions are themselves typesafe.

By typesafe what I mean is that an operation that can fail declares the possibility of failure in its signature, and the immediately calling code is forced by the compiler to explicitly handle the failure.

Types of failure

So what facilities should a language offer for modelling failure? Return codes or exceptions? Typesafe or not? To arrive at a partial answer to this question, let's start with the following classification of "failures":

  • Some failures represent problems that the immediately calling code is very unlikely to be able to recover from. Examples include transaction rollbacks, network failures, low memory conditions, or stack overflows.
  • Some failures are typically the result of bugs in the program logic. Examples include assertion failures, division by zero, and use of null pointers. This is a class of failure that, as far as possible, we would like to detect at compile time, but no type system will ever be powerful enough to detect all of these failures. After a few minutes of thought, you should be able to convince yourself this class of problems is actually a subclass of the first class: how can any computation possibly recover meaningfully from a bug in its own logic?
  • Finally, there are "failures" that often represent recoverable conditions. For example, one might recover from a nonexistent file by creating a file. Note that failures in this class need not always be recoverable.

Given this classification, I arrive relatively quickly at the following conclusions.

Handling recoverable failures

For "recoverable" conditions, the failure should be typesafe. The compiler should be able to verify that the calling code has made an explicit decision on what to do about the failure: recovering from it, or transforming it into an unrecoverable failure. We want to prevent recoverable failures from going unnoticed by accident.

It's clear that unchecked exceptions—or other untypesafe solutions such as returning null in a language like Java where null is untypesafe—don't prevent this, and allow failure conditions to go unnoticed, leading to bugs.

The most convenient, elegant, and efficient way to represent a recoverable failure is a union-typed return value. For example, if I have a function for parsing JSON, and it can fail for illegal input, I could use a function with the following signature:

JsonObject|ParseError parseJson() => ... ;

Or, if it seems that ParseError carries no useful information, I could just use Null instead:

JsonObject? parseJson() => ... ;

Alternatively, in some advanced cases, one could use a sum (enumerated) return type.

interface ParseResult of ParseSuccessful | ParseError {}
class ParseSuccessful(shared JsonObject result) satisfies ParseResult {}
class ParseError(shared String message) satisfies ParseResult {}

ParseResult parseJson() => ... ;

This is not usually necessary in Ceylon, however.

Handling unrecoverable failures

Now, for "unrecoverable" conditions, the failure should be an untyped (unchecked) exception. For an unrecoverable failure, we shouldn't be polluting the calling code with concerns it can't possibly do anything useful with. We want the failure to propagate quickly and transparently to some centralized, generic, infrastructure-level error handling.

Note that, since unchecked exceptions don't appear in the signature of the operation, the caller doesn't receive any kind of "fair warning" that they can occur. They represent a sort of designed-in "hole" in the type system.

When in doubt

But wait, you're probably thinking, haven't I left a huge question begging here?

What about failure that doesn't fall cleanly into "recoverable" or "unrecoverable"?

Isn't there a huge grey area there, filled with failures that are sometimes recoverable by the immediately-calling code?

Indeed there is. And I would say that, as a rule of thumb, treat these failures as recoverable.

Consider our parseJson() function above. A syntax error in the given JSON text could easily be the result of a bug in our program, but, crucially, it's not a bug in parseJson() itself. The code that knows whether it's a program bug or something else is the calling code, not the parseJson() function.

And it's always easy for the calling code to transform a recoverable failure into an unrecoverable failure. For example:

assert (is JsonObject result = parseJson(json));
//result must be a JsonObject here

Or:

value result = parseJson(json);
if (is ParseError result) {
    throw AssertionError(result.message);
}
//result must be a JsonObject here

That is, when in doubt, we make the calling code explicitly document its assumptions.

Looking at this another way, we err on the side of typesafety, since having too many unchecked exceptions starts to undermine the whole value of the static type system.

Furthermore, it's quite likely that the calling code is better placed to produce an error with more meaningful information than the code it's calling (though I have not shown that in the snippets above).

Three things to consider

I promised a "partial" answer to my original question, because there are still a couple of questions that I'm not sure I have a completely bottled answer to, and there's debate over these issues in the Ceylon community.

Is AssertionError overused?

First, what kind of failures are legitimate uses of AssertionError? Should every AssertionError represent a bug in the program? Is it ever reasonable for a library to throw an AssertionError when it encounters a situation it considers misuse of its API? Is it acceptable for generic exception handling code to recover from an AssertionError, or should AssertionErrors be considered fatal?

My answers would be yes, yes, and yes. But perhaps that implies that it was a mistake to follow Java in making AssertionError an Error instead of a plain Exception. (This leads to a larger debate about the role of Error.)

Is Null overused?

Second, the class Null is a seductively convenient way to represent failure of functions which return a value. But are we overusing it? Would it have been better to make the return type of Map<Key,Item>.get() be Item|NoItem<Key> instead of the much more generic type Item?, that is, Item|Null?

Perhaps. In a sense, returning null is like throwing Exception: a little too generic. But since a null return value must be handled by the immediately calling code, which is much better placed to know what this instance of null represents, it's not as harmful as a generic Exception which is handled far from its source.

Whether you agree with that or not, it still might be best to avoid operations where null can result from multiple different failure conditions. I have broken this rule in the past, and I'm going to be more careful in future.

Functions with no useful return value

Third, for functions with no useful return value, that is, functions which are called only for their side-effect—where the calling code has the option of simply ignoring any return value representing failure—should we err on the side of throwing an exception?

Or, alternatively, should the language offer some way to force the caller to do something with the return value of non-void function?

Ceylon doesn't have (and won't have) checked exceptions, but one could argue that this is the one situation where they would be most useful.

Conclusion

So, ultimately, there are some unanswered questions, and grey areas, but it seems to me that at least we have a rather strong conceptual framework in which to investigate these problems. And it's clear that the combination of facilities—union types, together with unchecked exceptions—is a powerful foundation for robust failure handling.