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: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:
- 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.
- 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.
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 part?
That the is only used if you want, for example, to pass a Sequence as a parameter to a function that expects a List?
Exactly.
Just wondering three things:
1. can I still use this adapter explicitly if I want to? Let's say like:
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 methods come from another class?
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 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.
Correct. It's an interface.
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.
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.
Because then the compiler's supposedly transparent "insertion" could leak out and start affecting the behavior of your application.
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.
Is there a chance to see extension methods from C#?
So by , 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 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.
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) { ... } }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.