Conversation
Addresses the common question of how to write Pony code that's generic over numeric types. Covers why `Unsigned` alone isn't sufficient as a type constraint (it's a union, allowing instantiation with e.g. `(U8 | U16)`), why the intersection `(Unsigned & UnsignedInteger[T])` is needed, and how to handle numeric literals in generic context with `T.from[U8](n)`. Also adds the "Generics" category, fixes a pre-existing gap where Streaming Patterns was missing from the llmstxt sections config, and adds a cross-reference from the Avoid Boxing pattern. Closes #46
b0842c2 to
c61a61d
Compare
| Let's start with the constraint. `Unsigned` in Pony is a union type containing all the built-in unsigned integers: `U8`, `U16`, `U32`, `U64`, `U128`, `ULong`, and `USize`. When you use only this union as a constraint, the compiler has to consider that `T` could be instantiated with a union like `(U8 | U16)`. That's a problem because you can't XOR a `U8` with a `U16`. Binary operations need both operands to be the same type. | ||
|
|
||
| Adding `UnsignedInteger[T]` to the constraint fixes this. `UnsignedInteger` is a trait that guarantees its type parameter can do binary operations, arithmetic, and comparisons with another value of the same type. By constraining `T` to be both `Unsigned` and `UnsignedInteger[T]`, you're telling the compiler: `T` must be one of the concrete unsigned integer types, and it must support operations with other values of exactly type `T`. |
There was a problem hiding this comment.
This explains why the union is insufficient, and the trait is necessary, but a reader might then wonder why the trait is insufficient, and the union is necesary.
The answer boils down to: Pony only accepts built-in numeric types for number literals in source code, so we need the type union part of the constraint to ensure that number type is one of the known built-in numeric types.
There was a problem hiding this comment.
Actually as we continued discussing, I'm unsure about why the type union is required - the example in this pattern compiles without it.
Also I went and checked other examples from other Zulip threads which all compile without a trait-only constraint (provided that the trait gets suffixed with val, which was implicit before when it was intersected with a val-only union).
I seem to remember that unions are required in some cases, but I don't know when/where/why.
Maybe you/Claude could try looking at stdlib places where the type union is part of the constraint, and try removing it, and seeing if the code can be made to work without that.
If we have any places that work fine without the union being part of the constraint, I believe it should be removed, because it's better to allow for as many safe cases as possible, loosening the constraint where we can.
If there are places that don't work without the union, we should use one as an example to explain here in the pattern, so that others including me will know when/why it's needed.
Adds a new "Generics" pattern category with a "Generic Numeric Code" pattern that explains how to write Pony code generic over numeric types. This is a common stumbling block — using
Unsignedalone as a type constraint doesn't work because it's a union type, and numeric literals need special handling in generic context.The pattern covers the
(Unsigned & UnsignedInteger[T])constraint,T.from[U8](n)for literals, and extends to signed/all-integer variants. Cross-references the existing "Avoid Boxing with Parameterization" pattern (which covers the same constraints from a performance angle).Also fixes a pre-existing gap where Streaming Patterns was missing from the llmstxt sections config.
Closes #46