-
Notifications
You must be signed in to change notification settings - Fork 511
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Remove enumerable runtime checking by default #3293
Conversation
fa57043
to
611e306
Compare
@@ -12,7 +12,7 @@ def name | |||
end | |||
|
|||
# @override Base | |||
def valid?(obj) | |||
def valid?(obj, deep=false) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is this positional while the others are keyword? I generally prefer that optional boolean arguments are keyword arguments.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On second thought, seeing how this is used throughout the typed_enumerable code below, it seems really easy to forget to forward through the deep
parameter. Can we make this parameter required, and change pay-server?
@@ -13,7 +13,7 @@ def underlying_class | |||
end | |||
|
|||
# @override Base | |||
def valid?(obj) | |||
def valid?(obj, deep=false) | |||
obj.is_a?(Array) && super |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ooc, we could short circuit all of these (typed array, typed hash, etc.) if deep
is true
, right? Why have you chosen to unconditionally call super
?
@aisamanra Can you remind me what the motivation is here? |
This now has a |
# statically known and being checked appropriately. If `checked` is true, raises an exception at | ||
# runtime if the value doesn't match the type. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This comment looks out of date?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Comment updated to be correct now. I've also added a bit to the docs, since I had forgotten to do that.
# in some cases this runtime check can be very expensive, especially | ||
# with large collections of objects. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
# in some cases this runtime check can be very expensive, especially | |
# with large collections of objects. | |
# in some cases this runtime check can be very expensive, especially | |
# with large collections of objects. | |
# | |
# This doesn't have any connection to static type refinements at all. | |
# If you additionally want the type to be cast statically, use `T.let` or `T.cast`. |
@@ -13,10 +13,15 @@ def underlying_class | |||
end | |||
|
|||
# @override Base | |||
def valid?(obj) | |||
def recursively_valid?(obj) | |||
obj.is_a?(Array) && super |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
obj.is_a?(Array) && super | |
valid?(obj) && super |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
^ This might be a change you want to make in a few other places too (just for consistency)
when Set | ||
obj.each do |item| | ||
return false unless @type.valid?(item) | ||
return false unless @type.recursively_valid?(item) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like this a lot better with the recursively_valid?
naming / boolean blindness issues.
rbi/sorbet/t.rbi
Outdated
@@ -32,6 +32,9 @@ module T | |||
sig {params(value: T.untyped, type: T.untyped, checked: T::Boolean).returns(BasicObject)} | |||
def self.assert_type!(value, type, checked: true); end | |||
|
|||
sig {params(value: T.untyped, type: T.untyped).returns(BasicObject)} | |||
def self.check_type_recursive!(value, type); end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we want to put this in T::Private
? Do we intend this to be a public API?
|
||
assert_raises(TypeError) do | ||
StructHash.new(the_hash: {'foo' => {}}) | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Was this deleted just because it's identical to below? Is it worth keeping the .new
call around and just deleting the assert_raises
?
|
||
it "does do recursive type-checking of arrays with `check_type_recursive!`" do | ||
assert_raises(TypeError) do | ||
T.check_type_recursive!([1], T::Array[String]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah seeing this twice, I'm leaning towards at least putting this in T::Utils
or something, and not putting it on T
directly, because I think we don't really want to encourage people to depend on this accidentally.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that for now I'd advocate for T::Utils
, but it depends on how heavily we think we'll remove this entirely in the future. I think it makes some sense to keep it around, but if we're otherwise pretty sure it'll be removed at some point, then maybe T::Private
is the right place for it?
end | ||
|
||
true | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Feel free to push back here. Do you think this method could be improved by implementing it something like
valid?(obj) && <logic for recursively_valid>
?
(motivation: looks like a lot of duplication with valid?
)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was going to push back for performance concerns (since it'll re-traverse the hash twice, once for valid?
and once for recursively_valid?
) but on reflection, if you're using recursively_valid?
, then you're already throwing perf out the window, so I'll do this refactor.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some small suggestions but I love the new overall direction.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems to make the ::Untyped
variants of TypedArray, TypedHash etc mostly redundant. Maybe not for this PR, but should we follow up and remove them?
# @override Base | ||
def recursively_valid?(obj) | ||
obj.is_a?(Array) && obj.length == @types.length && | ||
obj.zip(@types).all? {|item, type| type.recursively_valid?(item)} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Might want to rebase after #3302 is merged
@@ -16,6 +16,13 @@ def name | |||
"{#{@types.map {|(k, v)| "#{k}: #{v}"}.join(', ')}}" | |||
end | |||
|
|||
# @override Base | |||
def recursively_valid?(obj) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ditto, might want to rebase after #3302
@@ -42,6 +42,11 @@ def name | |||
end | |||
end | |||
|
|||
# @override Base | |||
def recursively_valid?(obj) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems like the non-enumerable variants of recursively_valid?
don't have any test coverage - am I missing something?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right—I've gone ahead and updated the types
test now to test not only both code paths but ensure that the returned error messages are identical in every test where we'd expect them to be the same.
af809ab
to
456ecd6
Compare
@jez I've made a few changes and so for thoroughness I figured I'd request a re-review. The changes since the last one are
|
I assume this is at the request of kv-client, right? would you mind adding a note above the |
it 'fails if an element of the array is the wrong type under deep checking' do | ||
type = T::Array[Integer] | ||
value = [true] | ||
msg = type.error_message_for_obj_recursive(value) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks!
@@ -15,10 +15,15 @@ def name | |||
end | |||
|
|||
# @override Base | |||
def valid?(obj) | |||
def recurisvely_valid?(obj) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typo?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oof, yes. …apparently not only do we not test that, we must not use it anywhere in pay-server, either.
Motivation
This adds an extra parameter to
valid?
that defaults tofalse
which governs whether it recurses into generic collection types for runtime type-checking. This allows us to avoid it by default (avoiding a big source of slowdown) but still do it if we need to, and opens up the possibility of future features which can "opt-in" to recursive checking for extra safety.This also adds the
deep
parameter toassert_type!
; methods which rely on "deep" runtime checking can retain their old externally-visible runtime behavior by using something like this:A standing question is whether we want to enable this by default for sigs using e.g. a construct like
.checked(:deep)
. Also, should thedeep
parameter be called something likerecursive
?Test plan
Updated tests to include both recursive and non-recursive variants of the same behavior. TODO: test over pay-server.
See included automated tests.