Generics Don't Respond Properly to Common Methods or Nil Checks
Hi all. I'm currently working on trying to implement a generic class that has a nilable instance variable of the generic type, however Steep is having issues with handling the generic type.
There a two issues at hand here as far as I can tell. The first is that generic variables despite all types in Ruby being children of the Object type, do not recognize that they do have access to these methods. The same goes for interfaces as well. This means you can't use methods like nil? or is_a? to verify the type in question.
The second issue here is that if you add a constraint to the parameter to have it derive from Object it refuses to update the type from being optional to not optional, which means a method that say performs a nil check and then returns the non-nil value refuses to work correctly.
Below is a minimal example of my code:
class Optional
def initialize(value = nil)
@value = value
end
def or_else(default)
return @value.nil? ? default : @value
end
end
And this is the RBS
class Optional[T]
@value: T?
def initialize: (?T? value) -> void
def or_else: (T default) -> T
end
These are some of the errors I get:
[error] Type `(T | nil)` does not have method `nil?`
│ Diagnostic ID: Ruby::NoMethod
│
└ return yield @value unless @value.nil?
~~~~
[error] The method cannot return a value of type `(T | nil)` because declared as type `T`
│ (T | nil) <: T
│ nil <: T
│
│ Diagnostic ID: Ruby::ReturnTypeMismatch
│
└ return @value unless @value.nil?
~~~~~~~~~~~~~
[error] Cannot pass a value of type `(T | nil)` as an argument of type `T`
│ (T | nil) <: T
│ nil <: T
│
│ Diagnostic ID: Ruby::ArgumentTypeMismatch
│
└ return [email protected]? && predicate.call(@value) ? self : Optional.empty
You need to add constraints over type variables to call some method with them.
class Optional[T < _NilP]
interface _NilP
def nil?: () -> bool
end
end
Note that the nil? definition won't work for type narrowing because of implementation limitation. You can use nil or unless syntax to let type narrowing work.
def or_else(default)
if value = @value
value
else
default
end
end
I think the problem is that BasicObject does not have #nil? method.
Besides @soutaro 's example, I think this is another useful usage.
class Optional[T < Object] # or [T < Kernel]
@value: T?
def initialize: (?T? value) -> void
def or_else: (T default) -> T?
end
@soutaro's solution works like a charm, I think the only limitation is that it is unable to handle an Optional that is passed a boolean value. (Basically, because the check can't meaningfully differentiate between nil and false because both are falsey)
Basically, because the check can't meaningfully differentiate between nil and false because both are falsey
Got it...
Hmm, possible but weird workaround would be using case-when:
def or_else(default)
case value
when NilClass
default
else
value
end
end
But I don't think I want to recommend it...
The case-when solution doesn't seem to work as that check doesn't seem to properly cast the nil off in the else block. It was a nice idea though
It does appear that .nil? does work when the value is set to a local variable however, just not when it's an instance variable
Yeah, it's complicated. Type narrowing works for local variables and some of the method calls, but not for instance variables.
No unwrap in else-block in case-when example might be a bug....