#270 Type Compatibility of Functions

alexlamsl Thu 3 Jul 2008

In the documentation:

|->Int|   fits  |->Void|   =>  false

I wonder why |Int, Int->Int| won't fit into |Int, Int->Void|?

andy Thu 3 Jul 2008

Because return types must be compatible.

That's an interesting use case though. But it would require some smarts in the emitter to generate multiple versions of the Func that return different types (or return nothing).

alexlamsl Thu 3 Jul 2008

I would have thought that allowing |->Obj| to fit |->Void| is like allowing method calls without necessarily assign the returned value to a variable.

Having said that, wrapping |->Obj| instead a handcrafted |->Void| is a straight-forward workaround - although it smells a bit like boiler plate code to me...

andy Thu 3 Jul 2008

Actually, |->Obj| does not fit |->Void|:

fansh> |->Obj|.type.fits(|->Void|.type)
false

alexlamsl Thu 3 Jul 2008

I was presenting the reason for |->Obj| to fit |->Void| - I am aware that it is currently not the case in Fan.

andy Thu 3 Jul 2008

Misunderstood - sorry :)

brian Thu 3 Jul 2008

That seems like a good idea, basically a function which returns a value is compatible with a function which doesn't return a value. I will look at next week.

tompalmer Fri 4 Jul 2008

I also think they should be compatible. I don't have a formal argument. It just seems easy/intuitive to interpret as the return value being ignored.

alexlamsl Fri 4 Jul 2008

That's okay Andy - we are using a powerful yet imprecise language for discussions on this forum, after all :o)

I am glad that we have concensus on this.

tompalmer Fri 4 Jul 2008

From the formal side, I actually have an argument now. You can't express it in the language, but Void is really the empty set, or rather the subtype of all other types. So it's effectively just a case of return type covariance, which Fan already supports.

tompalmer Fri 4 Jul 2008

Actually, never mind my last argument. I think this is contravariance which is officially bad for return types, but I think it's OK in this special case.

However, I think |->Void| fits |->Int| => false is also not ideal (and this is the covariance case), unless things default to not null. Or is Void the one not-null type in Fan already? Just seems like to be consistent, if Void is nullable like everything else, then it really could be the value null (and only null for that matter). Anyway, even if null isn't officially allowed for Void, I could see an argument for |->Void| as an |->Int| always returning null.

Or in default-not-nullable land |->Void?| as an |->Int?|, and Void would be the empty set.

Anyway, all this is feeling complicated to me at the moment. I think issues like this should be thought through, but it doesn't need to complicate the language in the end. Just go with anything intuitive that's sufficiently consistent and close enough to meaningful.

alexlamsl Fri 4 Jul 2008

When I read your covariance argument about Void being subtype of all other types just now, my thought was exactly resting on the non-null point in your following post ;-)

I am approaching the issue from an usability point of view, i.e. |->Obj| fits |->Void| => true would avoid frequent boiler-plate wrapper code. I would agree that if non-null is introduced into the language, then:

|->Void| fits |->Obj| => true
|->Void| fits |->Obj?| => false

would make sense as well. However, without the non-null feature, I think simply allowing |->Void| to fit |->Obj| would head straight for a perfect NPE storm.

(Just my 2 cents.)

tompalmer Fri 4 Jul 2008

Maybe you're right. I guess |->Int| fits |->Void| seems to have more practical value than |->Void| fits |->Int|. There are frequently return values that don't matter too much. On the other hand, something expecting a value but handling null well instead is probably uncommon.

Not that I've done statistics just now. Just my gut feeling based on past experience is going along with your argument.

Then again, there's my prior recommendation that covariance and contravariance both be allowed in all positions just because (and even though it's formally incorrect). If that were adopted (though it didn't seem popular), I'd recommend the same behavior here, just because.

helium Fri 4 Jul 2008

From the formal side, I actually have an argument now. You can't express it in the language, but Void is really the empty set, or rather the subtype of all other types.

Void is a unit type not the bottom type. The empty type has no valus. If a function has the emtpy type as return type it cannot return e.g. it is an infinit loop or allway throws an exception or whatever.

There is just no notation for the single (meaningless) value of Void in Fan as in most languages. (This is more a theoretical aspect but can become important if your type system gets a bit more advanced. In C++ I often wished they would have put some more thought in this so you don't have to special case void in templates, but that's off topic.)

What about just the opposit of your idea: Void as the top type? I haven't thought about this and I'm realy tired, so that might be bullshit. But it should at least allow the rule |->T| fits |->Void| forall T.

So Void is basically the union of all values of Obj and a single unit value. The type tree would look like this:

.       Void
.        ^
.        |
.       Obj
.        ^
.       /|\
.      / | \
.     /  |  \
.  all other types
.     \  |  /
.      \ | /
.       \|/
.   bottom type

Tomorow I might put some thought into whether that makes sense.

helium Sun 6 Jul 2008

OK, thought about it and absolutly don't like it. Go on and make that some kind of special case .. implicit conversation or whatever.

brian Sun 6 Jul 2008

To me Void just means no useful return value. If you call a Void method with reflection it does return something - it returns null. What we are talking about here is that if I declare a callback function where I don't care about the return (Void that is), then you can pass me a function with any return type. Void is kind of a special case already, so I think alexlamsl's original suggestion makes good sense.

helium Sun 6 Jul 2008

We are taking about |->Obj| fits |->Void| => true, right? At least that is what I was talking about ... Did I got this wrong? :(

brian Sun 6 Jul 2008

We are taking about |->Obj| fits |->Void| => true, right? At least that is what I was talking about

I thought that is what we were talking about - I was just recapping the entire discussion to make sure we were all on the same page.

brian Wed 9 Jul 2008

I took a look at this fix tonight:

// currently documented as
|->Int|   fits  |->Void|   =>  false

// proposal is to allow |->Void| to fit any return type
|->Int|   fits  |->Void|   =>  true

Actually it turns out the compiler was already implementing this behavior in its type checking. So this has always been legal from the compiler's perspective. However the reflective Type.fit API worked according to the documentation (and was inconsistent with the compiler's type checking).

I updated the Type.fits API to match the compiler's behavior. The new specification reads:

Specifically, function type A is compatible with function type B if these rules apply:

  1. A declares the same number or less parameters than B
  2. Each parameter type in A is compatible with its respective parameter type in B
  3. A returns a type compatible with B (or if B returns void, then A can return anything)

Login or Signup to reply.