Skip to content

ACP: Add clamp_magnitude method to float and signed integer primitives #686

@IntegralPilot

Description

@IntegralPilot

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) // <--- Here

Robotics

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); // <--- Here

Audio/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 -limit is safe from overflow. For any signed integer type T, the only value x for which -x overflows is T::MIN. The assertion limit >= 0 prevents limit from being T::MIN, thereby guaranteeing that -limit is always representable. It also correctly handles NaN inputs for limit, as any comparison with NaN is false.
  • Other names like clamp_symmetric or clamp_abs were considered. clamp_magnitude was chosen because it most directly describes the operation being performed on the value: its magnitude is being clamped. clamp_symmetric describes 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_magnitude besides self.clamp(-limit, limit) is self.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 of value.clamp(-limit, limit) to value.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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    ACP-acceptedAPI Change Proposal is accepted (seconded with no objections)T-libs-apiapi-change-proposalA proposal to add or alter unstable APIs in the standard libraries

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions