Problem: Union types in generics require same concrete type
When you define a generic with a union type like T: int|Nullopt_t, Nim requires ALL parameters of that type to be the SAME concrete type:
template handleNegativeIndex[T: int|Nullopt_t](idx: T, axisLen: int): T =
when idx is Nullopt_t:
idx
else:
if idx < 0:
idx + axisLen
else:
idx
- Calling
handleNegativeIndex(start, len)withstart: intandnulloptforstopfails:intandNullopt_tare different types even though both belong to the union.
Solution: Use distinct type
Define a distinct wrapper type that "unifies" the union:
type OptInt* = distinct int | Nullopt_t
template handleNegativeIndex*[T: int|Nullopt_t](idx: T, axisLen: int): T =
when idx is Nullopt_t:
idx
else:
if idx < 0:
idx + axisLen
else:
idx
func normalizedSlice*(
start, stop: distinct OptInt,
step: OptInt = nullopt, axisLen: int): TorchSlice {.inline.} =
let normStart = handleNegativeIndex(start, axisLen)
let normStop = handleNegativeIndex(stop, axisLen)
torchSlice(normStart, normStop, step)
distinct creates a new type with these effects:
- Is compatible with all types in the union at runtime
- Allows parameters to have DIFFERENT concrete types from the same union
- Preserves type safety while enabling flexible APIs
When to use this pattern
- API functions that accept either a value OR "none"/"default"
- Slice/indexing functions where parameters can be int or nullopt
- Callbacks that may receive typed or untyped values
Related patterns
option[T]from stdlib for explicit optional valuesnulloptsingleton for "no value provided"when defined(T)branches for type-specific logic