Description
Enumerable(T)#sum
and #product
usually use the element type (T
) as the type of the sum/product unless specified.
This is usually a good default but raises the question what should happen when the element type is not a single type, but a union?
Turns out, the implementations picks the first type in the union (note: order of types in a union is lexicographic by the type name):
typeof([1_i8, 2_i16].sum) # => Int8
I think this is quite unexpected and can lead to surprising behaviour: For example [1_i16, 12345678_i32].sum
raises an overflow error when the result would fit perfectly into Int32
.
If the implementation picks one type from the union, I figure it should rather be the biggest type of the union. For number types this might be relatively easy, but not without issues (e.g. when combinging signed and unsigned types of the same width). And there would be no way this could work for custom types. I don't have a real use cases for math operations on unions outside of Number
, but there is probably some with related values types.
In my opinion, it would probably be best to not automatically pick a type if there are multiple options. It's better to make this a compile time error instead of running into weird overflows at runtime because the sum type is smaller than expected.
It's trivial to specify the desired target type by passing an initial value in that type.
For example:
[1_i16, 12345678_i32].sum(0_i32) # => 12345679
Of course this would be a breaking change. But IMO that's acceptable because it corrects a confusing behaviour. This is a rarely used feature anyway, so impact should be minimal.
This repo doesn't seem to use this and test-ecosystem doesn't show anything breaking either: https://github.com/crystal-lang/test-ecosystem/actions/runs/12285549227/job/34286237286
This issue was originally reported at https://fosstodon.org/@[email protected]/113626739326812835