https://github.com/emmt/neutrals.jl
Universal neutral numbers of the addition and multiplication of numbers in Julia.
https://github.com/emmt/neutrals.jl
Last synced: 4 months ago
JSON representation
Universal neutral numbers of the addition and multiplication of numbers in Julia.
- Host: GitHub
- URL: https://github.com/emmt/neutrals.jl
- Owner: emmt
- License: mit
- Created: 2025-05-14T18:26:59.000Z (about 1 year ago)
- Default Branch: main
- Last Pushed: 2025-12-19T17:05:05.000Z (6 months ago)
- Last Synced: 2025-12-22T06:53:21.019Z (6 months ago)
- Language: Julia
- Size: 87.9 KB
- Stars: 3
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Changelog: NEWS.md
- License: LICENSE.md
Awesome Lists containing this project
README
# Neutrals
[](https://github.com/emmt/Neutrals.jl/actions/workflows/CI.yml?query=branch%3Amain)
[](https://ci.appveyor.com/project/emmt/Neutrals-jl)
[](https://juliahub.com/ui/Packages/General/Neutrals)
[](https://codecov.io/gh/emmt/Neutrals.jl)
[](https://juliahub.com/ui/Packages/General/Neutrals?t=2)
[](https://github.com/JuliaTesting/Aqua.jl)
This package provides two singleton values, `๐` and `๐` which are the respective *neutral
elements* for the addition and multiplication of numbers regardless of their types. In
other words, whatever the type and value of the number `x`, `๐ + x` and `๐*x` yields `x`
unchanged and without computations. Hence, `๐ + x === x` and `๐*x === x` hold even though
`x` is not an instance of `isbitstype` like `BigInt` or `BigFloat`. Besides, `๐` is a
so-called *strong zero* which means that `๐*x` always yields `๐` without computations. In
particular, `๐*Inf` and `๐*NaN` both yield `๐`. Since `๐` and `๐` are singletons, their
specific behaviors in arithmetic operations is inferable at compile time and can result in
valuable optimizations.
Consistent rules for the subtraction and division follow from the rules for the addition
and multiplication with `๐` or `๐`. For example, `-๐`, the opposite of `๐`, is also a
singleton whose effect in a multiplication is to negate the other operand: `(-๐)*x` always
yields `-x`.
Table of contents:
* [Compatibility](#compatibility)
* [Binary Operations](#binary-operations)
* [Addition](#addition)
* [Subtraction](#subtraction)
* [Multiplication](#multiplication)
* [Division](#division)
* [`div`, `rem`, and `mod`](#div-rem-and-mod)
* [Bitwise Binary Operations](#bitwise-binary-operations)
* [Bit-shift Operations](#bit-shift-operations)
* [Comparisons](#comparisons)
* [Conversion Rules](#conversion-rules)
* [Broadcasting Rules](#broadcasting-rules)
* [Ranges](#ranges)
* [Dimensionful Quantities](#dimensionful-quantities)
* [Miscellaneous](#miscellaneous)
* [Related packages](#related-packages)
## Compatibility
Before version 1.3 of Julia, `๐` and `๐` cannot be used as names of constants, the aliases
`ZERO` and `ONE` can be used instead.
## Binary operations
This section describes the rules involving a neutral number and any other number. For
[commutative operations](https://en.wikipedia.org/wiki/Commutative_property) like the
multiplication (`*`), the addition (`+`), binary bitwise operations (`|`, `&`, and `xor`
or `โป`), and the comparison for equality (`==`), the same rules apply if the operands are
exchanged.
### Addition
The following rules apply for the addition involving a neutral number and any
dimensionless number `x`:
``` julia
x + ๐ -> x
x + ๐ -> x + one(x)
x + (-๐) -> x - one(x)
```
The result of an addition with a neutral number has the same type as `x`, except if `x` is
a Boolean and the neutral number is `ยฑ๐` which yield an `Int` (as does the addition of
Booleans in Julia).
### Subtraction
The rules for the subtraction involving a neutral number and any dimensionless number `x`
follow from those of the addition:
``` julia
x - ๐ -> x
๐ - x -> -x
x - ๐ -> x - one(x)
๐ - x -> one(x) - x
x - (-๐) -> x + one(x)
(-๐) - x -> -one(x) - x
```
### Multiplication
The following rules apply for the multiplication of a neutral number and a number `x`:
``` julia
๐*x -> ๐ # if `x` is dimensionless
๐*x -> ๐*unit(x) # if `x` is dimensionful
๐*x -> x
-๐*x -> -x
```
If `x` is dimensionful, the result has the same dimensions as `x`. For example:
``` julia
julia> using Neutrals, Unitful.DefaultSymbols
julia> ๐*3
๐
julia> ๐*(3kg)
๐ kg
```
### Division
The rules for the division involving a neutral number and any number `x` follow from those
of the multiplication:
``` julia
๐/x -> ๐ # if `x` is dimensionless
๐/x -> ๐/unit(x) # if `x` is dimensionful
๐/x -> inv(x)
-๐*x -> -inv(x)
x/๐ -> DivideError
x/๐ -> x
x/-๐ -> -x
```
### `div`, `rem`, and `mod`
Similar rules are implemented for the quotient and remainder of the truncated division
(`div` or `รท` and `rem` or `%`) and for the modulo (`mod`). In Julia, for `x` and `y`
integers `div(x, y)` and `rem(x, y)` yield a result of the signedness of `x`, while
`mod(x, y)` yields a result of the signedness of `y`. This rule is preserved when one of
the operand is a neutral number, considering that neutral numbers are signed integers.
For `div`, `rem`, and `mod` when one operand is a Boolean and the other is a neutral
number the behavior implemented in Julia for Booleans is reflected. This implies that the
neutral number be converted into a `Bool`. Hence, if the neutral operand is `-๐`, an
`InexactError` is thrown.
### Bitwise Binary Operations
In binary bitwise operations `|`, `&`, and `xor` (also denoted `โป`) between an integer `i`
and a neutral number `n`, the implemented rules are such that the result is as if `๐` and
`๐` are converted to the type of `i` while `-๐` is assumed to represent a bit mask of the
same type as `i` with all bits set to `1`, that is `~zero(i)`. For a given binary bitwise
operation denoted by `โ`, this corresponds to the following rules:
``` julia
i โ ๐ -> i โ zero(i)
i โ ๐ -> i โ one(i)
i โ -๐ -> i โ ~zero(i)
```
These rules may be optimized in the implementation. For example:
``` julia
i | ๐ -> i
i | -๐ -> ~zero(i)
i & ๐ -> zero(i)
i & -๐ -> i
i โป ๐ -> i
```
It may be noted that, `i & ๐` yields `zero(i)` and not `๐` as would do `i*๐`. This is
because `๐` is defined relatively to the addition and the multiplication (`+` and `*`),
not the *bitwise-and* operation (`&`).
### Bit-shift Operations
In Julia, bit-shifting integer `x` by `n` bits yields a result of the same type as `x`
except for Booleans for which the result is an `Int`. With the `Neutrals` package, if `n`
is a neutral number (`๐`, `๐`, or `-๐`), the following rules are implemented:
``` julia
x << ๐ -> x
x << ๐ -> x << UInt(1)
x << -๐ -> x >> UInt(1)
x >> ๐ -> x
x >> ๐ -> x >> UInt(1)
x >> -๐ -> x << UInt(1)
x >>> ๐ -> x
x >>> ๐ -> x >>> UInt(1)
x >>> -๐ -> x << UInt(1)
```
These rules provide two optimizations: bit shifting `x` by `๐` bits leaves `x` unchanged,
while bit shifting `x` by `ยฑ๐` bit shifts `x` by one bit in the correct direction where
`UInt(1)` is to dispatch on the type of `x` not on that of the number of bits. This
closely reflects the behavior implemented in `base/int.jl` except that bit-shifting by `๐`
always yields the left operand unchanged even though it is a Boolean.
### Comparisons
When comparing values with `==`, `<`, `<=`, `isequal`, `isless`, and `cmp`, the rule of
thumb is that the behavior shall reflect the expression. This poses no problem for `๐` and
`๐` which are representable by any numeric type. This is not the case of `-๐` which
cannot be simply converted to a Boolean, an unsigned number (integer, rational, or
complex).
If `u` is an unsigned number, the following identities hold:
``` julia
u == -๐ -> false
u != -๐ -> true
isequal(u, -๐) -> false
```
Of course, these binary operators being symmetric, their result does not depend on the
order of the arguments.
Furthermore, if `u` is an unsigned real (i.e., not a complex), then:
``` julia
u < -๐ -> false
u โค -๐ -> false
u > -๐ -> true
u โฅ -๐ -> true
isless(u, -๐) -> false
isless(-๐, u) -> true
cmp(u, -๐) -> 1
cmp(-๐, u) -> -1
```
## Conversion Rules
As for other numbers, a neutral number `n` (`๐`, `๐`, or `-๐`) can be converted into a
numeric type `T` by `T(n)` which yields a value of type `T`. This operation is always
successful for `๐` and `๐` which are representable by any numeric type. For `-๐`, an
`InexactError` exception is thrown if `T` is not a signed type, this includes Booleans,
unsigned integers, but also rationals and complexes with Boolean or unsigned parts. As for
any non-big integer, `AbstractFloat(n)` and `float(n)` both yield `n` converted to
`Float64`.
The expression `n % T` can also be used to *convert* a neutral number `n` to an integer
type `T` modulo the number of integers representable in `T`. In this case, `-๐ % T` works
even though `T` is unsigned. For example:
``` julia
julia> -๐ % UInt16
0xffff
```
The method `convert(T, n)` with `T` a numeric type and `n` a neutral number amounts to
calling `T(n)`. As a result, a neutral number `n` is automatically converted to a value of
type `T` when stored in an array whose elements are of type `T` or when assigned to a
field of type `T` in a mutable structure. For example, assuming `x` is an array of
numbers, it can be zero-filled by:
``` julia
for i in eachindex(x); x[i] = zero(eltype(x)); end
```
which, provided `eltype(x)` is dimensionless, can be written as:
``` julia
for i in eachindex(x); x[i] = ๐; end
```
or, better, as:
``` julia
for i in eachindex(x); x[i] *= ๐; end
```
which works whether `eltype(x)` is dimensionful or dimensionless.
## Broadcasting Rules
Some broadcasted operations involving a neutral number and a number or an array of numbers
`x` are optimized to return `x` unchanged:
``` julia
x .+ ๐ -> x # idem for ๐ .+ x -> x
x .- ๐ -> x
๐ .* x -> x # idem for x .* ๐
x ./ ๐ -> x
x .^ ๐ -> x
```
In addition, if `x` has integer element type, then:
``` julia
x .รท ๐ -> x
x .| ๐ -> x # idem for ๐ .| x
x .& (-๐) -> x # idem for (-๐) .& x
x .โป ๐ -> x # idem for ๐ .โป x -> x
x .<< ๐ -> x
x .>> ๐ -> x
x .>>> ๐ -> x
```
Other broadcasted operations should work as can be inferred from the rules for numbers.
For multiplying or dividing an array of numbers by neutral numbers, you may
directly use the `*`, `/`, or `\` operators instead of `.*`, `./`, or `.\`:
``` julia
๐*x -> similar(x, typeof(๐*unit(eltype(x))))
๐*x -> x
๐\x -> x
x/๐ -> x
(-๐)*x -> -x
(-๐)\x -> -x
x/(-๐) -> -x
```
Note that `๐*x` is a lightweight array (`sizeof(๐*x) = 0`) whose elements are all equal to
the singleton `๐` if `eltype(x)` is dimensionless or to the singleton `๐*unit(eltype(x))`
if `eltype(x)` is dimensionful (see [Dimensionful Quantities](#dimensionful-quantities)).
## Ranges
Ranges can be constructed with neutral numbers specified as the start, step, and/or stop
parameters of the range. `๐:stop` is identical to `Base.OneTo(stop)` if `stop` is a
non-neutral integer or is `๐` and is identical to `Base.OneTo(Int(stop))` otherwise.
`start:๐:stop` identical to `start:stop` whatever, `start` and `stop`. Examples:
``` julia
๐:6 -> Base.OneTo(6)
3:๐:6 -> 3:6
collect(๐:๐) -> [๐]
collect(๐:๐) -> [๐]
```
## Dimensionful Quantities
Neutral numbers can work with dimensionful numbers provided the `Neutrals` package be
properly extended for such numbers and provided the operation makes sense (e.g., adding
`๐` to a length in meters does not make sense because `๐` is dimensionless).
This is the case of the [`Unitful`](https://github.com/PainterQubits/Unitful.jl)
quantities. For example:
``` julia
using Unitful, Unitful.DefaultSymbols
x = 3kg
๐*x === ๐*unit(x) # true
๐*x === x # true
-๐*x == -x # true
x + ๐ # error, ๐ is dimensionless
x + ๐*unit(x) == x # true
x - ๐ # error, ๐ is dimensionless
x - ๐*unit(x) == x # true
๐*unit(x) == zero(x) # true
๐*unit(x) !== zero(x) # true
๐*unit(x) == oneunit(x) # true
๐*unit(x) !== oneunit(x) # true
-๐*unit(x) == -oneunit(x) # true
๐ == one(x) # true
๐ !== one(x) # true
```
Note that `๐*unit(x)` is equal but not identical to `zero(x)` because it is `๐` with the
unit of `x`.
## Miscellaneous
`Complex(x,y)` and `complex(x,y)` yield the same result as `x + y*im` even though `x` or
`y` is a neutral number.
## Macros
The macro `@dispatch_on_value sym expr` generates code that dispatches expression `expr`
based on the run-time value of the symbol `sym`.
For example:
```julia
function xpby!(dst::AbstractArray, x::AbstractArray, ฮฒ::Number, y::AbstractArray)
@assert axes(dst) == axes(x) == axes(y)
@dispatch_on_value ฮฒ unsafe_xpby!(dst, x, ฮฒ, y)
return dst
end
function unsafe_xpby!(dst::AbstractArray, x::AbstractArray, ฮฒ::Number, y::AbstractArray)
@inbounds @simd for i in eachindex(dst, x, y)
dst[i] = x[i] + ฮฒ*y[i]
end
nothing
end
```
Above, the `@dispatch_on_value ...` statement expands to (with comments removed):
```julia
if !Neutrals.is_static_number(ฮฒ) && Base.iszero(ฮฒ)
unsafe_xpby!(dst, ฮฑ, x, Neutrals.Neutral{0}()*TypeUtils.units_of(ฮฒ), y)
elseif !Neutrals.is_static_number(ฮฒ) && ฮฒ == Base.oneunit(ฮฒ)
unsafe_xpby!(dst, ฮฑ, x, Neutrals.Neutral{1}()*TypeUtils.units_of(ฮฒ), y)
elseif !Neutrals.is_static_number(ฮฒ) && TypeUtils.is_signed(ฮฒ) && ฮฒ == -Base.oneunit(ฮฒ)
unsafe_xpby!(dst, ฮฑ, x, Neutrals.Neutral{-1}()*TypeUtils.units_of(ฮฒ), y)
else
unsafe_xpby!(dst, ฮฑ, x, ฮฒ, y)
end
```
## Related Packages
- In base Julia, `false` behaves as a strong zero when multiplied by a float. Moreover it
preserves the sign of the other operand, e.g. `false*(-NaN)` yields `-0.0`. The sign is
not preserved in the multiplication by `๐` which yields `๐`.
- [`Zeros.jl`](https://github.com/perrutquist/Zeros.jl) was a source of inspiration to
improve `Neutrals.jl`. `Zeros.jl` provides `Zero()` and `One()` which are also strong
neutral elements for addition and multiplication with numbers. `Zero()` and `One()` are
similar to `๐` or `ZERO`, and `๐` or `ONE`. However, `-One()` yields `-1` which is not a
singleton, division by `One()` converts the other operand to floating-point,
multiplication of a dimensionful number and `Zero()` is not supported, etc. Some
broadcasted binary operations involving a neutral number and and array `A` are faster,
like `๐ .+ A` compared to `Zero() .+ A`.
- [`StaticNumbers.jl`](https://github.com/perrutquist/StaticNumbers.jl) is a generalization
of `Zeros` to other any numeric values, not just `0` and `1`.