Consider the following program:
class Parent {}
class Child1 : Parent {}
class Child2 : Parent {}
proc foo(x: bool) {
if x {
return new Child1();
} else {
return new Child2();
}
}
During a recent discussion, there was broad agreement that this program should work.
It seems clear that we should infer the return type to be Parent. This is not currently specc'ed, and would be an (unstable) language improvement. However, there are three questions:
- What should happen to nilability?
- Do we require it to be the same?
- Do we pick most permissive (nilable if any nilable)
- What should happen to ownership?
- Do we require it to be the same?
- Do we perform conversions?
- Technically, all classes inherit from
RootClass. Should we unify the return type to be RootClass?
I performed a survey of languages where both OO and type inference coexist. There are not many. Not all languages unify child classes to the parent; however, in these languages, no unification is done, and all return types must match exactly. In particular, cases like return 1/return 1.0 are not allowed. I’d argue this makes their philosophy on return type inference very different from ours, and to me makes them less interesting as candidates for comparison. I have added an extra column for this to mark such languages, "Allows coercions in inferred-type returns". In languages with union types, typically, any combination of returns is permitted; it's just encoded as a return of A | B. When a union type is produced, I've marked the outcome 🟨. However, some languages (Crystal), do specifically collapse such unions to the parent class. In that case, I've marked the column ✅. Most of these languages do not have nilability. Those without nilability, I've marked as - for "merges nilability".
| Language |
Allows coercions in inferred-type returns |
Unifies Child1/Child2 to Parent |
Unifies unrelated classes to RootClass |
For unified child classes, merges nilability |
| Scala* (ex) |
✅ |
✅ |
✅ |
- |
| Kotlin* (ex) |
✅ |
✅ |
✅ |
✅ |
| Rust |
❌ |
❌ |
❌ |
- |
| Swift |
❌ |
❌ |
❌ |
❌ |
| TypeScript |
✅ |
🟨 |
🟨 |
🟨 |
| Python (typechecker) |
✅ |
🟨 |
🟨 |
🟨 |
| Ruby (typechecker) |
✅ |
🟨 |
🟨 |
🟨 |
| Crystal (ex) |
✅ |
✅ |
🟨 |
✅ |
| Chapel (main) |
✅ |
❌ |
❌ |
❌ |
🟨 = Language uses union types
* = this may be inherited from the JVM, on which these languages are based
Chapel is unique for having ownership of classes a built-in property. There is no precedent for how these should be unified.
Personally, I think we should rule out languages where coercions are disallowed, since they disagree with existing design decisions we've made for Chapel. Furthermore, I think we should rule out dynamic languages, because they do not need to use their type information from compilation, but only for describing function behavior. It is in their interest to be more permissive via unions. Since we don't have union types, I also propose we treat cases where unions are created as disallowed. The resulting table is:
| Language |
Allows coercions in inferred-type returns |
Unifies Child1/Child2 to Parent |
Unifies unrelated classes to RootClass |
For unified child classes, merges nilability |
| Scala* (ex) |
✅ |
✅ |
✅ |
- |
| Kotlin* (ex) |
✅ |
✅ |
✅ |
✅ |
| Crystal (ex) |
✅ |
✅ |
❌ |
✅ |
| Chapel (main) |
✅ |
❌ |
❌ |
❌ |
Looking at this table, my proposal is as follows:
- Unify
Child1/Child2classes toParent` when mix in returns.
- If any return is nilable, infer the whole return type to be nilable.
- Force management to be the same. This can be relaxed later.
- Do not unify return type to RootClass. I feel empowered by Crystal's precedent here, which disallows the unification even though it has a root object type. I think this is a good idea because:
- It catches errors in which a user meant to mark classes as inheriting, but didn't.
- It catches accidentally returning an unrelated class from the function.
- This can be overriden by explicitly specifying a
RootClass return type
- If we did pick
RootClass as the return type, it'd be pretty useless. We can't call any methods on it without down-casting, and we loose all type information.
This leads to:
| Language |
Allows coercions in inferred-type returns |
Unifies Child1/Child2 to Parent |
Unifies unrelated classes to RootClass |
For unified child classes, merges nilability |
| Chapel (proposed) |
✅ |
✅ |
❌ |
✅ |
Consider the following program:
During a recent discussion, there was broad agreement that this program should work.
It seems clear that we should infer the return type to be
Parent. This is not currently specc'ed, and would be an (unstable) language improvement. However, there are three questions:RootClass. Should we unify the return type to beRootClass?I performed a survey of languages where both OO and type inference coexist. There are not many. Not all languages unify child classes to the parent; however, in these languages, no unification is done, and all return types must match exactly. In particular, cases like
return 1/return 1.0are not allowed. I’d argue this makes their philosophy on return type inference very different from ours, and to me makes them less interesting as candidates for comparison. I have added an extra column for this to mark such languages, "Allows coercions in inferred-type returns". In languages with union types, typically, any combination of returns is permitted; it's just encoded as a return ofA | B. When a union type is produced, I've marked the outcome 🟨. However, some languages (Crystal), do specifically collapse such unions to the parent class. In that case, I've marked the column ✅. Most of these languages do not have nilability. Those without nilability, I've marked as-for "merges nilability".Child1/Child2toParentRootClass🟨 = Language uses union types
* = this may be inherited from the JVM, on which these languages are based
Chapel is unique for having ownership of classes a built-in property. There is no precedent for how these should be unified.
Personally, I think we should rule out languages where coercions are disallowed, since they disagree with existing design decisions we've made for Chapel. Furthermore, I think we should rule out dynamic languages, because they do not need to use their type information from compilation, but only for describing function behavior. It is in their interest to be more permissive via unions. Since we don't have union types, I also propose we treat cases where unions are created as disallowed. The resulting table is:
Child1/Child2toParentRootClassLooking at this table, my proposal is as follows:
Child1/Child2classes toParent` when mix in returns.RootClassreturn typeRootClassas the return type, it'd be pretty useless. We can't call any methods on it without down-casting, and we loose all type information.This leads to:
Child1/Child2toParentRootClass