Skip to content

(How) should return type inference work with sibling classes? #28276

@DanilaFe

Description

@DanilaFe

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)

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions