ruby / rbs

Type Signature for Ruby
Other
1.91k stars 201 forks source link

Support type narrowing #1822

Open mtsmfm opened 1 month ago

mtsmfm commented 1 month ago

Ref: https://github.com/soutaro/steep/issues/472

Currently RBS doesn't have any syntax to narrow types in control flow.

So there's no way to achieve the following:

a = [1, nil].sample # a is `Integer | nil`
unless a.nil?
  # a is `Integer` here
  p a + 1
end

But actually, both Sorbet and Steep treat some special methods to narrow types

https://github.com/sorbet/sorbet/blob/718bc64f895abeeff88f56efbc92a3554ffaa4f2/infer/environment.cc#L536-L545

https://github.com/soutaro/steep/blob/a868762c2bd09f0954b05cdd9eab1819e14a51d3/lib/steep/interface/builder.rb#L709-L721

Ideally, I think we need to have a syntax considering Ruby gems which extend Object like ActiveSupport's present?.

I'm not very familiar with type system though, I guess the syntax would be similar with TypeScript's type predicates

https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates

class Object[T?]
  def nil?: () -> self is nil
end
class Object[T?]
  def present?: () -> self is T
end
ParadoxV5 commented 1 month ago

:up: for something (not necessarily this)

From https://github.com/soutaro/steep/pull/905#issuecomment-1977213468, the current solution is

class Object
  def nil?: () -> bool # cannot be assertively `false` or else `NilClass#nil?` breaks polymorphism of `NilClass < Object`
end
class NilClass
  def nil?: () -> true
end

Or, with https://github.com/ruby/rbs/discussions/1639#discussioncomment-7648121,

class Object
  def nil?: [self < nil] () -> true
          | () -> bool
end

But in either case, the prickly part is that there’s no format to tell that non-nils’ #nil? always return false. In pseudocode, we need:

class Object
  def nil?: [self < nil] () -> true
          | [self !< nil] () -> false # ⬅ a way to declare a negative constraint
end

Alternatively, if the overload semantics changes so that earlier branches subtractively narrow latter branches,

class Object
  def nil?: [self < nil] () -> true
          | () -> false # `else`
end
ParadoxV5 commented 1 month ago

https://docs.python.org/3.13/library/typing.html#typing.TypeIs Y'all were talking about adding pure to RBS for type narrowing predicates before, perhaps this would work instead? ⸺@DanielPechersky, on Ruby Discord