Blog of Gavin King

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.

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.

Dependency injection in Ceylon with Weld and Guice

I'm personally ambivalent about the benefits of dependency injection. On the one hand, I recognize its usefulness in certain container environments such as Java EE. (For the record, I was the author of the CDI 1.0 specification, with my JCP Expert Group.) On the other hand, given the nature of what I've been working on for the last few years, I don't really have a use for it in my own programs.

But there are plenty of folks out there who swear by dependency injection, and ask me what Ceylon offers in this area. The short answer is: nothing special; the Ceylon SDK is architected around the notion of modular libraries. It offers neither framework nor container. This makes the SDK as general purpose as possible, meaning it can be reused from any other container environment (say, Java EE, vert.x, OSGi, or whatever).

So if you want dependency injection in Ceylon today, you're going to have to use a container written in Java. Fortunately, Ceylon 1.2 features such excellent interoperation with Java that this results in barely any friction at all. Surely someone will write a dependency injection container in Ceylon some day, but, as we're about to see, there's no urgency at all.

I'm going to explore:

  • Weld, which is the reference implementation of CDI, developed by my colleagues at Red Hat, and,
  • in the interests of giving equal time to a "competitor", Google's Guice, originally written by my friend Bob Lee, which was one of the major influences on the CDI specification.

These are my favorite containers for Java, though of course Spring has legions of fans. Perhaps I'll find time to play with it some other day.

You can find the example code in the following Git repository:

https://github.com/ceylon/ceylon-examples-di

Weld

I found it extremely straightforward to use Weld in Ceylon, except for one relatively minor problem, which I'll mention below.

Module descriptor for Weld

Weld provides a fat jar in Maven Central, which makes it especially easy to use in Ceylon. I used the following module descriptor to download Weld from Maven Central and import it into my project:

native("jvm")
module weldelicious "1.0.0" {
    import "org.jboss.weld.se:weld-se" "2.3.1.Final";
    import ceylon.interop.java "1.2.0";
}

Where org.jboss.weld.se is the Maven group id, and weld-se is the Maven artifact id. (I have not the slightest clue what these things actually mean, I just know there are two of them.)

I also imported the Ceylon SDK module ceylon.interop.java because I'm going to use its javaClass() function.

Bootstrapping Weld

Though it's not part of the CDI specification, Weld offers a very simple API for creating a container. I copy/pasted the following code from stackoverflow:

import org.jboss.weld.environment.se { Weld }

shared void run() {

    value container = Weld().initialize();

    //do stuff with beans
    ...

    container.shutdown();

}

I tried to run this function.

Gotcha!

Just like every other CDI developer ever, I forgot the beans.xml file. Fortunately, Weld gave me a rather clear error message. Not quite as poetic as "se te escapó la tortuga", perhaps, but good enough to remind me of this requirement of the spec. (Yeah, the spec I wrote.)

To resolve the problem, I added an empty file named beans.xml to the directory resource/weldelicious/ROOT/META-INF, which is the magical location to use if you want Ceylon to put a file into the META-INF directory of a module archive.

Defining Weld beans

I defined the following interface, for a bean I hoped to inject:

interface Receiver {
    shared formal void accept(String message);
}

Next, I defined a bean which depends on an instance of this interface:

import javax.inject { inject }

inject class Sender(Receiver receiver) {
    shared void send() => receiver.accept("Hello!");
}

(The inject annotation is the thing you write @Inject in Java.)

Finally, we need a bean which implements Receiver:

class PrintingReceiver() satisfies Receiver {
    accept = print;
}

Obtaining and calling a bean

Going back to the run() function, I added some code to obtain a Sender from the container, and call send():

import org.jboss.weld.environment.se { Weld }
import ceylon.interop.java { type = javaClass }

shared void run() {

    value container = Weld().initialize();

    value sender 
            = container
                .select(type<Sender>())
                .get();

    sender.send();

    weld.shutdown();

}

Note that I'm using the javaClass() function to obtain an instance of java.lang.Class for the Ceylon type Sender. An alternative approach, which uses only a CDI API, and which also works for generic types, is to use javax.enterprise.inject.TypeLiteral:

value sender 
        = container
            .select(object extends TypeLiteral<Sender>(){})
            .get();

Unfortunately, that's a little more verbose.

Named constructor injection

Using a little quick fix in the IDE, we can transform the Sender class into a class with a default constructor:

class Sender {
    Receiver receiver;
    inject shared new (Receiver receiver) {
        this.receiver = receiver;
    }
    shared void send() => receiver.accept("Hello!");
}

As far as Weld is concerned, this is the same as what we had before.

But we can even give our constructor a name:

class Sender {
    Receiver receiver;
    inject shared new inject(Receiver receiver) {
        this.receiver = receiver;
    }
    shared void send() => receiver.accept("Hello!");
}

Due to unanticipated serendipity, this actually Just Works.

Method and field injection

I don't think that method or field injection is a very natural thing to do in Ceylon, and so I don't recommend it. However, it does work, just as long as you mark any fields initialized by injection with the late annotation:

