The Crystal Programming Language Forum

Nilable cast always returns nilable, even if the cast always works

I was a bit surprised that the type of 1.as?(Int32) is Int32 | Nil. The Nil type doesn’t really make sense here. 1 always casts to Int32, so there’s no way it would be restricted to the alternative type Nil.
In general terms: The cast x.as?(Y) always succeeds if typeof(x) <= Y. It can’t possibly be Nil then, so the nilable type could be avoided.

Does this sound like a good idea, or am I missing something?

1 Like

If you know it’s an Int32, why use as?. If you are using as? then it means you don’t know if it’s going to be such type, and you expect to handle the nil case.

That said, I can’t remember why I implemented it this way. We could change it, it doesn’t sound hard to do.

Either the type or the value can come from parameters. Then it’s not as clear as 1.as?(Int32).

Exactly. If you see something like exp.as?(Int32) you can expect the result to be of type Int32 | Nil, regardless of the type of exp. That’s the reason behind this behavior.

I don’t understand why would I want to expect a Nil type that could never happen?

It’s probably this way for the same reason typeof((1 if true)) is Int32 | Nil. Complicating the compiler too much and spending precious developer time for very little (and subjective) improvement.

If you don’t expect a nil, why not use 1.as(Int32)? In fact, why not use just 1?

In summary, I need to see a real code using as? to understand the problem.

def foo(arg)
  arg.as?(Int32)
end

typeof(foo(1))        # => (Int32 | Nil) # The compiler could know that this is always Int32
typeof(foo("s"))      # => (Int32 | Nil) # The compiler could know that this is always Nil
typeof(foo(1 || "s")) # => (Int32 | Nil)

Ah, I see. And what do you use foo for?

I guess my question is, at some point of your program this is an issue. But the examples seem to be made up. Not that that’s bad, I’m just trying to understand where this behavior is causing an issue.

I don’t have an actual use case. It just came up when I was playing around, and it surprised me that the compiler doesn’t apply stronger type restrictions when it would be easy to do so.
So this is not a bug or limiting anything, I just think it would be nice if the compiler acted smarter.

Btw. replacing the method body with arg.is_a?(Int32) ? arg : nil, at least the second example typeof(foo("s")) is Nil. The first one is still Int32 | Nil, which is somewhat surprising, too.

To fix there, the fix should be done around here:

Though I think there’s something too in codegen, maybe in cleanup.