Help

One of the best features of Ceylon is lexically-scoped introduction, which we discussed here, calling it decoration.

I've recently come to the conclusion that the explicit decorate statement is a barrier to modularity, and decided upon a slightly different approach. In this approach, an interface may declare that it adapts another type:

doc "Adaptor that introduces List to Sequence."
see (List,Sequence)
shared interface SequenceList<T> 
        adapts Sequence<T>
        satisfies List<T> {
    
    shared actual default List<T> sortedElements() {
    	//define the operation of List in
    	//terms of operations on Sequence
        return asList(sortSequence(this));
    }
    
    ...
    
}

Then the interface is called an introduction - or, alternatively, an adapter, in a nod to the terminology used in the Go4 book. (In that book, a decorator is a slightly different concept.) According to the spec:

The interface may not:
  • declare or inherit a member that refines a member of any adapted type, or
  • declare or inherit a formal or non-default actual member unless the member is inherited from an adapted type.

Now, to enable the introduction in a certain compilation unit, all you need to do is import it.

import ceylon.collection { SequenceList }    //import the adapter

String[] cities = { "Melbourne", "Atlanta", "San Francisco", "Guanajuato", "Paris" };
List<String> sortedCities = cities.sortedElements();    //call the adapter

Again, according to the spec:

If, in a certain compilation unit, multiple introductions of a certain adapted type declare or inherit a member that refines a common member of a common supertype then either:
  • there must be a unique member from the set of members, called the most refined member, that refines all the other members, or
  • the adapted type must declare or inherit a member that refines all the members.
At runtime, an operation (method invocation, member class instantiation, or attribute evaluation) upon any type that is a subtype of all the adapted types is dispatched according to the following rule:
  • If the runtime type of the instance of the adapted type declares or inherits a member defining the operation, the operation is dispatched to the runtime type of the instance.
  • Otherwise, the operation is dispatched to the introduction that has the most-refined member defining the operation.

I think this is a significantly better approach, making introduction much easier to use.

UPDATE: A commenter asks if Ceylon will support C#-style extension methods. No need. Every concrete method of an adapter is an extension method! Indeed, an adapter without a satisfies clause is just a package of extension methods and attributes.

12 comments:
 
25. May 2011, 00:25 CET | Link

Just a quick question, am I right to assume that if you only want to extend a Sequence with a sortedElements() option (that still returns a List) you don't need the satisfies List part?

That the satisfies List is only used if you want, for example, to pass a Sequence as a parameter to a function that expects a List?

ReplyQuote
 
25. May 2011, 00:28 CET | Link
Quintesse wrote on May 24, 2011 18:25:
Just a quick question, am I right to assume that if you only want to extend a Sequence with a sortedElements() option (that still returns a List) you don't need the satisfies List part? That the satisfies List is only used if you want, for example, to pass a Sequence as a parameter to a function that expects a List?

Exactly.

 
25. May 2011, 00:53 CET | Link

Just wondering three things:

1. can I still use this adapter explicitly if I want to? Let's say like:

List<String> sortedCities = SequenceList(cities).sortedElements();

which of course is not too logical for an interface, but it brings me to the next thing:

2. knowing that an adapter will always need some code to work, which is probably very specific and not very re-usable, why is the adapter an interface and not a class?

also because of my next point:

3. is it possible to use an existing adapter implementation somehow? Maybe you've already got a SequenceList object that wraps a Sequence and now decide that you want to somehow re-use this class. Can I write an adapter (or any interface for that matter) whose implementation for actual default methods come from another class?

 
25. May 2011, 01:50 CET | Link
  1. Well, you can't instantiate it like that, but you can explicitly mix it into a class or object. If you do so, you have to make sure that the class or object is a subtype of the adapted type.
  2. Because introductions, like interfaces, are stateless. They don't have simple attributes or initialization logic.
  3. Not precisely sure what you mean. An adaptor (or any other interface) can't extend a class. It can extend some other interface.
 
25. May 2011, 08:05 CET | Link

2. So an adapter/introduction can never contain state? But wouldn't that reduce its usefulness? What if the conversion from one to another cannot be simply reduced calling interface's A methods in terms of interfaces B's methods? (a translations so to speak) What if some temporary object is needed between calls? (a cache for example because the conversion is costly)

I could write a wrapper object which could trivially take a List and implement a Sequence, why can't I mark it as a adapter/introduction and have it inserted automatically in the right places. It could be that a class like that needs an initializer of the right type but for the rest the same rules would apply as with the interface.

Seems like a much more usefull solution.

 
25. May 2011, 08:22 CET | Link
So an adapter/introduction can never contain state?

Correct. It's an interface.

But wouldn't that reduce its usefulness?

It's a very intentionally designed-in limitation. We want the compiler to be able to very freely instantiate and discard the adapter objects that implement the introduction, without that ever being visible to the application. As soon as an adapter can contain state or initialization logic, the compilers supposedly under-the-covers objects would get visible to the application. That would be Bad.

What if the conversion from one to another cannot be simply reduced calling interface's A methods in terms of interfaces B's methods?

The facility is designed to not be usable for implicit type conversions. After much discussion we came to the conclusion that implicit type conversions are just too potentially harmful, however convenient they can be.

I could write a wrapper object which could trivially take a List and implement a Sequence, why can't I mark it as a adapter/introduction and have it "inserted" automatically in the right places.

Because then the compiler's supposedly transparent "insertion" could leak out and start affecting the behavior of your application.

 
25. May 2011, 20:11 CET | Link

Would you be willing to explain what would be the Bad thing about implicit type conversions? At this moment I see very little difference (except for the fact that the generated adapter objects are obviosuly under complete control of the compiler), at least not enough to understand why this limitation is necessary.

 
26. May 2011, 08:47 CET | Link
Aliaksei Lahachou

Is there a chance to see extension methods from C#?

 
26. May 2011, 09:04 CET | Link
Would you be willing to explain what would be the Bad thing about implicit type conversions?

So by implicit type conversions, I'm imagining a facility where you can write a method (or constructor) that takes a type X and produces a second type Y, with relatively few constraints upon X and Y, and have the compiler automatically insert a call to that method whenever you try to do something Y-ish with an X.

So the first problem is that this converter method (or constructor) is arbitrary procedural code that can potentially have side-effects or make use of temporal state. There's no real way for the compiler to prevent this in a non-pure language like ... well ... basically every language I know of except Haskell. This means that the behavior of the program can depend upon how and where the compiler decides to insert these magical invisible calls to the converter method.

The second problem is that when you design a type system you go to all kinds of crazy lengths building in restrictions to ensure that an expression containing an operation upon a value has a single, clear, unambiguous meaning. For example, Java - and most other Java-like languages - will prevent you from implementing the same interface twice with different type arguments. A class can't be both Iterable<String> and Iterable<Foo> In Ceylon, we go as far as not even allowing overloading, since our type system has features like type inference and generics that can introduce ambiguities when you allow overloading.

Implicit conversions are a total end run around all these restrictions (which is why they are so potentially powerful). And so they reintroduce all the potential for ambiguity that we worked so hard to get rid of. If I have an implicit conversion from Iterable<Foo> to Iterable<String>, the return type of iterator() is now ambiguous. OK, so you can probably think of a way to resolve that particular ambiguity, Now imagine that we have multiple implicit conversions for the same type!

Another thing you strive for in a type system is transitivity of the assignability relationship. If X is assignable to Y and Y is assignable to Z, then you expect X to be assignable to Z. So does that mean we need to be able to handle chains of implicit type conversions? This introduces yet more ambiguity if there are two potential paths of converters between X and Z. Now, Scala, for example, resolves this problem by saying that no, implicit converters aren't chained. So therefore assignability is not transitive in Scala. Yew!

Now let's think about covariance. If I define a class Collection<out T>, the idea is that Collection<X> is assignable to Collection<Y> whenever X is assignable to Y. I don't see how you can preserve that relationship if you have implicit type conversions.

Implicit type conversions are very powerful, and let you do lots of things that seem near impossible without them. The reason they are so powerful is that they break so many carefully-engineered-in properties of the type system. So I worked really, really hard to not need them in Ceylon. Instead, adapters give you some of the power of implicit type conversions, but in a much safer, more disciplined, more elegant way. I think this is a key feature of the language.

 
26. May 2011, 09:07 CET | Link
Aliaksei Lahachou wrote on May 26, 2011 02:47:
Is there a chance to see extension methods from C#?

Adapters can do everything that extension methods can do, and much, much more.

shared interface ExtensionMethods
        adapts SomeType {
    shared Integer extensionMethod1() { .... }
    shared void extensionMethod2(String s) { ... }
}
 
27. May 2011, 19:52 CET | Link
I think this is a key feature of the language

Ok, thanks for your explanation :)

 
29. May 2011, 04:05 CET | Link
Quintesse wrote on May 27, 2011 13:52:
I think this is a key feature of the language Ok, thanks for your explanation :)

Oh and I forgot another problem with implicit type conversions: they tend to break the == identity operator. This might not be a problem in a functional language with referential transparency, but I think it is somewhat of a problem in languages with mutability and object identity.

Post Comment