Help

This is the fifth installment in a series of articles introducing the Ceylon language. Note that some features of the language may change before the final release.

Narrowing the type of an object reference

In any language with subtyping there is the hopefully occasional need to perform narrowing conversions. In most statically-typed languages, this is a two-part process. For example, in Java, we first test the type of the object using the instanceof operator, and then attempt to downcast it using a C-style typecast. This is quite curious, since there are virtually no good uses for instanceof that don't involve an immediate cast to the tested type, and typecasts without type tests are dangerously non-typesafe.

As you can imagine, Ceylon, with its emphasis upon static typing, does things differently. Ceylon doesn't have C-style typecasts. Instead, we must test and narrow the type of an object reference in one step, using the special if (is ... ) construct. This construct is very, very similar to if (exists ... ) and if (nonempty ... ), which we met earlier.

Object obj = ... ; 
if (is Hello obj) {
    obj.say();
}

The switch statement can be used in a similar way:

Object obj = ... ; 
switch(obj) 
case (is Hello) {
    obj.say();
} 
case (is Person) {
    stream.writeLine(obj.firstName);
} 
else {
    stream.writeLine("Some miscellaneous thing");
}

These constructs protect us from inadvertantly writing code that would cause a ClassCastException in Java, just like if (exists ... ) protects us from writing code that would cause a NullPointerException.

More about union types

We've seen a few examples of how ad-hoc union types are used in Ceylon. Let's just revisit the notion to make sure we completely understand it. When I declare the type of something using a union type X|Y, I'm saying that only expressions of type X and expressions of type Y are assignable to it. The type X|Y is a supertype of both X and Y. The following code is well-typed:

void print(String|Natural|Integer val) { ... }

print("hello");
print(69);
print(-1);

But what operations does a type like String|Natural|Integer have? What are its supertypes? Well, the answer is pretty intuitive: T is a supertype of X|Y if and only if it is a supertype of both X and Y. The Ceylon compiler determines this automatically. So the following code is also well-typed:

Natural|Integer i = ... ;
Number num = i;
String|Natural|Integer val = i;
Object obj = val;

However, num is not assignable to val, since Number is not a supertype of String.

Of course, it's very common to narrow an expression of union type using a switch statement. Usually, the Ceylon compiler forces us to write an else clause in a switch, to remind us that there might be additional cases which we have not handled. But if we exhaust all cases of a union type, the compiler will let us leave off the else clause.

void print(String|Natural|Integer val) {
    switch (val)
    case (is String) { writeLine(val); }
    case (is Natural) { writeLine("Natural: " + val); }
    case (is Integer) { writeLine("Integer: " + val); }
}

Enumerated subtypes

Sometimes it's useful to be able to do the same kind of thing with the subtypes of an ordinary type. First, we need to explicitly enumerate the subtypes of the type using the of clause:

abstract class Hello() 
        of DefaultHello | PersonalizedHello { 
    ...
}

