Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reified/Runtime Type Aliases #1359

Open
jvasileff opened this issue Jun 19, 2015 · 14 comments
Open

Reified/Runtime Type Aliases #1359

jvasileff opened this issue Jun 19, 2015 · 14 comments

Comments

@jvasileff
Copy link
Member

With reified generics, you can obtain an instance of Type<T> from a generic argument T. This proposal is to support the reverse: define a reified type alias T with an instance of Type<T>.

Type<Foo> someFooType = ...
alias F = someFooType; // F satisfies Foo

Reified (runtime?) type aliases would resemble type arguments. In fact, the behavior and capabilities of T and U in the example below would be indistinguishable:

String doSomething<U>(U arg) given U satisfies Object {
    return ...
}

String callSomething(Type<Object> theType, Anything arg) {
    // current (very unsafe!)
    assert(is String result = `function doSomething`.invoke([theType], arg));
    return result;

    // with reified type aliases:
    alias T = theType; // T satisfies Object
    assert (is T arg); // explicit, documentable point of failure (much better!)
    return doSomething(arg); // a normal, mistake free, type-checked invocation
}

An immediate benefit would be a greatly reduced need to use apply and invoke functions for meta programming, and a faster, safer bridge from meta to normal type checked code. I imagine a further benefit would be a tendency to create safer and more convenient apis due to the increased ability to use type arguments instead of Type<> arguments.

Note: another advantage above is that doSomething could have been a local declaration, which is not possible when a meta model reference is required.

A couple usage examples:

Example 1: Type Conversion Service

ConversionService provides functions to convert/coerce values from one type to another. toArray converts any Iterable to an Array of the required type, converting elements as necessary.

shared interface ConversionService {
    shared formal To? convert<To>(Anything from);

    // New style:
    shared formal To(From)? converter<From, To>();

    // Current style (not used in the example):
    shared formal Anything(Nothing)? converterForTypes(
            Type<Anything> from,
            Type<Anything> to);
}

"Convert [[items]] to an [[Array]] of type [[To]] (e.g. `Array<Integer>`).
 Return [[null]] if [[To]] is not an Array type, or if the element
 type of [[items]] cannot be converted to the element type of [[To]]."
shared To? toArray<To>(
        {Anything*} items,
        ConversionService conversionService) {

    value toType = `To`;
    if (is ClassOrInterface<Array<out Anything>> toType) {
        assert (exists toElementType = toType.typeArgumentList.first);
        assert (exists fromElementType = typeOfIterableElementFromInstance(items));

        alias ToElement = toElementType;
        alias FromElement = fromElementType;

        ToElement(FromElement)? elementConverter =
                conversionService.converter<FromElement, ToElement>();

        if (exists elementConverter) {
            // Option #1, with ceylon-spec/issues/560
            assert (abstracts Array<ToElement> To);
            return Array(items.map(elementConverter));

            // Option #2:
            assert (is To result = Array(items.map(elementConverter)));
            return result;
        }
    }
    return null;
}

This much more closely resembles a normal Ceylon program, rather than an alternate approach using invoke. And, importantly, the assertions are 1) explicit, and 2) easy to reason about. There are no calls to functions that may throw because we provided the wrong argument to a parameter of type Type<Anything> or Anything.

