-
Notifications
You must be signed in to change notification settings - Fork 14
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
Union Type Confusion #71
Comments
This might be a bit trickier than I thought, e.g. which of these is coerced to? Maybe the user would need to supply the type of the struct in order for something like this to work: class Test < T::Struct
const :key, String
const :value, String
end
class Test1 < T::Struct
const :key, String
const :value, String
const :other, String
end |
Hi @billy1kaplan, thanks for submitting an issue! I'll write out this response with the caveat that I'm only interning at @chanzuckerberg and am not the original creator of this repo. First, to get it out of the way, I can replicate this locally: class TestClass < T::Struct
const :a, String
end
TypeCoerce[T.any(String, TestClass)].new.from({
a: 'hello'
})
# TypeError: T.let: Expected type T.any(String, TestClass), got type Hash with value {:a=>"hello"} However, this is actually a bit complicated! If you're alright with it, join me on a deeper dive!
Ah, this is - it just hasn't been documented particularly well. If you peek at the code for converting a sorbet-coerce/lib/sorbet-coerce/converter.rb Lines 61 to 83 in f6f6659
The only "explicitly" supported unions are of types In particular, the way the code is written:
So, in your case, it picks the first type and converts to Note that this still fails if the union is part of the struct: class TestClass < T::Struct
const :a, String
end
class WithUnsupportedComplexUnion < T::Struct
const :union, T.any(String, TestClass)
end
TypeCoerce[WithUnsupportedComplexUnion].new.from({
union: {
a: 'hello'
}
})
# TypeError: Parameter 'union': Can't set WithUnsupportedComplexUnion.union to {:a=>"hello"} (instance of Hash) - need a T.any(String, TestClass) What I can definitely do is:
That's a great question. Right now, I don't know, though I'm happy to dig in a bit more if I have some free time. Of course, a PR is welcome if you find a way to make this work (even only for situations like your first example; the second can be "undefined behaviour" to some extent). The first step would be to relax the assumption (in the above code) that unions are only nilable or booleans, and add some more control flow logic. The I'm also not sure if I'm certainly no expert in this / safe_type (an underlying gem we use) / sorbet-runtime, and I could be completely wrong. @donaldong and @datbth implemented this original feature - if they have any thoughts, feel free to chime in! |
@mattxwang thanks!
+1. However this does not match what the README page says:
I haven't looked at the code for a while, but it feels doable to me. With class Test < T::Struct
const :key, String
const :value, String
end
class Test1 < T::Struct
const :key, String
const :value, String
const :other, T.nilable(String)
end should we expect the following? TypeCoerce[T.any(String, Test)].new.from(
"Hi",
) # => "Hi" Can be casted to String already, return String. TypeCoerce[T.any(Test1, Test)].new.from(
{
"key" => "Hello",
"value" => "World",
},
) # => Test1 Cast to Test1 first, because that's the first valid try TypeCoerce[T.any(Test, Test1)].new.from(
{
"key" => "Hello",
"value" => "World",
},
) # => Test Cast to Test first, because that's the first valid try Similarly, |
Thanks @donaldong for the clarification! I think what you proposed makes sense. It does seem like the types in
Should I update the docs? I think that was merged in by #54. @billy1kaplan any thoughts? If you'd like to contribute, I think this is a great opportunity to do so (and I can review your code, etc. if it's in the next 5 weeks). If not, no worries - I can take a stab at it. |
@donaldong @mattxwang thank you both for the detailed replies! I'm happy to take a shot at this hopefully within the next week or so. My understanding of a possible approach would be to try coercing to the types defined by |
Great! Let me know how I can support you - happy to lend a hand however I can.
That sounds right to me! |
We implemented a monkey patch for this based on version 0.5.0. It caught the "old" ArgumentError. Not a great approach but it worked. Now that it is a TypeError nested Union types don't seem to work so great anymore. I also submitted #72 that illustrates the error. |
I think automatically trying to coerce union types is indeed convenient.
The performance cost is the main concern to me. |
Thanks @datbth. @billy1kaplan has done some work in #73, I think it's reasonable to add a toggle (since this is a breaking change). Do you have any additional context on the original PRs that you wrote with Donald? (for context: wrapping up an internship at CZI and doing some maintenance for some infra repos on the side) |
Context of #53Okay let me try to recall the context of #53. class Owner < T::Struct
const :name, String
const :pet_id, T.any(Integer, String)
end
owner_data = {
name: 'Alice',
pet_id: 'abcdef',
}
TypeCoerce[Owner].new.from(owner_data) Then I got the error
I don't remember exactly how nested So by implementing #53, I was trying to achieve:
Some additional comments on coercing union typesI myself would try to stay away from actually coercing union types because:
One example for the tricky behavior issue: class Cat < T::Struct
const :name
end
class Dog < T::Struct
const :name
end
class Owner < T::Struct
const :name, String
const :pet, T.any(Cat, Dog)
end
data = {
name: 'Alice',
pet: {
type: 'dog',
name: 'Goofy',
}
}
TypeCoerce[Owner].new.from(data) In this case, even though the pet data is coercible into For the case where the user explicitly tries to coerce a union type (e.g. the union type is the root type) like |
That is great context, thank you @datbth. I don't have too much context into the vision of this gem (we still use it at CZI, but much of that is out of scope of my direct work). I can do two things:
|
Describe the bug:
I would expect the following to be a "coercable" union type:
Actual result:
This does seem to work with the other half of the union type (i.e. for a String) where we're able to return a String directly:
Expected behavior:
Naively, I would expect the above to return the
Test
struct with the properly initialized values. Is this a known limitation? And would this be a possible feature to support?Versions:
The text was updated successfully, but these errors were encountered: