Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions .claude/skills/write-pattern/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,6 @@ Add the pattern to the `nav` section in `mkdocs.yml`. Categories are listed alph
- Pattern Name: 'category-name/pattern-name.md'
```

Also update the `llmstxt` `sections` block in `mkdocs.yml` to include the new category. Keep the sections in alphabetical order. If adding a pattern to an existing category that's already listed, no change is needed.

### Cross-references

When the new pattern relates to existing patterns, add links in both directions. The new pattern should link to the related ones in its Discussion, and the related patterns should be updated to mention the new one.
Expand Down
1 change: 1 addition & 0 deletions .cspell/project-words.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ backpressure
MessagePack
msgpack
RESP
signedness
unrepresentable
Wadler
Zulip
84 changes: 84 additions & 0 deletions docs/generics/generic-numeric-code.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
---
hide:
- toc
---

# Generic Numeric Code

## Problem

You want to write a class that works with any unsigned integer type. A block cipher component, say, that stores a value and provides parity checking and XOR operations. Your first instinct might be to use `Unsigned` as the type constraint:

```pony
class Block[T: Unsigned]
var data: T

new create(data': T) =>
data = data'

fun parity(): Bool =>
(data.popcount() % 2) == 1

fun ref apply_xor(o: Block[T]) =>
data = o.data xor data
```

This doesn't compile. You'll get errors on the `%`, `==`, and `xor` operations, plus the numeric literals `2` and `1`. The constraint looks right, but `Unsigned` alone isn't enough to make generic numeric code work in Pony.

## Solution

Two things need to change. First, the type constraint needs to be `(Unsigned & UnsignedInteger[T])` instead of just `Unsigned`. Second, numeric literals need to be wrapped with `T.from[U8](n)`.

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`.
Comment on lines +32 to +34

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.


```pony
class Block[T: (Unsigned & UnsignedInteger[T])]
```

The second fix is numeric literals. Pony can't create a number literal for a generic type. When the compiler sees `2`, it needs to know the concrete type right then. In non-generic code that's inferred from context, but inside a generic function the compiler only knows `T`, not whether it's `U8` or `U64`.

The workaround is `T.from[U8](n)`. This tells the compiler the literal `n` is a `U8`, then converts it to whatever `T` turns out to be:

```pony
fun parity(): Bool =>
(data.popcount() % T.from[U8](2)) == T.from[U8](1)
```

Since we're dealing with constant values, LLVM trivially optimizes away the conversion. There's no runtime cost.

Putting it all together:

```pony
class Block[T: (Unsigned & UnsignedInteger[T])]
var data: T

new create(data': T) =>
data = data'

fun parity(): Bool =>
(data.popcount() % T.from[U8](2)) == T.from[U8](1)

fun ref apply_xor(o: Block[T]) =>
data = o.data xor data

actor Main
new create(env: Env) =>
let a = Block[U32](0b1011)
let b = Block[U32](0b1100)
env.out.print("a parity (odd number of 1s): " + a.parity().string())
env.out.print("b parity (even number of 1s): " + b.parity().string())
```

You can instantiate `Block[U8]`, `Block[U32]`, `Block[U128]`, or any other concrete unsigned integer type, and the compiler generates specialized code for each.

## Discussion

The same pattern works for signed integers and for all integers. For signed types, use `(Signed & Integer[T])`. For code that works with any integer regardless of signedness, use `(Int & Integer[T])`. You might expect `SignedInteger[T]` to mirror `UnsignedInteger[T]`, but `SignedInteger` takes two type parameters (the signed type and its unsigned counterpart), making it awkward to use in constraints. `Integer[T]` is simpler and is what the standard library uses. The standard library follows this convention throughout. `Format.int` uses `[A: (Int & Integer[A])]`, and `String.read_int` uses `[A: ((Signed | Unsigned) & Integer[A] val)]`. Whenever you see a generic function constrained to a numeric type in the standard library, you'll find this intersection pattern.

The reason `Unsigned` alone doesn't work comes down to how Pony's type system handles union types as constraints. A constraint like `[T: Unsigned]` means `T` can be any subtype of `Unsigned`. Since `Unsigned` is a union, `(U8 | U16)` is a valid subtype of it, so `Block[(U8 | U16)]` would be a legal instantiation. With a union as `T`, binary operations between two values of type `T` could end up mixing a `U8` and a `U16`, which Pony doesn't allow. The `UnsignedInteger[T]` trait constrains `T` to a single concrete type that supports operations with itself, ruling out union instantiations.

The `T.from[U8](n)` pattern for literals deserves a closer look. `U8` is the natural choice for the source type because it's the smallest unsigned integer, and numeric literals used in generic code tend to be small constants (0, 1, 2, bitmasks). You could use any integer type as the source, but `U8` keeps the intent clear: this is a small constant being widened to whatever `T` is. The `from` method is part of the `Integer` trait, so it's available on any type that satisfies the constraint.

If you're reaching for generic numeric code because of performance concerns around boxing, the [Avoid Boxing with Parameterization](../performance/avoid-boxing.md) pattern covers that angle. Boxing happens when a primitive value gets passed where any type is expected (`Any val` or a union parameter), forcing a heap allocation. Using type parameters avoids boxing because the compiler knows the concrete type at each call site. The constraint patterns described here and in that pattern are the same; the difference is the motivation. Here the focus is on making generic numeric code compile correctly. There the focus is on eliminating unnecessary heap allocations.
8 changes: 8 additions & 0 deletions docs/generics/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
hide:
- toc
---

# Generics Patterns

Pony's generics let you write code that works across multiple types without duplicating logic. But the type system has rules that aren't always obvious, especially around numeric types and capabilities. These patterns cover the common situations where a straightforward generic approach doesn't compile and show you how to fix it.
2 changes: 2 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ Patterns are organized by the kind of problem they solve.

**[Error Handling Patterns](error-handling/index.md)** go beyond Pony's built-in `error` keyword. Union types give you richer error handling where the compiler helps you cover every case.

**[Generics Patterns](generics/index.md)** cover the situations where a straightforward generic approach doesn't compile. Pony's type system has rules around numeric types and capabilities that aren't always obvious, and these patterns show you how to work with them.

**[Object Capabilities Patterns](object-capabilities/index.md)** show how to use Pony's object capability system to control what parts of your program can do. Authority hierarchies, single-use capabilities, and the discipline that makes capability security practical.

**[Performance Patterns](performance/index.md)** are for when you need to squeeze out more speed. Avoiding boxing, short-circuiting, preallocating, and keeping string allocations under control.
Expand Down
2 changes: 2 additions & 0 deletions docs/performance/avoid-boxing.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,5 @@ actor Collector[A: Any val]
Now `Collector[U64]` stores unboxed `U64` values, and `Collector[String]` stores `String` references. Each instantiation is specialized to its type. The trade-off is that a single collector instance can only hold one type, but in practice that's usually what you want. Code that genuinely needs mixed types can still use `Any val`, paying the boxing cost only where heterogeneity is actually needed.

The standard library uses this pattern in several places. `Format.int` is the most direct example, with the same `[A: (Int & Integer[A])]` constraint shown in the Solution above. `String.read_int` uses `[A: ((Signed | Unsigned) & Integer[A] val)]` to parse an integer from a string into whatever concrete type the caller requests. The `math` package's `GreatestCommonDivisor` and `LeastCommonMultiple` both parameterize their `apply` methods over integer types. Whenever you find yourself reaching for `Any val` or a match across numeric types, check whether a type parameter can do the job instead.

If you're writing generic code over numeric types and running into compile errors around binary operations or numeric literals, the [Generic Numeric Code](../generics/generic-numeric-code.md) pattern explains how to get the type constraints right.
7 changes: 5 additions & 2 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,14 @@ plugins:
- domain-modeling/*.md
Error Handling Patterns:
- error-handling/*.md
Generics Patterns:
- generics/*.md
Object Capabilities Patterns:
- object-capabilities/*.md
Performance Patterns:
- performance/*.md
Resource Management Patterns:
- resource-management/*.md
Streaming Patterns:
- streaming/*.md
Testing Patterns:
- testing/*.md

Expand Down Expand Up @@ -136,6 +136,9 @@ nav:
- Error Handling Patterns:
- Overview: 'error-handling/index.md'
- Error as Union Type: 'error-handling/error-as-union-type.md'
- Generics Patterns:
- Overview: 'generics/index.md'
- Generic Numeric Code: 'generics/generic-numeric-code.md'
- Object Capabilities Patterns:
- Overview: 'object-capabilities/index.md'
- Authority Hierarchy: 'object-capabilities/authority-hierarchy.md'
Expand Down
Loading