(See also #560)

Example 2: Configuration Service

This example is adapted from ceylon/formatter/options/formattingFile_meta.ceylon and contrasts style and error handling between the two approaches.

class Settings() {
    shared variable Boolean? booleanSetting = null;
    shared variable Integer? integerSetting = null;
    shared variable Float? floatSetting = null;
}

// This utility function only makes sense with runtime aliases.
// (it's not nearly as useful if both "Return" and
// "Arguments" need to be known statically.)
Function<Return, Arguments>? findFunction<Return, Arguments>
        (Package container, String functionName)
        given Arguments satisfies [Anything*] {
    try {
        return container.getFunction(functionName)
                ?.apply<Return, Arguments>();
    }
    catch (_) {
        return null;
    }
}

// For completeness (not relevant to this issue):
String nameOfParseFunction(Type<Anything> type) {
    String fullTypeString = type.intersection(`Object`).string;
    Integer? endOfPackageIndex = fullTypeString.inclusions("::").first;
    assert (exists endOfPackageIndex);
    String trimmedTypeString = fullTypeString[endOfPackageIndex+2 ...].trim('?'.equals);
    return "parse" + trimmedTypeString;
}

// Using current api and language features:
void setOld(Settings settings, String option, String optionValue) {
    "invalid option"
    assert (exists attribute = `Settings`.getAttribute<Settings>(option));

    "parse function of expected name not found"
    assert (exists parseFunction =
            `package ceylon.language`.getFunction(
            nameOfParseFunction(attribute.type)));

    // fail if parse function argument type is not `String`
    value applied = parseFunction.apply<Anything, [String]>();

    // fail if parse function result type is wrong
    // fail if attribute is not settable
    Anything parsedOptionValue = applied(optionValue); // Note Anything result
    attribute(settings).setIfAssignable(parsedOptionValue);
}

// Using runtime/reified aliases:
void setNew(Settings settings, String option, String optionValue) {
    "invalid option"
    assert (exists attribute = `Settings`.getAttribute<>(option));

    alias AttributeType = attribute.type; // runtime alias

    "option not settable"
    assert (is Attribute<Settings, AttributeType, AttributeType> attribute);

    "parse function not available"
    assert (exists parseFunction = findFunction<AttributeType, [String]>(
                `package ceylon.language`,
                nameOfParseFunction(attribute.type)));

    AttributeType parsedOptionValue = parseFunction(optionValue); // Note AttributeType result
    attribute(settings).set(parsedOptionValue);
}

shared void tryIt() {
    Settings settings = Settings();
    setOld(settings, "integerSetting", "10");
    print(settings.integerSetting);
    setNew(settings, "integerSetting", "20");
    print(settings.integerSetting);
}
@gavinking
Copy link
Member

Yeah, I've had ideas along these lines before. But never sat down and really tried to think it through. It's an interesting idea.

@pthariensflame
Copy link
Contributor

I like this idea, but I'm a little wary of the alias-based syntax. I guess it's not terrible, though, and it's not as though it's misleading as to its effects.

@tkaitchuck
Copy link

What if there were a more generalized concept of this? Suppose there was a syntax that was defined as "The reverse of backticks" (perhaps ' '). IE: 'Foo' is equivalent to Foo

Then one could write:

alias T => 'theType'; 
assert (is T arg); 
return doSomething(arg);

Without making alias keyword special in any way. Or more concisely:

    assert (is 'theType' arg); 
    return doSomething(arg);

@pthariensflame
Copy link
Contributor

@tkaitchuck What you're now proposing is basically dependent typing (without Pi or Sigma). You'd be able to use a runtime type in any place where a compile-time type is accepted. A system like that can totally work, but it needs very careful design, and I believe @gavinking has stated a negative reaction to features like this before.

@RossTate
Copy link
Member

It's only considered dependent typing if you also do computation with the values at the type level. This looks to me like unpacking an implicitly existentially qualified type variable. That's much easier to do right than dependent types. Generally syntax is the biggest challenge with this kind of stuff.

@pthariensflame
Copy link
Contributor

@RossTate Oh, OK! I guess you'd know more about it than me. It still seems like you'd be able to pull stuff like this, though:

class Sigma<A>(fst, tyMap, snd) {
    shared A fst;
    shared Type<out Anything> tyMap(A x);
    shared 'tyMap(fst)' snd;
}

@RossTate
Copy link
Member

Yeah, the difference is subtle. I suppose in your example the difference would be best illustrated by the fact that you couldn't guarantee that multiple occurrences of 'tyMap(fst)' represent the same type. In fact, it would be unsound to do so because tyMap might be stateful.

@pthariensflame
Copy link
Contributor

tyMap might be stateful

_!!!_

That just makes it worse; how is it remotely possible, given what you said, to guarantee that compilation terminates? Or is there something else preventing me from writing my Sigma class?

@RossTate
Copy link
Member

I think our interpretations of the proposal differ. I interpreted 'the stuff in ticks' to be an expression that is evaluated at run time. Hence any reasoning about it at compile time is quite limited, mostly restricted to the existential type stuff I was talking about earlier.

That makes me think that things like alias T = runtimetype; is clearer syntax than simply 'runtimetype'.

@pthariensflame
Copy link
Contributor

Oh, I see. I guess one could still use stuff like 'theType' where theType had the type Type<out T> for some known type T, and then the compiler would just regard any usage of 'theType' as a subtype of T, but I don't know how useful that would be. :/

@zamfofex
Copy link

@RossTate Sorry, but can something like this work?:

class Type<out T>
{
    // ...
}

String callSomething(Type<Object> theType, Anything arg)
{
    assert(is theType.T arg);
    return(doSomething(arg));
}

Or, more interestingly:

theType.T elseThrow(Type<Anything> theType, Anything obj)
{
    if(is theType.T obj)
    {
        return(obj);
    }
    else
    {
        throw(Exception("``obj`` isn't of type ``theType``"));
    }
}

void foo(Type<Object> someType, Anything someObject)
{
    Object obj = elseThrow(someType, someObject);
    // Here, we know that `elseThrow` will return at least an
    // `Object`, because `someType` is of type `Type<Object`>,
    // but if the `someType` is of a different type (let's say,
    // `Type<Qux>`), at runtime, `elseThrow` will always return
    // a `Qux`!
}

That's something I feel like is interesting, but I'm unsure whether this can cause some sort of problem.

@FroMage
Copy link
Member

FroMage commented Jun 29, 2015

We need something like that, to be able to pass runtime types as type arguments. This is specially useful for functions that require reified generics:

void invokeController(Object container, Class<Controller,Nothing> klass){
  alias ContainerType = type(container);
  assert(exists method = klass.getDeclaredMethod<ContainerType,Response,[]>("handle"));
  method(container)();
}

Certainly the runtime behaviour of passing a reified generic from a ceylon.language.meta.model::Type should be straightforward. Same for is. What other operations on types do we have that would be affected by this?

Note that this is not enough to make it easy to scan for methods of certain parameter types given parameter instances:

void invokeController(Object container, Object[] arguments, Class<Controller,Nothing> klass){
  alias ContainerType = type(container);
  // this would probably fail because it's not trivial to convert an Object[] to a tuple of each element type
  // for example, we want a [Integer,String] and not an Object[] or [Object,Object]
  alias ParameterTypes = type(arguments);
  assert(is ParameterTypes arguments);
  assert(exists method = klass.getDeclaredMethod<ContainerType,Response,ParameterTypes>("handle"));
  method(container)(*arguments);
}

But I suppose this can be fixed by a toplevel that turns a sequence of T into a tuple whose elements are of the type of the sequence elements, even though this is not known at compile-time. Might even warrant a function on Sequential (do we already have that?).

@jvasileff
Copy link
Member Author

Yeah, nice examples. I think we'd still need an assert on container:

alias ContainerType = type(container);
assert (is ContainerType container);

unless we stipulate cases where the narrowing would be implied (basically when using the result of type on a non-variable reference.) Obviously it would be safe here. Maybe there should be some accommodation for this in the syntax.

@RossTate
Copy link
Member

So @FroMage's seems like it would work using the existential technique I mentioned. @Zambonifofex's examples, if I understand them correctly (I take it theType.T is supposed to represent the exact type argument theType was constructed with), could be made to work by recognizing that theType is a constant reference and its field T is an immutable field.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

7 participants