eclipse-archived/ceylon

Metamodel, typeLiteral<Kind>(), type(kind) provides different results

Closed this issue · 5 comments

When executing metamodel invocation on touple type '[String,Integer]' versus type(["abc",3]) although both provides at runtime ClassImpl of Touple the parameterizations of those differs.
Assertion Failure example :

shared test void shouldEqualTupleTypes(){
		Class<[String, Integer],[String, [Integer]]> clazzMetaModel = `[String,Integer]`;
		[String,Integer] val=["abc",3];
		ClassModel<[String, Integer],[Nothing]> classType = type(val);
		assert(clazzMetaModel==classType);
		assert(clazzMetaModel.hash==classType.hash);
	}

Other assertion failure example with parametrized function:

shared void fun<Args>(Args args){
		value classModel = type(args);
		value literal=typeLiteral<Args>();
		assert(classModel==literal);
	}

shared test void shouldCallFunPositive(){
		fun(["abc",2]);
	}

As far as I know, there is no other way to introspect, type of provided value differently than using type(value); expression in runtime. So runtime provided type, would differ from statically typed.
Is it intensional ?

I'm not sure the behavior for equals is documented (I thought there was an issue about that).

But at least exactly and subtypeOf should work, or so it would seem, in the slightly modified example:

import ceylon.language.meta { ... }
import ceylon.language.meta.model { ... }

shared void run() {
    Class<[String, Integer],[String, [Integer]]> clazzMetaModel = `[String,Integer]`;
    ClassModel<[String, Integer],[Nothing]> classType = type(["abc",3]);

    print(clazzMetaModel); // has ceylon.language::Empty
    print(classType);      // has ceylon.language::empty (lowercase empty)

    assert (clazzMetaModel.supertypeOf(classType)); // ok
    assert (clazzMetaModel.subtypeOf(classType)); // fails
    assert (clazzMetaModel.exactly(classType)); // fails
}

The reason for the failed assertions is that type(["abc",3]) has \Iempty for the Rest type, whereas `[String,Integer]` has Empty. \Iempty is the singleton value, which is a subtype of the sealed interface Empty.

So everything here is technically correct, but I suppose it is surprising behavior and could be considered a bug.

Since singleton types aren't often denoted in Ceylon, I think the change would be to have ["abc",3] use the less precise Empty. @gavinking would have to weigh in.

(Edit fixed code in final paragraph)

To add a bit of context, what It is causing in my case. I have static parametrized types, which are registered and stored in HashMap by using it's type parameters.

example:

abstract class AType<out Result,in Input>() {
	shared formal Result doSmth(Input input);

	
	shared Hasher hasher=Hasher(typeLiteral<Result>(),typeLiteral<Input>());
	
			
}
shared final class Hasher(Object* toHash) {
	
	hash =toHash.fold(0)((Integer initial, Object element) => 32*initial+element.hash);
	
	string ="Hashed: ``toHash``, value: ``hash.string``";
	
	
	shared actual Boolean equals(Object that) {
		if (is Hasher that) {
			return this.hash==that.hash;
		}
		else {
			return false;
		}
	}
}

Then I construct such object

object aTypeInstance extends AType<Boolean,Integer>(){
	shared actual Boolean doSmth(Integer input) => input>=0;
		
}

I put it in HashMap<Hasher,AType<Anything,Nothing>>

value map =HashMap<Hasher,AType<Anything,Nothing>>();
map.put(aTypeInstance.hasher,aTypeInstance);

Then I want to retrieve instance of this AType, from HashMap by providing Class<Result> and Input as a object value to the method responsible for this process. I don't have static type parameters in some cases, so I can't make respective typeLiteral<Input>() as when putting to HashMap. So I need to type(input) and calculate hash from Class<Result> and type(input), it gives me what I need in most cases.

HashMap<Hasher,AType<Anything,Nothing>> map= HashMap<Hasher,AType<Anything,Nothing>>();
Result doSmthProxy<Result>(Class<Result> resultType,Anything input){
        value kind=type(input);
	assert(exists inputType=if(kind.declaration.anonymous) then kind.extendedType else kind);
	value hasher=Hasher(resultType,inputType);
	assert(exists aTypeObject=map.get(hasher));
	...
}

I spotted that for tuple instances as Anything input, it fails. So whenever define such object like:

object tupleInputInstance extends AType<Boolean,[String,Integer]>(){
	shared actual Boolean doSmth([String, Integer] input) => input.first.size>0 && input.rest.first>0;

}

Assertion will fail for doSmthProxy function.

Of course everything is simplified here for sake of example, and in real implementation it is done a bit differently but it is not important, for this case.

@Voiteh I think that design is a bit fragile, as the lookup mechanism for ATypes does not account for subtyping (Result and Input types must be exact matches rather than allowing supertype and subtype matches respectively).

For example, an AType<Object, Object> would not be found for an input of String and/or an output of Anything.

Of course, making lookups more flexible would introduce two new challenges: 1) performance and 2) overload resolution for when multiple ATypes are found.

Perhaps these are things you already though of, but without addressing variance for inputs and outputs, there may be more surprises like the \Iempty vs Empty issue (which, btw, would not be an problem if the lookup mechanism allowed the input type to be a subtype of AType.Input).

@jvasileff Thank You for looking into this. This hashing mechanism is performance optimization, for cases where, I have exact types defined, for specific parameterized AType. For more generic cases I introduced "matching" mechanism, which is nothing fancy but iteration over Maps key,values and trying to match provided params to declared component Matcher. Like for specific AType<List<Anything>,Anything> there is inner interface AType<List<Anything>,Anything>.Matcher, which provides method Boolean match(Anything input, Class<List<Anything>> resultType), and Integer priority for cases where one component needs to override other one (I may resign from priority as it provides some confusion).

I was thinking about algorithm which would produce super type & implemented interface hash lookups but this gives me headache, as I would need to produce recurrent type parameters super types lookup also. For cases like Class<SomeType,[List<OtherType>,Integer ] there is a lot of computing, and as You mentioned overload resolution is also the case here. I would probably need, to take in consideration variance also.

My simple iteration (matching), for few of components, which would be registered may be faster but I haven't done calculations yet. I'm taking in consideration this If my library would grow up. I'm still in sandbox though :) with final implementation. AType is just an example here, to provide a bit of context for the issue and has nothing to do with final implementation.

I'm inclined to think this behavior is correct.

  1. The type [String,Integer] is defined to by terminated by Empty (not \Iempty) in the language spec.
  2. On the other hand, the function type(x) should always return the most precise (runtime) type of x.
  3. Empty and \Iempty are definitely not exactly equal types.

I'm going to close this, if there's nothing else here I'm missing.