Extend or satisfy class / interface, having contravariant type parameter, bound to Class<>

59 views
Skip to first unread message

Voitech

unread,
Jul 27, 2018, 12:05:13 PM7/27/18
to ceylon-users
Lets define an interface:
shared interface Smth<in Klass> given Klass satisfies Class<>{
    shared formal
void smth(Klass klass);
   
}

It is perfectly legal from point of compiler.

Lets now define an object to test if this is possible to satisfy such interface

shared object smth satisfies Smth<Class<String>>{
    shared actual
void smth(Class<String,Nothing> klass) {}
}

It is fine also. Now when try to satisfy this interface `Smth` by an class `SmthImpl` :

shared class SmthImpl() satisfies Smth<Class<String>>{
    shared actual
void smth(Class<String,Nothing> klass) {}
}

I get an error:

type with contravariant type parameter 'Class' appears in contravariant or invariant location in supertype: 'Smth<Class<String,Nothing>>'
shared
class SmthImpl() satisfies Smth<Class<String>>

Why is that ? How can I overcame this behavior, to be able to pass `Class<String>` to type parameter `Klass`?

John Vasileff

unread,
Jul 27, 2018, 8:16:29 PM7/27/18
to ceylon...@googlegroups.com
I think the error is correct, although perhaps Gavin can weigh in if I have this wrong. In fact, it seems there should also be an error for the `object` definition.

A reduced example:

interface Consumer<in T> {}
interface I<T> {}
interface Disallowed satisfies I<Consumer<String>> {} // error
// type with contravariant type parameter 'Consumer' appears in contravariant
// or invariant location in supertype: 'I<Consumer<String>>'

Or even:

interface Consumer<in T> {}
class Disallowed() satisfies Consumer<Consumer<Nothing>> {} // error
// type with contravariant type parameter 'Consumer' appears in contravariant
// or invariant location in supertype: 'Consumer<Consumer<Nothing>>'

