-
Notifications
You must be signed in to change notification settings - Fork 24
Description
Problem statement
Clamping a numeric value to a symmetric range [-limit, +limit] is a frequent operation in many domains like graphics, physics, and signal processing. The current method in Rust is to use value.clamp(-limit, limit).
While functional, this has a few drawbacks:
1. The limit value must be specified twice.
Updating a symmetric limit requires changing two values, which increases the risk of inconsistencies. It’s easy to make a typo either when initially writing it or when modifying it later, for example: value.clamp(-limit, other_limit).
2. Lack of semantic clarity
Writing clamp(-limit, limit) shows how the operation works by clamping between two bounds, but it doesn’t clearly convey the developer’s intent, which is simply to restrict a number’s magnitude. This goes against Rust’s philosophy of designing APIs that are intention-revealing and immediately understandable.
3. Inefficiency
The general clamp(min, max) method can clamp to any range, including asymmetric ones. The specific case of symmetric clamping (clamping -val, val) is quite common, searches of public Rust code on GitHub show roughly 1.7k occurrences. Using clamp for this requires manually constructing the symmetric range, effectively emulating a more specific operation. This is suboptimal and doesn’t align with Rust’s zero-cost, intention-revealing abstractions.
4. Ergonomics/readability
Repeating the limit value adds visual noise and reduces readability. It isn’t ergonomic or intuitive, which goes against Rust’s emphasis on clean, readable, and intention-revealing APIs.
Motivating examples or use cases
Game Development
Limiting the pitch (up/down rotation) of a camera is a standard requirement to prevent it from flipping over. This is often done by clamping the pitch angle to a range like [-π/2, π/2].
From veloren/veloren, a popular open-source voxel RPG:
// voxygen/src/session/mod.rs
let mut cam_dir = camera.get_orientation();
let cam_dir_clamp =
(global_state.settings.gameplay.camera_clamp_angle as f32).to_radians();
cam_dir.y = cam_dir.y.clamp(-cam_dir_clamp, cam_dir_clamp); // <--- Here
camera.set_orientation(cam_dir);From janhohenheim/avian_pickup, a Bevy physics plugin:
// examples/minimal.rs
const PITCH_LIMIT: f32 = FRAC_PI_2 - 0.01;
let pitch = (pitch + delta_pitch).clamp(-PITCH_LIMIT, PITCH_LIMIT); // <--- Here
transform.rotation = Quat::from_euler(EulerRot::YXZ, yaw, pitch, roll);Physics
In physics simulations (including within games), it's essential to cap movement speeds and forces to a maximum magnitude.
From rust-gamedev/rust-game-ports, in a port of the classic game "Boing":
// boing-ggez/src/controls.rs
// ... determine the offset to the target position
// then make sure we can't move any further than MAX_AI_SPEED each frame
(target_y - bat.y).clamp(-MAX_AI_SPEED, MAX_AI_SPEED) // <--- HereRobotics
In robotics, control commands sent to hardware often need to be constrained to safe operating limits.
From farm-ng/amiga-ros-bridge-v1, a ROS bridge for a robotics platform:
// examples/amiga-joystick.rs
state.cmd_linear_x_vel += delta_t * state.joystick_linear_accel;
state.cmd_linear_x_vel = state
.cmd_linear_x_vel
.clamp(-MAX_LINEAR_VEL_MPS, MAX_LINEAR_VEL_MPS); // <--- HereAudio/Signal Clipping
To prevent distortion, audio samples or intermediate signals in a processing graph are often hard-clipped to a specific range, most commonly [-1.0, 1.0].
From NibbleRealm/twang, an audio synthesis library:
// src/ops/clip.rs
pub fn step(&mut self, input: Ch32, limit: Ch32) -> Ch32 {
let limit = limit.to_f32().abs();
Ch32::from(input.to_f32().clamp(-limit, limit)) // <--- Here
}Other
A GitHub search using a regular expression found roughly 1,700 instances of the .clamp(-limit, limit) pattern in public Rust code. These instances could be made more readable by using clamp_magnitude (if it were to be introduced) instead. You can see additional real-world examples of this pattern in Rust by checking the search here.
Solution sketch
I propose adding a new method, clamp_magnitude, to all primitive signed integer types (i8, i16, i32, i64, i128, isize) and floating-point types (f32, f64, and the experimental f16, f128).
This method takes a single non-negative limit and clamps the value self to the symmetric range [-limit, limit].
Proposed API
The implementation would be added to each respective primitive type. The following examples for f64 and i32 are representative of the full implementation.
For floating-point types (e.g., f64):
impl f64 {
/// Clamps this number to a symmetric range centered around zero.
///
/// The method clamps the number's magnitude (absolute value) to be at most `limit`.
///
/// This is functionally equivalent to `self.clamp(-limit, limit)`, but is more
/// explicit about the intent.
///
/// # Panics
///
/// Panics if `limit` is negative or NaN, as this indicates a logic error.
///
/// # Examples
///
/// ```
/// assert_eq!(5.0.clamp_magnitude(3.0), 3.0);
/// assert_eq!(-5.0.clamp_magnitude(3.0), -3.0);
/// assert_eq!(2.0.clamp_magnitude(3.0), 2.0);
/// assert_eq!(-2.0.clamp_magnitude(3.0), -2.0);
/// ```
#[must_use = "this returns the clamped value and does not modify the original"]
#[inline]
pub fn clamp_magnitude(self, limit: f64) -> f64 {
assert!(limit >= 0.0, "limit must be non-negative");
let limit = limit.abs(); // Canonicalises -0.0 to 0.0
self.clamp(-limit, limit)
}
}For signed integer types (e.g., i32):
impl i32 {
/// Clamps this number to a symmetric range centered around zero.
///
/// The method clamps the number's magnitude (absolute value) to be at most `limit`.
///
/// This is functionally equivalent to `self.clamp(-limit, limit)`, but is more
/// explicit about the intent.
///
/// # Examples
///
/// ```
/// assert_eq!(5.clamp_magnitude(3), 3);
/// assert_eq!(-5.clamp_magnitude(3), -3);
/// assert_eq!(2.clamp_magnitude(3), 2);
/// assert_eq!(-2.clamp_magnitude(3), -2);
/// ```
#[must_use = "this returns the clamped value and does not modify the original"]
#[inline]
pub fn clamp_magnitude(self, limit: u32) -> i32 {
if limit >= i32::MAX as Self {
self // all possible values are in-bounds
} else {
self.clamp(-(limit as Self), limit as Self)
}
}
}Implementation notes
- The use of
-limitis safe from overflow. For any signed integer typeT, the only valuexfor which-xoverflows isT::MIN. The assertionlimit >= 0prevents limit from beingT::MIN, thereby guaranteeing that-limitis always representable. It also correctly handlesNaNinputs for limit, as any comparison withNaNisfalse. - Other names like
clamp_symmetricorclamp_abswere considered.clamp_magnitudewas chosen because it most directly describes the operation being performed on the value: its magnitude is being clamped.clamp_symmetricdescribes the range, not the operation, and clamp_abs could be ambiguous. - This method is not proposed for unsigned integer types, as the concept of a symmetric range around zero is less meaningful. The equivalent operation for unsigned types is simply
self.min(limit). - Another mathematically correct implementation for
clamp_magnitudebesidesself.clamp(-limit, limit)isself.signum() * self.abs().min(limit). I think that the clamp version is better as it will likely compile down to more efficient branchless code (using min/max instructions).
Potential drawbacks
- This adds a new method to the public API of primitive numeric types, which should always be done with care. However, given its direct relationship to the existing clamp method and its common use case, I think the ergonomic benefit outweighs the minor cost of this small addition.
- Users may not know the method exists and continue to write
value.clamp(-limit, limit). This is unavoidable, but proper documentation and announcements can help mitigate it. Adding a clippy lint recommending the change ofvalue.clamp(-limit, limit)tovalue.clamp_magnitude(limit), should this ACP be accepted, would also increase discoverability and further mitigate this.
Alternatives
The primary alternative is to continue using value.clamp(-limit, limit). This is well-understood and works perfectly. However, the semantic clarity and prevention of simple errors offered by clamp_magnitude justifies its addition as a small ergonomic improvement, in line with Rust's philosophy.
This functionality could also easily be provided by a trait in a third-party crate. However, clamp itself is in the standard library because it's a fundamental, primitive operation. Clamping to a symmetric range is an equally fundamental and common variant of clamping that deserves a place alongside it, and this is such a small piece of functionality that a whole seperate crate is too overkill.