Skip to content

ACP: Intuitive alternative for .min() and .max() #665

@Kyuuhachi

Description

@Kyuuhachi

Proposal

Problem statement

It is a fairly common mistake to write x.max(8) and expect "x but no more than 8", when it's in fact the opposite.

Motivating examples or use cases

While I don't have any concrete examples from my own codebases (because I've fixed any I've found), I think everyone has written something like arr[..x.max(arr.len())] at some point.

There are also a few clippy lints related to similar errors, such as min_max with x.max(100).min(0), and manual_clamp which mentions a rather confusing input.max(min).min(max). These indicate that there is precedent for accidentally swapping min and max.

Solution sketch

I propose adding a function .limit() to Ord (and to float types), which serves the role of both .min(), .max(), and .clamp(). In brief:

x.limit(5..) == x.max(5)
x.limit(..=10) == x.min(10)
x.limit(5..=10) == x.clamp(5, 10)
x.limit(..) == x // because why not?

and yes I did confuse min and max while writing the above. Case in point.

With this, the above examples would be written as arr[..x.limit(arr.len()..)], x.limit(100..).limit(..=0), and input.limit(min..).limit(..=max), respectively. I think all three make the intention (and the mistakes) significantly clearer.

I also imagine a clippy lint that suggests replacing .min() and .max(), and maybe also .clamp(), with their corresponding .limit() forms.

Implementation sketch

pub trait Ord {
    ...
    fn limit<B: LimitBounds<Self>>(self, bounds: B) -> Self {
        bounds.limit_bounds(self)
    }
}

pub trait LimitBounds<T: Ord>: Sized + Sealed {
    fn limit_bounds(self, value: T) -> T;
}

impl<T: Ord> LimitBounds<T> for RangeFrom<T> {
    fn limit_bounds(self, value: T) -> T {
        value.max(self.start)
    }
}

impl<T: Ord> LimitBounds<T> for RangeToInclusive<T> {
    fn limit_bounds(self, value: T) -> T {
        value.min(self.end)
    }
}

impl<T: Ord> LimitBounds<T> for RangeInclusive<T> {
    fn limit_bounds(self, value: T) -> T {
        let (start, end) = self.into_inner();
        value.clamp(start, end)
    }
}

impl<T: Ord> LimitBounds<T> for RangeFull {
    fn limit_bounds(self, value: T) -> T {
        value
    }
}


impl f32 {
    ...
    fn limit<B: LimitBounds<f32>>(self, bounds: B) -> f32 {
        bounds.limit_bounds(self)
    }
}

impl LimitBounds<f32> for RangeFrom<f32> {
    fn limit_bounds(self, value: f32) -> f32 {
        value.max(self.start)
    }
}
// and so on for each range and float type

Alternatives

This could totally be a separate crate, and I would have made one if I could come up with a name. But I think it's a common enough annoyance that it would fit in std.

The operations can be written less error-pronely with function form (usize::min(x, arr.len())) or with clamp (x.clamp(0, arr.len())). In my opinion these are both unsatisfactory: both are rather verbose, functoin form doesn't fit into method chains, and clamp requires the type to be bounded.

Unfortunately the name clamp is already taken, and the function cannot be retrofitted due to different number of arguments. Otherwise, that name would be much better for this operation.

Links and related work

IRLO thread.

The clamp RFC considered making clamp take a range, but as I understand it, that was rejected because inclusive ranges were rather immature at the time. This is no longer the case.

What happens now?

This issue contains an API change proposal (or ACP) and is part of the libs-api team feature lifecycle. Once this issue is filed, the libs-api team will review open proposals as capability becomes available. Current response times do not have a clear estimate, but may be up to several months.

Possible responses

The libs team may respond in various different ways. First, the team will consider the problem (this doesn't require any concrete solution or alternatives to have been proposed):

  • We think this problem seems worth solving, and the standard library might be the right place to solve it.
  • We think that this probably doesn't belong in the standard library.

Second, if there's a concrete solution:

  • We think this specific solution looks roughly right, approved, you or someone else should implement this. (Further review will still happen on the subsequent implementation PR.)
  • We're not sure this is the right solution, and the alternatives or other materials don't give us enough information to be sure about that. Here are some questions we have that aren't answered, or rough ideas about alternatives we'd want to see discussed.

Metadata

Metadata

Assignees

No one assigned

    Labels

    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