The semantics of type restictions

Hm, is it a good thing that Crystal works that way? That the return type annotation is only a contract for the method body but not for those using it? This way, adding a perfectly valid subclass can break existing code using the superclass, i.e. it kindof violates the Liskov substitution principle.

Huh! Had no idea the compiler was working like this!

Is there some sort of “–enforce-restrictions” option or something that can be passed to the compiler to prevent this type of behavior? The idea that the compiler might just not listen to my explicitly programmed in type restrictions is… rather distressing to say the least! I’ve been relying on it as a tool to keep me in check and make sure I haven’t accidentally messed up somewhere, so knowing that it might just let my mistakes go along without even telling me is concerning.

I think the issue that covers the return type would be Function return type to be automatically typecasted similar to assignment/function args · Issue #10837 · crystal-lang/crystal · GitHub

There is no compiler option to enforce that check so far.

It’s called a type restriction for a reason. It specifies which types are allowed, but preserves the concrete type.

This is a useful feature because it allows the compiler to reason about which types are actually encountered in instantiations, and apply appropriate optimizations.

I guess Crystal is in a bit of a unique position there, but so is the entirety of its type system.

A compiler flag to enable stricter type casting semantics probably wouldn’t work because so much code, including in the standard library, depends on this behaviour.

I’m open to explore the idea of evolving the type system towards a bit more strictness. It’s great if we can eliminate the potential for bugs at the type system.

For example, it could be an option to keep a reference to the concrete type for optimization purposes, while also keeping track of the “shape type” which restricts the available interface.

That’s an interesting idea, I’ll explore it on the project I have going now.

Every type in a parameter restriction is effectively a covariant type parameter. One way to move forward is to make the covariance explicit via Upper-bounded free variables in def restrictions · Issue #11908 · crystal-lang/crystal · GitHub

The entire return type restriction is covariant. It must also name an actual valid type, so the following is currently illegal:

def foo(x : Array(Array)) : Array(Array)
  x
end

foo([[1]]) # Error: method ::foo must return Array(Array(T)) but it is returning Array(Array(Int32))

Following the example Doubt about overloading - #4 by HertzDevil, if you know C++, the snippet in the OP would be analogous to:

std::convertible_to<std::variant<int, std::nullptr_t>> auto
foo(std::convertible_to<int> auto x) {
  return
    // x == 0 ? nullptr :
    1 / x;
}

foo(3) + 1;
1 Like