In a lot of the code I’ve been writing, I’ve needed to do sign manipulation, and in principle this can be done with Boolean values. However, I find this to be a bit of a mess - just because I know how to treat true and false as signbits doesn’t mean it makes it clear or easy to read what I’ve done. So I decided to write a package that just implements a signbit type.
The feedback I got from other programmers outside of the Julia community was that a package that just implements a signbit type would be pointless, but at JuliaCon, several people encouraged me to register this package and said they would find it useful for readability and resolving issues that may arise with promotion.
This package exports a type called Sign which just represents a signbit. It supports the same logical semantics as Bool, but the arithmetic semantics correspond to the values +1 and -1. For convenience, we have the notation Sign(+) and Sign(-) (a bit of a hack, but it’s nice). Instead of exponentiating -1, you can just raise Sign(-) to a power.
There are some quirks of having a numeric type that does not represent zero:
zero(::Sign) === false. (This is because false is a strong zero and is thus the best analog to Sign(+) and Sign(-)).
The Sign constructor on any zero value generally returns the signbit, but convert(Sign, x) will throw an InexactError.
Constructing or converting a Sign from an isnan value also throws an InexactError.
One other quirk: Rational division (//) of Sign with Sign returns a Sign, because Rational{Sign} and Sign represent exactly the same values.
There’s still some work to be done with getting random sign generation working (and perhaps having a dedicated SignArray type analogous to BitArray) but otherwise, this package should be ready to use if you want to clarify sign manipulation in your codebase.
Thanks to @Kevin Qing for helping me out with this at JuliaCon 2025!
Link to package on JuliaHub (missing in the OP): SignType.jl
Regarding the package name, presumably it should have come in plural, if the package naming rules were to be followed.
After a quick look at the package:
Why use primitive type? Would it not be simpler to just make it a struct with a Bool field? Actually, that could maybe even be more amenable to some compiler optimizations, I think, because LLVM knows a Bool is just a single bit wide, instead of eight bits.
Why subtype Signed? AFAIK Signed is meant for two’s complement integers, meant to support bitwise operations. I suppose Integer would be a more appropriate direct supertype.
PSA to folks interested in this: do also take a look at the base functions copysign and flipsign. Took me way too many years before I discovered them.
Of course, they’re no replacement for a package like SignTypes.jl, so use that too! Just wanted to give these functions some PR to an audience that might benefit from them.
The naming rules would have called for a plural only if the name had been Signs, for a “package providing the type Sign”. I didn’t find SignType objectionable, though: both using Signs and using SignType read well as “plain English”, which I think is at least in part behind those guidelines.
In any case, the package was registered a few days ago, so it is too late for any changes at this point. Anyone who is interested in upholding naming guidelines or other QA issues throughout the ecosystem is encouraged to keep an eye on the #new-packages-feed on Slack or Zulip and to leave comments on the registration during the waiting period. Conversely, anyone wanting feedback on a package, including the name, is encouraged to post in Package Announcements before registration, or during the waiting period.
Tangentially related is my package SymbolicSigns.jl, which lets you do computations with symbolic signs. The sign exponents are symbolic integers, thought of as degrees of some objects:
julia> using SymbolicSigns
julia> da, db, dc = Deg(:a), Deg(:b), Deg(:c)
(Deg(:a), Deg(:b), Deg(:c))
julia> s = Sign(da*db + dc)
(-1)^(|c|+|a||b|)
julia> (s+1)^2
Linear{Sign{Symbol}, Int64} with 2 terms:
2*(-1)^(|c|+|a||b|)+2
One of the reasons why I decided to write this package is that there was a discussion about how the number of methods that explicitly include the term path in them (e.g. splitpath, joinpath) was evidence that Julia was in need of a path type. Similarly, with a lot of methods that are designed to handle signs (sign, signbit, flipsign, copysign) indicated to me that it would be good to have a Sign type.
I can change this around: I figured since Sign mirrors the implementation of Bool it would make sense for it to be a primitive type, but I didn’t realize LLVM may be able to apply its optimizations for Bool if the struct explicitly contained one.
This is a bit of a radical decision, but the perspective I had on Sign is that it is effectively an Int1: just the sign bit of a signed integer and nothing else. Perhaps this is the wrong idea (there is the huge caveat that Sign cannot represent zero!) and this would be the better option, mirroring Bool.
Sign supports bitwise operations (with the exception of short-circuiting boolean operators, which require Bool).
I did not realize this. I guess when working with abstract types provided by Base, everything really needs to be explicitly defined for individual types rather than over supertypes. This was an issue when defining construction and conversion, but since my tests didn’t detect method ambiguities I assumed it was fine.
Sign should be unobtrusive in its promotion rules (promoting any T<:Real that can represent negative numbers with Sign should be promoted to T, and with U<:Unsigned it promotes to signed(U)).