(This makes Hello into Ceylon's version of what the functional programming community calls an algebraic data type.)

Now the compiler won't let us declare additional subclasses of Hello, and so the union type DefaultHello|PersonalizedHello is exactly the same type as Hello. Therefore, we can write switch statements without an else clause:

Hello hello = ... ; 
switch (hello) 
case (is DefaultHello) {
    writeLine("What's your name?");
} 
case (is PersonalizedHello) {
    writeLine("Nice to hear from you again!");
}

Now, it's usually considered bad practice to write long switch statements that handle all subtypes of a type. It makes the code non-extensible. Adding a new subclass to Hello means breaking all the switch statements that exhaust its subtypes. In object-oriented code, we usually try to refactor constructs like this to use an abstract method of the superclass that is overridden as appropriate by subclasses.

However, there are a class of problems where this kind of refactoring isn't appropriate. In most object-oriented languages, these problems are usually solved using the visitor pattern.

Visitors

Let's consider the following tree visitor implementation:

abstract class Node() {
    shared formal void accept(Visitor v);
}
class Leaf(Object val) extends Node() {
    shared Object value = val;
    shared actual void accept(Visitor v) { 
        v.visitLeaf(this); 
    }
}
class Branch(Node left, Node right) extends Node() {
    shared Node leftChild = left;
    shared Node rightChild = right;
    shared actual void accept(Visitor v) { 
        v.visitBranch(this);
    }
}
interface Visitor {
    shared formal void visitLeaf(Leaf l);
    shared formal void visitBranch(Branch b);
}

We can create a method which prints out the tree by implementing the Visitor interface:

void print(Node node) {
    object printVisitor satisfies Visitor {
        shared actual void visitLeaf(Leaf l) {
            writeLine("Found a leaf: " l.value "!");
        }
        shared actual void visitBranch(Branch b) {
            b.leftChild.accept(this);
            b.rightChild.accept(this);
        }
    }
    node.accept(printVisitor);
}

Notice that the code of printVisitor looks just like a switch statement. It must explicitly enumerate all subtypes of Node. It breaks if we add a new subtype of Node to the Visitor interface. This is correct, and is the desired behavior. By break, I mean that the compiler lets us know that we have to update our code to handle the new subtype.

In Ceylon, we can achieve the same effect, with less verbosity, by enumerating the subtypes of Node in its definition, and using a switch:

abstract class Node() of Leaf | Branch {}
class Leaf(Object val) extends Node() {
    shared Object value = val;
}
class Branch(Node left, Node right) extends Node() {
    shared Node leftChild = left;
    shared Node rightChild = right;
}

Our print() method is now much simpler, but still has the desired behavior of breaking when a new subtype of Node is added.

void print(Node node) {
    switch (node)
    case (is Leaf) {
        writeLine("Found a leaf: " node.value "!");
    }
    case (is Branch) {
        print(node.leftChild);
        print(node.rightChild);
    }
}

Typesafe enumerations

Ceylon doesn't have anything exactly like Java's enum declaration. But we can emulate the effect using the of clause.

shared class Suit(String name) 
        of hearts | diamonds | clubs | spades 
        extends Case(name) {}
        
shared object hearts extends Suit("hearts") {} 
shared object diamonds extends Suit("diamonds") {} 
shared object clubs extends Suit("clubs") {} 
shared object spades extends Suit("spades") {}

We're allowed to use the names of object declarations in the of clause if they extend the language module class Case.

Now we can exhaust all cases of Suit in a switch:

void print(Suit suit) {
    switch (suit)
    case (hearts) { writeLine("Heartzes"); }
    case (diamonds) { writeLine("Diamondzes"); }
    case (clubs) { writeLine("Clidubs"); }
    case (spades) { writeLine("Spidades"); }
}

(Note that these cases are ordinary value cases, not case (is...) type cases.)

Yes, this is a bit more verbose than a Java enum, but it's also slightly more flexible.

For a more practical example, let's see the definition of Boolean from the language module:

shared abstract class Boolean(String name) 
        of true | false 
        extends Case(name) {}
shared object false extends Boolean("false") {}
shared object true extends Boolean("true") {}

And here's how Comparable is defined. First, the typesafe enumeration Comparison:

doc "The result of a comparison between two
     Comparable objects."
shared abstract class Comparison(String name) 
        of larger | smaller | equal 
        extends Case(name) {}
doc "The receiving object is exactly equal 
     to the given object."
shared object equal extends Comparison("equal") {}
doc "The receiving object is smaller than 
     the given object."
shared object smaller extends Comparison("smaller") {}
doc "The receiving object is larger than 
     the given object."
shared object larger extends Comparison("larger") {}

Now, the Comparable interface itself:

shared interface Comparable<in Other> 
        satisfies Equality
        given Other satisfies Comparable<Other> {
    
    doc "The <=> operator."
    shared formal Comparison compare(Other other);
    
    doc "The > operator."
    shared Boolean largerThan(Other other) {
        return compare(other)==larger;
    }
    
    doc "The < operator."
    shared Boolean smallerThan(Other other) {
        return compare(other)==smaller;
    }
    
    doc "The >= operator."
    shared Boolean asLargeAs(Other other) {
        return compare(other)!=smaller;
    }
    
    doc "The <= operator."
    shared Boolean asSmallAs(Other other) {
        return compare(other)!=larger;
    }
    
}

Type inference

So far, we've always been explicitly specifying the type of every declaration. I think this generally makes code, especially example code, much easier to read and understand.

However, Ceylon does have the ability to infer the type of a locals or the return type of a local method. Just place the keyword local in place of the type declaration.

local hello = DefaultHello();
local operators = { "+", "-", "*", "/" };
local add(Natural x, Natural y) { return x+y; }

There are some restrictions applying to this feature. You can't use local:

  • for declarations annotated shared,
  • for declarations annotated formal,
  • when the value is specified later in the block of statements,
  • for methods with multiple return statements, or
  • to declare a parameter.

These restrictions mean that Ceylon's type inference rules are quite simple. Type inference is purely right-to-left and top-to-bottom. The type of any expression is already known without needing to look to any types declared to the left of the = specifier, or further down the block of statements.

  • The inferred type of a local declared local is just the type of the expression assigned to it using = or :=.
  • The inferred type of a method declared local is just the type of the returned expression.

Type inference for sequence enumeration expressions

What about sequence enumeration expressions like this:

local sequence  = { DefaultHello(), "Hello", 12.0 };

What type is inferred for sequence? You might answer: Sequence<X> where X is the common superclass or super-interface of all the element types. But that can't be right, since there might be more than one common supertype.

The answer is that the inferred type is Sequence<X> where X is the union of all the element expression types. In this case, the type is Sequence<DefaultHello|String|Float>. Now, this works out nicely, because Sequence<T> is covariant in T. So the following code is well typed:

local sequence  = { DefaultHello(), "Hello", 12.0 }; //type Sequence<DefaultHello|String|Float>
Object[] objects = sequence; //type Empty|Sequence<Object>

As is the following code:

local nums = { 12.0, 1, -3 }; //type Sequence<Float|Natural|Integer>
Number[] numbers = nums; //type Empty|Sequence<Number>

What about sequences that contain null? Well, do you remember the type of null from Part 1 was Nothing?

local sequence = { null, "Hello", "World" }; //type Sequence<Nothing|String>
String?[] strings = sequence; //type Empty|Sequence<Nothing|String>
String? s = sequence[0]; //type Nothing|Nothing|String which is just Nothing|String

It's interesting just how useful union types turn out to be. Even if you only very rarely explicitly write code with any explicit union type declaration (and that's probably a good idea), they are still there, under the covers, helping the compiler solve some hairy, otherwise-ambiguous, typing problems.

There's more...

A more advanced example of an algebraic datatype is shown here.

In Part 6 we'll explore Ceylon's generic type system in more depth.

21 comments:
 
28. Apr 2011, 23:31 CET | Link
Thorsten

Would you mind to discuss the collection hierarchy in one of the next parts? I'd be specially interested in the types and implementations of filter() and map() methods and how you handled the typing problems there which led to the introduction of higher-kinded types into Scala 2.8 (http://lampwww.epfl.ch/~odersky/papers/fsttcs09.html).

ReplyQuote
 
29. Apr 2011, 01:19 CET | Link

Yes, good idea. Honestly the work on the collections framework is not totally done. You can reasonably have map() and filter() without any sort of higher kinds, but higher kinds can certainly make it a bit slicker. What I've been thinking of, however, is to have a kind of limited support for higher kinds, where only methods (and not types) can accept a parametrized type parameter. This would be more than enough to make map() and filter() work.

 
29. Apr 2011, 08:45 CET | Link
Thorsten
Honestly the work on the collections framework is not totally done. You can reasonably have map() and filter() without any sort of higher kinds, but higher kinds can certainly make it a bit slicker.

Ok, I'll wait then until it is in a shape you'd like to share :-)

What I've been thinking of, however, is to have a kind of limited support for higher kinds, where only methods (and not types) can accept a parametrized type parameter.

Wouldn't this be a deviation from the nice regularity of Ceylon? So far you have made classes and methods more similar in many respects. Having just methods accept higher-kinded types would break this.

Note: IMHO map() should not in general answer a result of the same collection type as the receiver: this is typically wrong for Sets and for sorted collections!

 
29. Apr 2011, 15:52 CET | Link
Thorsten
You can reasonably have map() and filter() without any sort of higher kinds, but higher kinds can certainly make it a bit slicker.

So it might look somehow like the following? (I just had to try out coding it :-)

When trying it out I stumbled upon two problems with having just a single constructor (see the comments in the code). Am I missing something?

shared interface Iterable<out E>
	satisfies Producer<E>
{
	shared formal void foreach(void doWith(E element));

	shared Result filter<Result>(Bool predicate(E element))
		given Result(E... elements) satisfies Consumer<E>
		// problem(?): ctor args not needed here but there is only one ctor per class
		// so it must be the expected one for collections
	{
		Result result = Result();
		foreach {
			void doWith(E element) {
				if (predicate(element)) result.add(element);
			}
		}
		return result;
	}

	shared Result map<Result>(U f(E element))
		given Result(U... elements) satisfies Consumer<U>
		// here I'd like to use another ctor which takes the size of the collection to be created
		// (so no reallocations will be necessary when adding the mapped elements)
	{
		return mapTo(f, Result());
	}

	shared Result mapTo<Result>(U f(E element), Result result)
		given Result satisfies Consumer<U>
	{
		foreach {
			void doWith(E element) {
				result.add(f(element));			
			}
		}
		return result;
	}
}

shared interface List<E> satisfies Iterable <E> {}

shared class Array<E>(E... elements) Iterable List<E> { ... }

Array<Integer> ints = { 1, 2, 3 };
Array<String> strings = ints.map(toString);
 
29. Apr 2011, 16:14 CET | Link

Without parameterized type parameters, you need to override map() and filter() on List to return a List, on String to return a String, on Map to return a Map, etc. Sure, it's annoying, but I think it's actually more-or-less OK. There's a limited number of direct subtypes of Iterable. And if you fail to override? Well, no major problem, you at least get back an Iterable.

I think it only really starts to become a problem if you think that ArrayList.map() should return an ArrayList, and LinkedList.filter() should return a LinkedList, etc, which would be nice, certainly. Then you're definitely going to need parameterized type parameters. But I personally think that code using the collections module should always refer to the more abstract types like List or SortedList, not concrete types like ArrayList and LinkedList. So the collections module is only a fairly weak usecase for this.

Anyway, I'm not completely ruling out support for parameterized type parameters (especially for methods) in a future version of the language. But for the first version, we're going to live without it, and live with the workaround.

 
29. Apr 2011, 16:26 CET | Link
What I've been thinking of, however, is to have a kind of limited support for higher kinds, where only methods (and not types) can accept a parametrized type parameter. Wouldn't this be a deviation from the nice regularity of Ceylon? So far you have made classes and methods more similar in many respects. Having just methods accept higher-kinded types would break this.

Definitely, a huge deviation from regularity, which is why I'm holding off having higher-kinded types at all.

Truth is, we may eventually decide to grow support for higher-kinded polymorphism, but that's a decision I don't think we should take at all lightly, and definitely not one we should take quickly.

I mean, do you want to be the one to explain to new developers the difference between Foo<List> and Bar<List<String>>? That the type argument to Foo is not actually a type at all, but rather a function from types to types? Shit, most people have enough trouble understanding covariance the first time they see it.

Note: IMHO map() should not in general answer a result of the same collection type as the receiver: this is typically wrong for Sets and for sorted collections!

