eclipse-archived / ceylon

The Ceylon compiler, language module, and command line tools
http://ceylon-lang.org
Apache License 2.0
399 stars 62 forks source link

inferred supertypes for object expressions #5739

Open gavinking opened 9 years ago

gavinking commented 9 years ago

Consider the following code:

interface Duck {
    shared formal void quack();
}
void fun(Duck duck) {
    duck.quack();
}

void run() => fun(object satisfies Duck { quack() => print("quack"); });

I just did a quick and rough implementation of inference for object expression interfaces such that the following code passes the typechecker and nothing else important seems to have broken:

void run() => fun(object { quack() => print("quack"); });

The logic is:

then the supertype is inferred to be the type of the function parameter.

This is basically analogous to the sort of logic we do to infer anonymous function parameter types. I like it.

I'm still not sure there aren't any holes in the typechecker logic, however.

Reactions?

lucaswerkmeister commented 9 years ago

My first reaction was “WTF, duck typing”?

But once you explained under which conditions it’s allowed… that actually seems reasonable, somewhat consistent with parameter type inference, and not unsound to me. Not sure if satisfies Duck is so much of a nuisance to write that we should offer this, but I’m not opposed to it.

I guess this could also work nicely for dynamic interfaces, as an alternative to the dynamic [ … ] syntax.

gavinking commented 9 years ago

I might even be able to make this work for something like this:

Iterator<String> iterator() => object { next() => "hello"; };

Which would cut down on some verbosity in Iterable.

ghost commented 9 years ago

This is basically analogous to the sort of logic we do to infer anonymous function parameter types.

Yes, I feel similarly. However I don't feel like this works here... I think it's one of those things were the compiler is being too clever and assuming too much stuff that would be better have explicit in the code... It could be a little confusing for people reading the code, even if it's the person who wrote it, reading it a couple minutes after they wrote it. Also the restriction makes it feel to me like it would be useful really rarely, and it would be one of those features that everyone forgets, and gets confused when they see it...

quintesse commented 9 years ago

So how about the next step: allow to pass a function if the interface has only a single method?

gavinking commented 9 years ago

So how about the next step: allow to pass a function if the interface has only a single method?

Well, right, that's where this idea came from—to be able to infer the type of a Java-style lambda.

ghost commented 9 years ago

I think it'd be better to map functional interfaces to Callableor something like that...

gavinking commented 9 years ago

@Zambonifofex Well there are some things we can perhaps do along those lines by they wouldn't be perfect. And this particular change is independently useful, IMO.

quintesse commented 9 years ago