These violate my reading of the section 3.5.2 in the spec (https://ceylon-lang.org/documentation/1.3/spec/html_single/#variancevalidation):

Furthermore, a type with a contravariant type parameter may only appear in a covariant position in an extended type, satisfied type, case type, or upper bound type constraint.

Note: this restriction exists to eliminate certain undecidable cases described in the paper Taming Wildcards in Java's Type System, by Tate et al.


HTH

John


--
You received this message because you are subscribed to the Google Groups "ceylon-users" group.
To unsubscribe from this group and stop receiving emails from it, send an email to ceylon-users...@googlegroups.com.
To post to this group, send email to ceylon...@googlegroups.com.
Visit this group at https://groups.google.com/group/ceylon-users.
To view this discussion on the web visit https://groups.google.com/d/msgid/ceylon-users/4e52dcb7-2638-4d18-8ee5-6e6e25388b43%40googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Voitech

unread,
Jul 30, 2018, 5:31:48 AM7/30/18
to ceylon-users
Thanks John for Your answer, so the error is according to docs. I still need to constrain method parameter with Type<>, to be able to distinct: is It ClassOrInterface? Maybe Interface or Class or ClassModel ? Or Union?  It is vital for my lib's API or it will not be type safe. So the use case...

I work on library for converting one type to another, with runtime selection on how to do that. So let say You have few model.

shared serializable class Model{
   shared
String id;
   shared
Boolean isSomething;
   shared
Integer someNumber;

}

Another one

shared serializable class Other{
   shared 
Integer id;
   shared 
String isSomething;
   shared 
Float someNumber;

}

Let's create an object
Model modelInstance = Model("123",false,123);

We can call 

assert(is Other other=api.convert(modelInstance,`Other`));

And it already works

To achive this You need a Converter which will do the convertion for You. 

Declaration:



shared
interface TypedConverter<in Source=Nothing ,in ResultType=Nothing,out Result=Anything> satisfies Component given ResultType satisfies Type<Result> {

     
throws(`class ConvertionException`)
     shared formal
Result convert(Context context,Source source,ResultType resultType);

 

    shared interface Matcher {

        shared formal
Boolean match(Source source,ResultType resultType);
     } 

 

 shared
default Matcher? matcher=>null;

 

}


An implementation: 
shared class MetaConverter() satisfies TypedConverter<Object,ClassOrInterface<Object>,Object> {

    shared actual
Object convert(Context context, Object source, ClassOrInterface<Object> resultType) {
    value resolvedType
= context.resolve(resultType);
    value description
= context.describe(source, resolvedType);
    value accumulator
=context.create(description.accumulatorClass, null);
         for(value part in description.parts){ 
        value sourcePart = part.obtainable.obtain;
        value destPart=context.convert(sourcePart, part.targetable.type);
        
if(exists error=accumulator.accumulate(part.targetable, destPart)){
            
throw ConvertionException(source, resultType,error);
                 } 
    }

   
return context.create(resolvedType, accumulator);

 
}

    matcher
=> object satisfies MetaConverter.Matcher{
        shared actual
Boolean match(Object source, ClassOrInterface<Object> resultType) =>true;
      }; 


}

As can be seen I use Context calls to delegate some responsibilities. There are other support components for generic convertion, like Resolver, which provides specific Class<Kind> for abstract classes, interfaces or intersection types. One of such is SelfResolver it is used,  when other components requests resolvance, of unknown (mostly Type<Anything>) to some specific Class<Other>. It may be so, that this provided type is already a Class<Other> but it is not the job to check that by calling component (in this case MetaConverter) but it does delegated to Resolver to ensure that. It's parent's type definition looks like this: 

shared interface TypedResolver<out Base=Anything,out Output=Anything, in Input=Nothing> satisfies Component given Input satisfies Type<Base> {

 shared formal
Class<Output> resolve(Input type);

 shared
interface Matcher {

     shared formal
Boolean match(Input inputType);
 }

 shared formal
Matcher? matcher;

}


implementation looks like this:

shared class SelfResolver() satisfies TypedResolver<Anything,Anything,ClassOrInterface<Anything>>{
 shared actual
Class<Anything,Nothing> resolve(ClassOrInterface<Anything> type){
     
assert(is Class<Anything> type);
     
return type;
 
}

 matcher
=> object satisfies SelfResolver.Matcher{
     shared actual
Boolean match(ClassOrInterface<Anything> inputType) => inputType is Class<Anything>;

 };

}

Because I need to store components somehow, I need to be able to type something like TypedResolver<>,so none of 3 type parameters, can be declared as invariant, as invariant type parameters don't have super types like Nothing/Anything,  when in runtime there will be request to resolve type from other component (MetaConveter in this case), which is already a Class<>. I use Matcher for that (Iteration over registered Resolvers and calling match on every of them, not very optimal but this is not the case). The third type parameter in TypedResolver<Anything,Anything,ClassOrInterface<Anything>>, (in Input=Nothing), declaration is telling that method parameter inputType in resolve and match must be bound by this type parameter. As You can see I do assert in resolve method, and a check in match method. It is type safety flaw. I shouldn't be forced by type system to do so, but I can't constrain third parameter of TypeResolver with Class<>, as it gives me this error "Type with contravariant type parameter Class appears in contravariant or invariant location in supertype: TypedResolver<Anything,Anything,Class<Anything,Nothing>>"


How can I workaround this ? 

John Vasileff

unread,
Jul 30, 2018, 1:26:47 PM7/30/18
to ceylon...@googlegroups.com
It would take a while for me to read through your use case, but would it work to hide the contravariant type parameter, as in:

import ceylon.language.meta.model { Class }

shared class ClassHolder<out Type>(shared Class<Type, Nothing> c) {}

shared interface Smth<in Klass> given Klass satisfies ClassHolder<Anything> {
shared formal void smth(Klass klass);
}

shared class SmthImpl() satisfies Smth<ClassHolder<String>>{
shared actual void smth(ClassHolder<String> klass) {}
}

ClassHolder could also have an invariant type parameter for the Arguments, but I’m not sure that would help.

John

On Jul 30, 2018, at 5:31 AM, Voitech <wojciech...@gmail.com> wrote:

Thanks John for Your answer, so the error is according to docs. I still need to constrain method parameter with Type<>, to be able to distinct: is It ClassOrInterface? Maybe Interface or Class or ClassModel ? Or Union?  It is vital for my lib's API or it will not be type safe. So the use case...

I work on library for converting one type to another, with runtime selection on how to do that. So let say You have few model.

shared serializable class Model{
   shared 
String id;
   shared 
Boolean isSomething;
   shared 
Integer someNumber;

}

Another one

shared serializable class Other{
   shared 
Integer id;
   shared 
String isSomething;
   shared 
Float someNumber;

}

Let's create an object
Model modelInstance = Model("123",false,123);

We can call 

assert(is Other other=api.convert(modelInstance,`Other`));

And it already works

To achive this You need a Converter which will do the convertion for You. 

Declaration:



shared 
interface TypedConverter<in Source=Nothing ,in ResultType=Nothing,outResult=Anything> satisfies Component given ResultType satisfies Type<Result> {


     
throws(`class ConvertionException`) 
     shared formal 
Result convert(Context context,Source source,ResultTyperesultType); 

 

    shared interface Matcher { 

        shared formal 
Boolean match(Source source,ResultType resultType); 
     } 

 

 shared 
default Matcher? matcher=>null; 

 

}


An implementation: 
shared class MetaConverter() satisfies TypedConverter<Object,ClassOrInterface<Object>,Object> {
 

    shared actual 
Object convert(Context context, Object source,ClassOrInterface<Object> resultType) { 
    value resolvedType 
= context.resolve(resultType); 
    value description 
= context.describe(source, resolvedType); 
    value accumulator 
=context.create(description.accumulatorClass, null); 
         for(value part in description.parts){ 
        value sourcePart = part.obtainable.obtain;
        value destPart=context.convert(sourcePart, part.targetable.type); 
        
if(exists error=accumulator.accumulate(part.targetable, destPart)){ 
            
throw ConvertionException(source, resultType,error);
                 } 
    } 

    
return context.create(resolvedType, accumulator); 

 
} 

    matcher 
=> object satisfies MetaConverter.Matcher{ 
        shared actual 
Boolean match(Object source, ClassOrInterface<Object>resultType) =>true; 
      }; 


}

As can be seen I use Context calls to delegate some responsibilities. There are other support components for generic convertion, like Resolver, which provides specific Class<Kind> for abstract classes, interfaces or intersection types. One of such is SelfResolver it is used,  when other components requests resolvance, of unknown (mostly Type<Anything>) to some specific Class<Other>. It may be so, that this provided type is already a Class<Other> but it is not the job to check that by calling component (in this case MetaConverter) but it does delegated to Resolver to ensure that. It's parent's type definition looks like this: 

shared interface TypedResolver<out Base=Anything,out Output=Anything, inInput=Nothing> satisfies Component given Input satisfies Type<Base> { 

 shared formal 
Class<Output> resolve(Input type); 

 shared 
interface Matcher { 

     shared formal 
Boolean match(Input inputType); 
 } 

 shared formal 
Matcher? matcher; 

}


implementation looks like this:

shared class SelfResolver() satisfies TypedResolver<Anything,Anything,ClassOrInterface<Anything>>{ 
 shared actual 
Class<Anything,Nothing> resolve(ClassOrInterface<Anything>type){ 
     
assert(is Class<Anything> type); 
     
return type; 
 
} 

 matcher
=> object satisfies SelfResolver.Matcher{ 
     shared actual 
Boolean match(ClassOrInterface<Anything> inputType) =>inputType is Class<Anything>; 

 }; 

}

Because I need to store components somehow, I need to be able to type something like TypedResolver<>,so none of 3 type parameters, can be declared as invariant, as invariant type parameters don't have super types like Nothing/Anything,  when in runtime there will be request to resolve type from other component (MetaConveter in this case), which is already a Class<>. I use Matcher for that (Iteration over registered Resolvers and calling match on every of them, not very optimal but this is not the case). The third type parameter in TypedResolver<Anything,Anything,ClassOrInterface<Anything>>, (in Input=Nothing), declaration is telling that method parameter inputType in resolve and matchmust be bound by this type parameter. As You can see I do assert in resolve method, and a check in match method. It is type safety flaw. I shouldn't be forced by type system to do so, but I can't constrain third parameter of TypeResolver with Class<>, as it gives me this error "Type with contravariant type parameter Class appears in contravariant or invariant location in supertype: TypedResolver<Anything,Anything,Class<Anything,Nothing>>"

Voitech

unread,
Jul 30, 2018, 1:54:48 PM7/30/18
to ceylon-users
Thank You, John for all Your help. I will take a look on this holder workaround.

Ross Tate

unread,
Aug 8, 2018, 11:11:15 AM8/8/18
to ceylon...@googlegroups.com
Cool to see this conversation! As a related note, our more recent research found a different restriction that also ensures decidable subtyping plus a number of other useful properties. It also allows the pattern Voitech is hoping to employ here. At the time we did our research, I remember Gavin checked and found that the newer restriction was compatible with existing Ceylon code. I think switching to the newer restriction was deferred until things had settled and y'all had a larger code base to check for compatibility. So maybe now is the time to switch?

On Mon, Jul 30, 2018 at 1:54 PM, Voitech <wojciech...@gmail.com> wrote:
Thank You, John for all Your help. I will take a look on this holder workaround.
--
You received this message because you are subscribed to the Google Groups "ceylon-users" group.
To unsubscribe from this group and stop receiving emails from it, send an email to ceylon-users+unsubscribe@googlegroups.com.

To post to this group, send email to ceylon...@googlegroups.com.
Visit this group at https://groups.google.com/group/ceylon-users.
Reply all
Reply to author
Forward
0 new messages