With the overloading approach Set.map() can return any container type it likes (as long as it's Iterable<Entry<U,V>>).

 
29. Apr 2011, 16:31 CET | Link
I mean, do you want to be the one to explain to new developers the difference between...

By the way, I'm not saying that as some kind of elitist oh I can understand the concept, but everyone else is too stupid to. What I mean is that, for people trying to just get work done, it's hard to see how the mental effort needed to understand higher kinds is really worth it. And no, I don't buy arguments like oh but only library developers need to understand this stuff. I think languages should not have major features of the type system that aren't intended for use by everyone who writes code in the language.

 
03. May 2011, 11:11 CET | Link
Aliaksei Lahachou | aliaksei.lahachou(AT)gmail.com

Type inference is a neat feature, I absolutely loved it in C#. However, I fail to understand, why I can't use it in methods with multiple return statements. Could you explain it, please?

 
03. May 2011, 14:34 CET | Link
Aliaksei Lahachou wrote on May 03, 2011 05:11:
Type inference is a neat feature, I absolutely loved it in C#. However, I fail to understand, why I can't use it in methods with multiple return statements. Could you explain it, please?

You're the second person to ask me that in the last couple of days.

No really strong reason other than that I'm trying to keep it super-simple for now. But you're right, the type of a method with multiple returns could be inferred to be the union of the returned types.

 
21. May 2011, 14:57 CET | Link

Out of curiosity, is there a technical reason why enumerations need to extend Case(name)? Is it only to distinguish between the cases where objects and where classes can be used in case statements?

Adam

 
21. May 2011, 15:01 CET | Link

And as for the map/filter operations, aren't type parameters where you can specify constructor signature enough to implement them in a generic way? Individual implementations would just need to extend the abstract classes giving their constructor as a type argument?

Adam

 
21. May 2011, 19:46 CET | Link
Adam Warski wrote on May 21, 2011 08:57:
Out of curiosity, is there a technical reason why enumerations need to extend Case(name)? Is it only to distinguish between the cases where objects and where classes can be used in case statements?

The only real reason for Case is to prevent you from refining equals() in a pathological way that breaks the compile-time guarantee of exhaustiveness (for example, an equals() that always returns false). The thing is that case(foo) is an equality test. By having the compiler force you to extend Case, we prevent your pathological custom equals() methods.

 
21. May 2011, 20:13 CET | Link
Adam Warski wrote on May 21, 2011 09:01:
And as for the map/filter operations, aren't type parameters where you can specify constructor signature enough to implement them in a generic way? Individual implementations would just need to extend the abstract classes giving their constructor as a type argument?

Some people think it's important that the return type of map() is the exact same kind of container class as the argument. So, according to this view, the following is not an acceptable signature for map():

Y[] map<X,Y>(X[] xs, Y select(X x)) { .... }

If we pass a subtype of X[] to this function, such as List<X>, let's say, we get back a Y[], not an List<Y>.

List<Person> people = .... ;
String[] names = map (people) select (Person p) (p.name);

(I'm using the smalltalk-style invocation protocol here, which we have not yet covered.)

This is what the signature of map() should be, according to this view:

C<Y> map<C,X,Y>(C<X> xs, Y select(X x)) given C<E>(E... xs) { .... }

(Note that C here is not a type parameter, it's a type constructor parameter. It accepts a type constructor, not a type. A type constructor is a function that maps types to types, like Set or Sequence. This language feature is called higher kinded polymorphism or type constructor parameterization.)

Then in theory I get back the same container type I passed in:

ArrayList<Person> people = .... ;
ArrayList<String> names = map (people) select (Person p) (p.name);

I'm not totally convinced of this, on several levels, but I think it depends a lot on really the whole overall design of your collections framework.

  • For one thing, I don't think it's correct that map(set, function) should be a Set.
  • For another, it seems to encourage you to use concrete collection types like ArrayList instead of more abstract types like List and Sequence.

But it's a very interesting topic and I'm sure my views will evolve once I really sit down and do some serious design work on the collections framework. (Which will probably look very different to Java collections.)

 
24. May 2011, 19:52 CET | Link
Gavin King wrote on May 21, 2011 14:13:
This is what the signature of map() should be, according to this view:
C<Y> map<C,X,Y>(C<X> xs, Y select(X x)) given C<E>(E... xs) { .... }
(Note that C here is not a type parameter, it's a type constructor parameter. It accepts a type constructor, not a type. A type constructor is a function that maps types to types, like Set or Sequence. This language feature is called higher kinded polymorphism or type constructor parameterization.)

Ah right, even with type parameters where you can specify the constructor you need the higher kind.

Gavin King wrote on May 21, 2011 14:13:
I'm not totally convinced of this, on several levels, but I think it depends a lot on really the whole overall design of your collections framework.
  • For one thing, I don't think it's correct that map(set, function) should be a Set.
  • For another, it seems to encourage you to use concrete collection types like ArrayList instead of more abstract types like List and Sequence.
But it's a very interesting topic and I'm sure my views will evolve once I really sit down and do some serious design work on the collections framework. (Which will probably look very different to Java collections.)

Well, I guess if I filter a Set, I'd want a Set back, and if I filter a List, I'd want a List back. Maybe the concrete impl is not so important (it's usually HashSet and ArrayList anyway ;) ), but still. Maybe the collections can simply have a method to create a new empty collection of the same type (to which then the new elements can be added), or a builder of some kind?

Adam

 
24. May 2011, 20:26 CET | Link
Well, I guess if I filter a Set, I'd want a Set back, and if I filter a List, I'd want a List back.

Yeah but if you map a Set you might prefer to get a List back. And remember there's a lot you can do with covariant method return types.

Maybe the collections can simply have a method to create a new empty collection of the same type (to which then the new elements can be added), or a builder of some kind?

Yes, that's a really good possibility. What I've been thinking of recently is to have everything just return a Sequence, or perhaps, as you say, some kind of Builder, and let the user call a method to transform it to the kind of collection they want. Sometimes there's nothing wrong with being explicit.

 
20. Aug 2011, 06:22 CET | Link


shared interface Comparable <in Other>
        satisfies Equality
        given T satisfies Comparable<Other> {
     
    doc "The <=> operator."
    shared formal Comparison compare(Other other);
     
    doc "The > operator."
    shared Boolean largerThan(Other other) {
        return compare(other)==larger;
    }
     
    doc "The < operator."
    shared Boolean smallerThan(Other other) {
        return compare(other)==smaller;
    }
     
    doc "The >= operator."
    shared Boolean asLargeAs(Other other) {
        return compare(other)!=smaller;
    }
     
    doc "The <= operator."
    shared Boolean asSmallAs(Other other) {
        return compare(other)!=larger;
    }
     
}

given T should be Othet

 
31. Dec 2013, 10:59 CET | Link
Why did you chose this Yoda style?
if (is Class obj)
isnt following better?
if (obj is Class)
if (obj isnt Empty)
and even
if (obj is)
for null check. for consistency
 
08. Sep 2014, 07:41 CET | Link
Vedat

Be sure to exfoliate your face on a regular basis. Exfoliate your dry or sensitive skin one, two or even three times weekly; less than that will cause you to lose the chance to expose flourishing skin under your top layer of skin. Exfoliating will let your face look more fresh and radiant, and will help to prevent oil and dirt buildup. penis advantage review

 
08. Sep 2014, 11:04 CET | Link
should

When selecting a brush, you should choose one that is made from natural animal hairs, as opposed to one made from synthetic materials. The bristles on the natural brushes will be more soft and flexible, so it will be less likely to cause any damage to your hair, if you use it regularly. revitol hair removal cream

 
08. Sep 2014, 11:40 CET | Link
protein

Calculate your consumption of protein daily. You need to consume about one gram of protein for every pound of somanabolic muscle maximizer weight each day. Consuming the right amount of protein will increase the muscle growth you get from the weight training that you are doing. Varying the consumption by a little here and there is not going to make much of a difference, but you should strive for the same amount daily.

 
18. Sep 2014, 13:10 CET | Link
schedule

Do a practice run-through of your schedule prior to your first day of class. Determine how long it takes you to travel from one spot to the next, and map a route. Also indicate other important places on your map.

Stick to your studies. College can be a fun time, with lots of new experiences, but it's crucial to remember why you're there. Take the time to ask teachers and other students for help, and make sure you get your homework done when you need to so that your tao of badass pdf grades stay strong.

Post Comment