-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
Fix the return type of Enumerable#sum/product
for union elements
#15314
base: master
Are you sure you want to change the base?
Conversation
This PR expands on the (accidentally closed) PR 15308. Tagging @Blacksmoke16, @straight-shoota, and @BlobCodes to continue the discussion here. |
Using the first type of a union type as the type of the result of `Enumerable#sum/product()` call can cause runtime failures, e.g. `[1, 10000000000_u64].sum/product` will result in an ``OverflowError``. A safer alternative is to flag/disallow the use of union types with `Enumerable#sum/product()` and recommend the use of `Enumerable#sum/product(initial)` with an initial value of the expected type of the sum/product call.
src/enumerable.cr
Outdated
raise("Enumerable#sum/product() does support Union types. " + | ||
"Instead, use Enumerable#sum/product(initial) with an " + | ||
"initial value of the expected type of the sum/product call.") |
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.
suggestion: Polish the error message:
raise("Enumerable#sum/product() does support Union types. " + | |
"Instead, use Enumerable#sum/product(initial) with an " + | |
"initial value of the expected type of the sum/product call.") | |
raise("`Enumerable#sum` and `#product` cannot determine the initial value from a union type. " + | |
"Please pass an initial value of the intended type to the call.") |
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.
After reading your fix, I realized a missing negation in the first sentence of error message I committed :)
Also, how about we update the error message as follows?
raise("Enumerable#sum` and #product do not support union types. Instead, use Enumerable#sum/product(initial) with an initial value of the expected type of the sum/product call.")
Following is my reasoning for the above change.
- The existing logic fails to identify an appropriate/safe type that can hold the final value of the call.
- Using the identity is a way of determining the appropriate/safe type. Since it is an implementation detail, I think surface such details in documentation while mentioning what (not how) is wrong and its fix in the error message may be better.
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 not support union types" isn't entirely accurate. They do support them. Just without automatically determining the return type, hence the need to specify explicitly.
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.
With this change, "sum()" and "product()" will not support union types (but "sum(initial)" and "product(initial)" will support union types). So, do you mean that we should be precise in the error message and the doc by referring to the no-arg variant of the methods?
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.
Yes, we could differentiate between the different overloads by signature.
Enumerable#sum()
and#product()
do not support union types.`
It's a bit of a subtle detail though. Hence my suggestion to write out the issue explicitly:
Enumerable#sum
and#product
cannot determine the initial value from a union type.
I'm fine either way though.
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 have updated the PR such that
- error messages and doc refer to specific overloads and
- error messages focus only on the what is wrong and how to fix it.
The error message and documentation specifically refers to the variants of Enumerable#sum/product that do not support union types.
src/enumerable.cr
Outdated
raise("Enumerable#sum() and #product() do not support Union types. " + | ||
"Instead, use Enumerable#sum(initial) and #product(initial), " + |
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.
Stylistic suggestion:
raise("Enumerable#sum() and #product() do not support Union types. " + | |
"Instead, use Enumerable#sum(initial) and #product(initial), " + | |
raise("`Enumerable#sum` and `#product` do not support Union types. " + | |
"Instead, use `Enumerable#sum(initial)` and `#product(initial)`, " + |
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 can make this change.
Out of curiosity, assuming the back ticks are Markdown's code syntax, what is the effect of including them in in error messages (which I assume are processed and rendered as plain text)?
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 my eyes it makes them more standout and a clear sign we're referencing the method/type definitions, as in the other, doc-related places.
This change is rather ill-placed, given it modifies the internal |
@Sija can you please elaborate on "this change is rather ill-placed'? |
@rvprasad As I've already mentioned, this change modifies the internal |
@Sija I see what you mean. Here are a few thoughts and observations.
While I realize surfacing more issues is not ideal, I hope they can help clear up a small corner of the language. |
I don't think this change is ill-placed. The purpose of I believe the approach taken here to handle this case explicitly with a custom error message is better. The specific implementation such as the method name of the internal helper can be adjusted easily. Any issue with the yielding overloads |
With this PR, Reflect is not used to determine the first type of a union type. This commit aligns Reflect's method and related comments with the class' purpose.
I have pushed a revision that updates the implementation and comments in Regarding the yielding overloads, I did not realize that, during type coercion, Crystal uses the type of expression based on the type of the first sub-expression of the expression, i.e., typeof(1 * 1u64) is Int32 while typeof(1u64 * 1) is UInt64. On a related note, I believe Furthermore, when I tried to use
|
With regards to |
While Also, this observation is true of heterogeneous arrays, i.e., arrays whose element type is a union type. Edit1: I have captured this observation in issue #15317. |
Assuming we can pursue other issues in separate dedicated tickets, are there any objections to the proposed change to address the primary issue captured in this ticket? |
@straight-shoota thanks for the approval! Do I need to do anything more for these changes to be merged? |
Using the first type of a union type as the type of the result of
Enumerable#sum/product()
call can cause runtime failures, e.g.[1, 10000000000_u64].sum/product
will result in anOverflowError
. A safer alternative is to flag/disallow the use of union types withEnumerable#sum/product()
and recommend the use ofEnumerable#sum/product(initial)
with an initial value of the expected type of the sum/product call.