This works, but doesn't feel very Ceylonic:

class Sender() {
    inject late Receiver receiver;
    shared void send() => receiver.accept("Hello!");
}

This works too:

class Sender() {
    late Receiver receiver;
    inject void init(Receiver receiver) {
        this.receiver = receiver;
    }
    shared void send() => receiver.accept("Hello!");
}

Using a CDI producer

One nice thing about using Ceylon with Weld is that you can use the produces annotation on a toplevel function.

import javax.enterprise.inject { produces }

produces Receiver createReceiver() 
        => object satisfies Receiver {
            accept = print;
        };

CDI qualifiers

We can define CDI qualifier annotations in Ceylon:

import javax.inject { qualifier }

annotation Fancy fancy() => Fancy();
final qualifier annotation class Fancy() 
        satisfies OptionalAnnotation<Fancy> {}

A qualifier annotation must be applied at both the injection point and to the bean or producer function. First, I annotated the bean class:

fancy class FancyReceiver() satisfies Receiver {
    accept(String message) 
            => print(message + " \{BALLOON}\{PARTY POPPER}");
}

Next, I tried annotating an injected initializer parameter:

//this doesn't work!
inject class Sender(fancy Receiver receiver) {
    shared void send() => receiver.accept("Hello!");
}

Unfortunately, this didn't work. When compiled to Java bytecode, Ceylon actually places this fancy annotation on a generated getter method of Sender, not on the parameter, and Weld only looks for qualifier annotations on injected parameters. I had to use constructor injection to make the qualifier work right:

//this does work
class Sender {
    Receiver receiver;
    inject shared new (fancy Receiver receiver) {
        this.receiver = receiver;
    }
    shared void send() => receiver.accept("Hello!");
}

For the record, qualifier annotations also work with method injection. They don't work with field injection.

This was the only disappointment I had using Weld with Ceylon, and I believe I already know how to solve this in Ceylon 1.2.1.

Scoped beans

You can define scoped beans (beans with what the CDI spec calls a normal scope) in Ceylon, just by applying a scope annotation to the bean:

import javax.enterprise.context { applicationScoped }

applicationScoped
class PrintingReceiver() satisfies Receiver {
    accept = print;
}

However, there's something to be careful of here: CDI creates proxies for scoped beans, and since the operations of a Ceylon class are "final" by default, you have a choice between:

  • annotating all operations of the bean default, or
  • injecting an interface instead of the concrete bean class.

I think the second option is a much better path to go down, and is probably even the best approach in Java.

Surely the same caveat applies to beans with CDI interceptors or decorators, though I did not test that.

Weld offers lots of additional functionality which I did not have time to test, but that I anticipate will work in Ceylon.

Guice

Guice was also pretty easy to get set up, though I wasted a bit of time on the Maven side of things.

Module overrides for Guice

Guice doesn't come in a fat jar, so we'll have to deal with a common problem when using Maven modules from Ceylon. Maven is designed for a flat Java classpath, so a Maven module doesn't come with metadata about which of its dependencies are re-exported via its public API. There are three basic strategies for solving this problem:

  1. Compile and run with a flat classpath by using --flat-classpath. This makes Ceylon work like Java, and robs us of module isolation.
  2. Use --export-maven-dependencies to re-export all dependencies of every Maven module.
  3. Use an overrides.xml file to explicitly specify which dependencies are re-exported.

We're going to go with option 3, since it's the hardest.

But wait—you must be thinking—XML?! And yeah, don't worry, we hate XML just as much as you do. This is a stopgap measure until Ceylon has real assemblies. Once we have assemblies, you'll be able to override module dependencies in a Ceylon assembly descriptor.

Anyway, after that longwinded preamble, all I had to do was mark javax.inject as a shared dependency:

<overrides xmlns="http://www.ceylon-lang.org/xsd/overrides">
    <module groupId="com.google.inject" 
         artifactId="guice" 
            version="4.0">
        <share groupId="javax.inject" 
            artifactId="javax.inject"/>
    </module>
</overrides>

You're very welcome to copy and paste the above bit of boilerplate into your own Ceylon and Guice projects.

Module descriptor for Guice

The following module descriptor fetches Guice and its dependencies from Maven Central, and imports Guice into the project:

native("jvm")
module guicy "1.0.0" {
    import "com.google.inject:guice" "4.0";
    import ceylon.interop.java "1.2.0";
}

Code we can reuse from the Weld example

Since Guice recognizes the inject annotation defined in javax.inject, we can reuse the definitions of Sender, Receiver, and PrintingReceiver we started out with above.

import javax.inject { inject }

inject class Sender(Receiver receiver) {
    shared void send() => receiver.accept("Hello!");
}

interface Receiver {
    shared formal void accept(String message);
}

class PrintingReceiver() satisfies Receiver {
    accept = print;
}

Bootstrapping Guice

Guice has the notion of a module object, which has a collection of bindings of types to objects. Unlike Weld, which automatically scans our module archive looking for beans, bindings must be registered explicitly in Guice.