I like this option btw and extended to the Java-style lambda even more. I like the terseness better than any possible confusion it could cause for newbie users. (Doesn't mean we should always do it, but I don't feel we're stepping over the line here)

RossTate commented 9 years ago

Oh, I was actually about to tell you that you could do this. Overloading complicates things a bit, but not too much.

So, in short, this is approved by math proofs.

On Sun, Nov 15, 2015 at 8:50 PM, Tako Schotanus notifications@github.com wrote:

* WARNING: THE ATTACHED DOCUMENT(S) CONTAIN MACROS \ MACROS MAY CONTAIN MALICIOUS CODE \ Open only if you can verify and trust the sender \ Please contact infosec@redhat.com if you have questions or concerns

I like this option btw and extended to the Java-style lambda even more. I like the terseness better than any possible confusion it could cause for newbie users. (Doesn't mean we should always do it, but I don't feel we're stepping over the line here)

— Reply to this email directly or view it on GitHub https://github.com/ceylon/ceylon/issues/5739#issuecomment-156888214.

pthariensflame commented 9 years ago

Why just interfaces? You should be able to do this with any type at all (and then just error appropriately if you cannot inherit from it).

gavinking commented 9 years ago

Well to inherit from a class you have to explicitly pass value arguments.

We could make it work for an intersection of interfaces.

FroMage commented 9 years ago

We're getting more and more left-to-right inference, with its non-refactorability effects, and "invisible semantics". Where's the line? It'd also be nice to pass parameters lazily based on the receiver type, but IMO that hides behaviour from the caller, and can introduce really hairy issues with side-effects.

I mean, I do see what's nice about these callee-implied invisible-on-call-site behaviours, but I worry they stray from the original idea of "more clearly".

ghost commented 9 years ago

Interestingly enough, Snap! has lazy parameters. They look the same as eager ones from the caller. In fact, loops are literally just like functions in Snap!!

gavinking commented 9 years ago

We're getting more and more left-to-right inference

Well, sure, and when we didn't have any left-to-right inference, I resisted adding it. But once we had it for anon function parameters, it made sense to have it for function refs. And now we've opened that door, I don't see that it makes much sense to rush to close it now.

with its non-refactorability effects

Well, its effect is to limit cut-and-paste. Tool based refactorings have no problem with these constructs.

Where's the line?

Well it's not like this is some totally open-ended thing where we'll keep adding more and more special cases of left-to-right inference. There's a rather limited set of cases where it is even possible in Ceylon. Our type system is simply not amenable to HM-style type inference, even if we wanted it.

And TBH I have absolutely no desire to add parameter type inference to anything other than anonymous functions.

I worry they stray from the original idea of "more clearly".

Well, I don't think that this particular feature makes the code less clear.

Actually it's possible that it opens up some interesting options for clean framework design that previously would not have bean feasible.

quintesse commented 9 years ago

I would argue it's actually more clear. I mean the information we're leaving out does not hinder understanding the code much and might in many cases improve it by removing the non-essential.

And like using value for example there might be times where you'd prefer to be more explicit just to make things more clear but most of the time value work just fine. I see a similar situation here.

ghost commented 9 years ago

@quintesse I feel like the problem here is that there isn't anything indicating that there is implicit stuff going on...

quintesse commented 9 years ago

@Zambonifofex sure, I just don't think it's the kind of implicitness that makes things difficult to understand, it's not like you have to go look all over the place to figure out what the heck the code is doing.

In the case of the object it's clear that something must be going on because passing an object without an interface to a function does make sense, so you look at the type of the parameter and you'll see the information you're looking for. It's more indirect perhaps but the information is right there.

In the case of passing a function to a single-method interface you might indeed not notice at first that the type of the parameter of the function it's passed to is not a function reference but an interface, but it does not take away from the understanding of the code when you read it IMO.

gavinking commented 9 years ago

I feel like the problem here is that there isn't anything indicating that there is implicit stuff going on...

@Zambonifofex. I don't think that's quite right.

Remember, there is essentially no practical use at all for an object expression with no supertypes. So if an object expression has no explicit supertypes, one can infer pretty quickly that it must have implicit supertypes.

That, and the fact that it is explicitly refining methods that it doesn't itself declare!

So I don't see any real possibility for confusion here.

gavinking commented 9 years ago

In the case of the object it's clear that something must be going on because passing an object without an interface to a function does make sense, so you look at the type of the parameter and you'll see the information you're looking for. It's more indirect perhaps but the information is right there.

Well it's even easier than that in fact. Just hover the object expression in the IDE.

quintesse commented 9 years ago

Just hover the object expression in the IDE.

I always think of the case of people looking at code in an article or a book, etc, it has to be understandable for them as well. :)

jvasileff commented 9 years ago

This is tantalizingly close to being really nice for Java 8 lambdas, but the {;} and method name still get in the way.

gavinking commented 9 years ago

This is tantalizingly close to being really nice for Java 8 lambdas, but the {;} and method name still get in the way.

Sure, but if we did this, I would tackle that problem next, as an abbreviation of this.

jvasileff commented 9 years ago

I always think of the case of people looking at code in an article or a book, etc, it has to be understandable for them as well. :)

IMO, the original method name (fun in the example) provides plenty of context, given Ceylon (and Java) conventions.

Java 8 dispenses with interface names for this. And for something similar, Ceylon, unlike some languages, does not require (or even offer the ability for an API writer to require) parameter names, which are only used in named argument invocations. And, finally, you currently only have this "extra" argument type information when defining the object inline–it's missing if the argument is, for example, a top-level value.

chochos commented 9 years ago

I suppose this only works when the supertypes are omitted, and required. If you must pass a Duck and you want to define Duck&Dog then you must specify the supertypes, that is, Duck should not be automagically inferred unless no supertypes are defined.

luolong commented 9 years ago

