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 AType
s 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 AType
s 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 Map
s 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.
- The type
[String,Integer]
is defined to by terminated byEmpty
(not\Iempty
) in the language spec. - On the other hand, the function
type(x)
should always return the most precise (runtime) type ofx
. Empty
and\Iempty
are definitely not exactly equal types.
I'm going to close this, if there's nothing else here I'm missing.