import ceylon.interop.java {
    type = javaClass
}
import com.google.inject {
    AbstractModule,
    Guice {
        createInjector
    },
    Injector
}

Injector injector
        = createInjector(
    object extends AbstractModule() {
        shared actual void configure() {
            bind(type<Receiver>()).to(type<PrintingReceiver>());
        }
    });

This code binds the implementation PrintingReceiver to the interface Receiver.

Obtaining and calling an object

Now it's easy to obtain and call a container-bound instance of Sender:

import ceylon.interop.java {
    type = javaClass
}

shared void run() {
    value sender = injector.getInstance(type<Sender>());
    sender.send();
}

We're again using javaClass(), but Guice has its own TypeLiteral. (For the record, CDI stole TypeLiteral from Guice.)

import com.google.inject {
    Key,
    TypeLiteral
}

shared void run() {
    value key = Key.get(object extends TypeLiteral<Sender>(){});
    value sender = injector.getInstance(key);
    sender.send();
} 

Constructor injection

Injection into default constructors works, and looks exactly like what it looks like for Weld. However, injection into named constructors doesn't work with Ceylon 1.2.0 and Guice 4.0. This is pretty easy to fix on our side, and so it should work in Ceylon 1.2.1.

Method and field injection

The creators of Guice strongly prefer constructor injection, which is, as we have observed, also more natural in Ceylon. But method and field injection works fine, as with Weld, if you mark injected field late.

Provider methods

Guice scans the module object for methods annotated provides.

import com.google.inject {
    AbstractModule,
    Guice {
        createInjector
    },
    Injector,
    provides
}

Injector injector
        = createInjector(
    object extends AbstractModule() {
        shared actual void configure() {}
        provides Receiver createReceiver()
                => object satisfies Receiver {
                    accept = print;
                };
    });

I find this significantly inferior to the approach in CDI where producer methods can be defined as toplevel functions.

Binding annotations

Guice's binding annotations work almost exactly like CDI qualifier annotations (since that's where CDI copied them from). The code to define a binding annotation is exactly the same as for Weld.

import javax.inject { qualifier }

annotation Fancy fancy() => Fancy();
final binding annotation class Fancy() 
        satisfies OptionalAnnotation<Fancy> {}

The qualifier annotation must be specified when defining a binding:

Injector injector
        = createInjector(
    object extends AbstractModule() {
        shared actual void configure() {
            bind(type<Receiver>())
                .to(type<PrintingReceiver>());
            bind(type<Receiver>())
                .annotatedWith(Fancy()) //binding annotation
                .to(type<FancyReceiver>());
        }
    });

Just like in Weld, qualifier annotations work with constructor or method injection, but don't currently work with initializer parameter or field injection.

Scoped beans

Like CDI, Guice has scoped objects.

import com.google.inject { singleton }

singleton
class PrintingReceiver() satisfies Receiver {
    accept = print;
}

I didn't have time to test this feature of Guice extensively, but I happen to know that Guice doesn't use proxies, so it's not necessary to use an interface instead of a concrete class.

Conclusion

If you want dependency injection in Ceylon, it's clear that you have at least two excellent options.

Porting Ceylon IDE to IntelliJ

We've had many questions about developing Ceylon in IntelliJ IDEA, so I thought it would be worth a quick status update.

TL;DR: The screenshots are below.

As you might know, Ceylon already has the most feature rich IDE of any modern language for the JVM, with some features that even the Java IDE for Eclipse doesn't have. But IntelliJ users don't like having to switch to Eclipse when they code Ceylon, so a few months ago we got serious about porting Ceylon IDE to IntelliJ. Bastien Jansen is working on this fulltime, together with David Festal from SERLI.

The approach they're taking is to refactor reusable functionality of Ceylon IDE out into a separate project ceylon-ide-common. Simultaneously they're rewriting the common code in Ceylon (which David reports is really helping simplify and improve the code). Then this "abstracted" code is reused in the ceylon-ide-intellij project—which is also being written in Ceylon—and in ceylon-ide-eclipse. Thus, ceylon-ide-common gives us a common foundation for both IDEs, and enables us to get some really sophisticated functionality into the IntelliJ IDE very quickly.

Even better, once ceylon-ide-common is stabilized, we can reuse it elsewhere, for example, in the Web IDE, or in the new (experimental) plugin for NetBeans. Bastien was able to add autocompletion to the experimental Netbeans plugin in about 2-3 hours.

This also all demonstrates just how well Ceylon's Java interop works in practice. Here we have Java calling Ceylon and Ceylon calling back to Java all over the place!

The IntelliJ plugin isn't really usable just yet, since David is still working on abstraction of the Ceylon IDE incremental builder, but we expect to have a first release in a handful of months.

Screenshots

Ceylon IDE for IntelliJ already features completion:

completion

Including linked mode argument completion:

linked mode

Outline view and hover:

outline     hover

Live error reporting:

errors

And execution:

run

Much more functionality is coming soon!