If this works AND is unambiguous, I have no problems whatsoever with this.

And to be sure, the extra object {...} syntax does not bother me at all. If anything, it makes it so much clearer that we are defining inline anonymous object instance instead of an anonymous function. At the moment I like to make that distinction.

But I have next to no experience at the moment with interop with Java 8 API's from Ceylon, so it is a bit too early to form an opinion

davidfestal commented 8 years ago

+1. I like this also I find it nice and clear

chochos commented 8 years ago

I suppose this only works if you don't specify any supertypes, right? If you want your object to satisfy some other interface besides the inferred ones, you'll have to specify all supertypes?

gavinking commented 8 years ago

If you want your object to satisfy some other interface besides the inferred ones, you'll have to specify all supertypes?

That's what I'm proposing, yes.

xkr47 commented 8 years ago

Are you planning on limiting this to interfaces with only one function? I guess supporting intersection types would not add much value if so..?

gavinking commented 8 years ago

Are you planning on limiting this to interfaces with only one function?

No.

I guess supporting intersection types would not add much value if so..?

I plan to implement support for intersection types now.

gavinking commented 8 years ago

It seems nobody objects strongly to this feature, so I'm going to merge this work to master now.

ghost commented 8 years ago

@gavinking Well, I do! I didn't say anything anymore because I didn't want to be redundant, but I strongly dislike this feature...

Another point I've been wanting to make: What if I have a function that accepts MyInterface, and from one version of my module to another, I change the parameter type to Object, or some other supertype of MyInterface? Now other modules might be broken because of an apparently, and intuitively "innocent" change I made...

gavinking commented 8 years ago

@Zambonifofex that is a reasonable objection. OTOH it's an objection that in principle also applies to parameter type inference. But perhaps it's worse in this case.

I'll give it some thought.

ghost commented 8 years ago

For me, it feels like this feature will be useful very rarely. It barely reduces boilerplate and increases clarity, besides being considerably intuitive...

I'm generally against this sort of inference. Honestly, I ain't even too keen on inference for lambda arguments' type, and type parameters inference; however they help eliminate a lot of boilerplate very frequently, and really help improve readability, unlike this feature, which I can't imagine being frequently used... As I said before, I think it will be one of those "WTF" features that people forget, and get confused when they see it...

ncorai commented 8 years ago

While I have no objections to the feature itself, I'm not too fond of the 1.2.1 version target. Could we keep 1.2.x releases for bug fixes but not new features? That's how 1.1.1 became 1.2.0 and we had to wait one year for some bugs to be fixed in an official release.

gavinking commented 8 years ago

Huh?! What the hell happened to @Zambonifofex? He's gone! And his comments with him...

lucono commented 8 years ago

I like this proposal in its original form.

I however, do not like the additional proposal of being able to pass an anonymous function or function reference if the required interface is one with only a single method.

That is unless a sort of named anonymous function or named function reference is used, of which the name must be the same as that of the single interface method being implemented. That, I feel, would make it clearer and more immediately intuitive, and therefore more at home in Ceylon.

lucaswerkmeister commented 8 years ago

This would also be awesome to have for TypeScript interop. When a function has a structural type as parameter type, e. g.

export function createServer(options?: { allowHalfOpen?: boolean; }, connectionListener?: (socket: Socket) =>void ): Server;

then all the TS model loader can do, I think, is to turn it into some toplevel type __StructuralType_1. And it would be really ugly if users had to spell out that generated name.

jvasileff commented 8 years ago

A nice benefit of using this for implementing Java SAMs is that the possibly-null parameter and return type support would "just work".

For comparison, the best I could come up with when trying to write a flexible helper function consumer() to create Consumers was:

Consumer<Arg> consumer<Arg>(Anything(Arg) | Anything(Arg?) f)
        given Arg satisfies Object => object
        satisfies Consumer<Arg> {
    accept = maybeFun(f);
};

Result(Arg?) maybeFun<Result, Arg>(Result(Arg?) | Result(Arg) f)
    =>  let(nullFunction
            = if (is Anything(Arg?) f) then f else null)
        ((Arg? t) {
            if (exists nullFunction) {
                return nullFunction(t);
            }
            if (!exists t) {
                throw NullPointerException();
            }
            return f(t);
        });