Hello,
I'm not sure what this is doing
julia> 130 % Int8
-126
methods(%) shows that % is the same as rem, and that there are definitions of rem(::IntX,::Type{IntY}) for all pairs X,Y \in {8,16,32,64,128} (and unsigned equivalents). These do not seem to be documented.
(As an aside: there's some behaviour with Bools here as well. x % Bool for x::Integer returns whether x is odd. Furthermore x % true == 0 for all x and x % false gives a DivideError. In conclusion: ???)
It depends on some specific conditions on the types given, but generally it's equivalent to trunc(T, x), bitcast(T, x), or convert(T, x). I agree this should be documented as I've seen it in use in many places.
The difference is that it's explicit about how wrap-around behavior should work – convert throws an error if the input value cannot be faithfully represented in the target type. This views the target type as modular and picks an equivalent representative of the input value under that modulus. This behavior is what C typically does by default with casts, although it's not a defined behavior, iirc.
Typically that's a very cheap operation between primitive integer types:
In regards to the Bool behavior, this makes sense when you think about the definition of rem: the remainder after division. If you divide by false, you're dividing by 0, since false == 0. Thus the DivideError makes sense. If you divide by true, you're dividing by 1, so there will never be a remainder. Thus x % true == 0 for all x makes sense.
@ararslan Agreed. Fun though it is, if say Bool stops being a Number and this no longer inherits its behaviour from rem(x::Real,y::Real), I guess it would not be missed terribly :stuck_out_tongue:
As far as I can tell, x % T actually calls Base.trunc_int(T, x), which is defined by ... add_tfunc(trunc_int, 2, 2, bitcast_tfunc) ? Not sure what that does technically, the definition confuses me.
But because of the wrap-around behaviour it's very different to rem or mod, since those are sign-stable on one of their arguments. Whereas x % Int8 seems to subtract 256 from x until the result is in [typemin(Int8),typemax(Int8)]. (Likewise x % Bool repeatedly subtracts 2.) There's obviously an analogy to rem(x, y) here, but properties that rem has guaranteed in the manual -- sign of rem(x, y) is the same as the sign of x -- don't hold. And since I don't understand the pun, it makes me a bit uncomfortable.
The analogy is best seen by viewing rem and mod as providing solutions to congruence relations and restricting the result to some interval for uniqueness.
Both z = rem(x, y) and z = mod(x, y) find z such that x ≡ z (mod y). To make the solution unique, z is restricted to some interval, either [0,abs(y)) or (-abs(y),0] depending on operation and signs of x and y.
For integer types, z = rem(x, T) = mod(x, T) finds z such that x ≡ z (mod n), where n is the cardinality of T, and z is restricted to the interval of values representable by T.
@GunnarFarneback Nice description :+1: And thanks for pointing out that rem(x,T) = mod(x,T), I guess that sort of improves the pun in fact.
Trying to narrow down my confusion about the analogy, it's that the target intervals are inconsistent. (rem(x,y) and mod(x,y) -- different names for different target range behaviour. But rem(x,T) = mod(x,T) shares names while taking a third kind of target range.) But I can't think of a better way. Do we want the same docstring for both rem(x,T) and mod(x,T)?
cc @simonbyrne who has done a lot of thinking about rem and mod. One way to understand the difference is rounding of the remainder – for rem one truncates to zero, for mod one takes the floor. There are other behaviors as well, but I think the bottom line is that this is a reasonable method to include in rem as the "ur remainder operation". Regarding inconsistency of target intervals, the target type T completely specifies the target interval. We could get rid of the mod(x,T) alias for rem(x,T), but it seems fairly harmless.
@felixrehren: trunc_int is an intrinsic; the line you found is the code that tells type inference how trunc_int behaves in the type domain, not the definition of its behavior. It's definition is in C++ in the file src/intrinsics.cpp. It does integer truncation.
For what it's worth, when T is unsigned, mod(X, T) agrees quite well with mod(x, y) in terms of target interval.
Ah, yes, that's a fair point. Seems best to just keep both rem and mod for this.
Can this be closed or is there something left to be done?
Thanks @GunnarFarneback, this seems to have been sufficiently addressed by #20759.