Rust: 钳位 RFC 的跟踪问题

创建于 2017-08-26  ·  101评论  ·  资料来源: rust-lang/rust

https://github.com/rust-lang/rfcs/pull/1961 的跟踪问题

公关在这里: #44097 #58710
稳定 PR: https :

去做:

  • [x] 让 RFC 通过最终评论期
  • [x] 实现 RFC
  • [ ] 稳定
B-unstable C-tracking-issue Libs-Tracked T-libs

最有用的评论

如果人们想要定义扩展特性的任何方法永远无法添加到标准库中,那么我们似乎处于一个非常糟糕的境地。

所有101条评论

请注意:这破坏了伺服和探路者。

cc @rust-lang/libs,这是一个类似于min / max ,其中生态系统已经在使用clamp名称,因此添加它会导致歧义. 这是每个 semver 策略允许的破坏,但它仍然会导致下游的痛苦。

提名周二的分类会议。

在此期间有什么想法吗?

我有点喜欢@bluss ,因为最好不要重复它。 “Clamp”可能是一个很棒的名字,但我们可以通过选择一个不同的名字来回避这个吗?

restrict
clamp_to_range
min_max (因为它有点像结合 min 和 max。)
这些可能会奏效。 我们可以使用火山口来确定clamp实际影响有多严重吗? clamp在多种语言和库中得到了很好的认可。

如果我们认为我们可能需要重命名,最好立即恢复 PR,然后使用 crater 等进行更仔细的测试。 @Xaeroxe ,为此?

当然。 我以前从未使用过火山口,但我可以学习。

@Xaeroxe啊对不起,我的意思是快速恢复 PR。 (我今天在度假,所以你可能需要其他人使用 libs,比如@BurntSushi@alexcrichton来帮助登陆)。

我现在正在准备PR。 祝您假期愉快!

clamp_to_range(min, max)可以由clamp_to_min(min)clamp_to_max(max) (附加断言min <= max ),但这些函数也可以独立调用?

我想这个想法需要一个 RFC。

我得说,虽然我一直在努力将 4 行函数放入 std 库中 6 个月了。 我有点累了。 相同的函数在 2 天内合并到num中,这对我来说已经足够了。 如果其他人真的想要在标准库中使用它,请继续,但我还没有准备好再使用 6 个月。

我正在重新打开它,以便仍然可以看到@aturon之前的提名。

我认为这应该按原样进行,或者应该更新关于可以进行哪些更改的指南,以避免将来浪费人们的时间。

从一开始就很清楚这可能会导致它的破损。 就个人而言,我将它与ord_max_min ,后者破坏了很多东西:

对此的回应是“添加了Ord::min函数 [...] 库团队今天决定这是可接受的破坏”。 这是一个具有更常见名称的 TMTOWTDI 功能,而clamp没有以不同的形式存在于 std 中。

主观上,对我来说,如果这个 RFC 被恢复,实际规则是“你基本上不能在 std 中的特性上放置新方法,除了Iterator ”。

您也不能真正将新方法放在实际类型上。 考虑某人对 std 中的类型具有“扩展特征”的情况。 现在 std 实现了一个方法,该扩展特征作为此类型的实际方法提供。 然后这达到稳定,但这个新方法仍然落后于功能标志。 然后编译器会抱怨该方法位于功能标志后面并且不能与稳定工具链一起使用,而不是编译器像以前一样选择扩展特征的方法,从而导致稳定编译器损坏。

还值得注意的是:这不仅仅是一个标准库问题。 方法调用语法使得在生态系统中几乎任何地方都很难避免引入破坏性更改。

(元)只是这里

如果我们同意 #44438 是合理的,

  1. 我们可能需要重新考虑是否真的可以将保证类型推断破坏等视为 XIB。

    目前,RFC 11051122认为类型推断更改是可以接受的,因为人们总是可以使用 UFCS 或其他方式来强制类型。 但是社区并不真正喜欢#42496 ( Ord::{min, max} ) 造成的损坏。 此外,#41336( T += &T第一次尝试)由于 8 种类型推理回归而“仅”关闭。

  2. 每当我们添加一个方法时,应该有一个弹坑运行以确保名称不存在。

    请注意,添加固有方法也会导致推理失败 - #41793 是由添加固有方法{f32, f64}::from_bits ,这与下游特征中的ieee754::Ieee754::from_bits方法冲突。

  3. 当下游 crate 没有指定#![feature(clamp)] ,除非这是唯一的解决方案,否则永远不应考虑候选Ord::clamp (仍然可以发出未来兼容的警告)。 这将允许引入新的 trait 方法而不是“insta-breaking”,但在稳定时问题仍然会出现。

如果人们想要定义扩展特性的任何方法永远无法添加到标准库中,那么我们似乎处于一个非常糟糕的境地。

Max/min 遇到了一个特别糟糕的地方,即在共同特征上使用通用方法名称。 同样不需要适用于钳位。

我仍然想说是的,但是@sfackler我们真的必须在一个由不同类型如此普遍实现的特征上添加方法吗? 当我们将所有类型的 api 添加到现有特性中时,我们必须小心。

随着专业化的到来,我们不会因为将扩展方法放在扩展特性中而失去任何东西。

一个令人讨厌的部分是,如果新的 std 方法破坏了您的代码:它会在您真正使用它之前很久就出现,因为它不稳定。 除此之外,如果冲突与具有相同含义的方法发生冲突,那就不是那么糟糕了。

我认为给这个函数一个不同的名字以避免损坏是一个糟糕的解决方案。 虽然它有效,但它的优化不会破坏一些板条箱(所有这些板条箱都选择每晚加入),而不是优化使用此功能的任何代码的未来可读性。

我有一些担忧,其中一些并不担心 imo。

  • 名称和阴影并不理想,但它有效
  • 对于数值向量和矩阵,我认为 max/min/clamp 并不理想,但这可以通过根本不使用 Ord 来解决。 Ndarray 想做元素和泛型参数(标量或数组)钳位,但我们或类似库不使用 Ord。 所以不用担心。
  • 现有的非数字复合类型:BtreeMap 将通过此更改获得方法限制。 这在一般情况下有意义吗? 除了默认之外,它是否可以为其实现合理的含义?
  • 按值调用模式并不适合每个实现。 再次,BtreeMap。 钳子应该消耗 3 张地图并返回其中一张吗?

复合类型

我认为它和BtreeSet<BtreeSet<impl Ord>>::range一样有意义。 但是有些特殊情况甚至可能会有所帮助,例如Vec<char>

按值调用模式

当这在 RFC 中出现时,答案就是使用 Cow

当然,重用存储可能是这样的

    fn clamp<T>(mut self, low: &T, high: &T) -> Self
        where T: ?Sized + ToOwned<Owned=Self> + Ord, Self : Borrow<T>
    {
        assert!(low <= high);
        if self.borrow() < &low {
            low.clone_into(&mut self);
        } else if self.borrow() >= &high {
            high.clone_into(&mut self);
        }
        self
    }

其中https://github.com/rust-lang/rfcs/pull/2111可能符合人体工程学的调用。

libs 团队在几天前的分类中讨论了这个问题,结论是我们应该进行一次火山口运行,看看这个变化在整个生态系统中发生了什么。 其结果将决定对这个问题应该采取什么行动。

我们可以添加许多可能的未来语言功能,以简化添加这样的 api,例如低优先级特征或以更有趣的方式使用扩展特征。 然而,我们并不想在像这样的进步上阻止这一点。

此功能是否曾发生过陨石坑?

我计划在#48552 合并后恢复clamp()方法。 然而, RangeInclusive将在此之前稳定下来,这意味着基于范围的替代方案现在可以考虑(这实际上是最初的提议,但由于..=太不稳定而被撤回了😄):

// Current
trait Ord {
    fn clamp(self, min: Self, max: Self) -> Self { ... }
}
assert_eq!(9.clamp(6, 7), 7);


// Alternative
trait Ord {
    fn clamp(self, range: RangeInclusive<Self>) -> Self { ... }
}
assert_eq!(9.clamp(6..=7), 7);

稳定的RangeInclusive还开辟了其他可能性,例如翻转事物(这可以使用 autoref 实现一些有趣的可能性,并完全避免名称冲突):

impl<T: Ord + Clone> RangeInclusive<T> {
    fn clamp(&self, mut x: T) -> T {
        if x < self.start { x.clone_from(&self.start); }
        else if x > self.end { x.clone_from(&self.end); }
        x
    } 
} 

    assert_eq!((1..=10).clamp(11), 10);

    let strings = String::from("aa")..=String::from("b");
    assert_eq!(strings.clamp(String::from("a")), "aa");
    assert_eq!(strings.clamp(String::from("aaa")), "aaa");

https://play.rust-lang.org/?gist=38def79ba2f3f8380197918377dc66f5&version=nightly

我还没有决定我是否认为这更好,但是......

如果用作范围方法,我会使用不同的名称。

当然,无论形状如何,我都会喜欢早日拥有该功能。

目前的状态是什么?
在我看来,有一个共识,即为 RangeInclusive 添加钳位可能是更好的选择。
所以有人必须写一个RFC?

此时可能不需要完整的 RFC。 只是决定选择哪种拼写:

  1. value.clamp(min, max) (按原样遵循 RFC)
  2. value.clamp(min..=max)
  3. (min..=max).clamp(value)

选项 2 或 3 将允许更容易的部分夹紧。 您可以执行value.clamp(min..)value.clamp(..=max) ,而无需特殊的clamp_to_startclamp_to_end方法。

@egilburg :我们已经有了那些特殊的方法: clamp_to_startmaxclamp_to_endmin :wink:

不过一致性很好。

@egilburg Rust 不支持直接重载。 要使选项 2 与您的建议一起工作,我们需要为RangeInclusiveRangeToInclusiveRangeFrom实现一个新特征,这感觉很重。

我认为,选项 3 是最好的选择。

1 或 2 是最不令人惊讶的。 我会继续使用 1,因为用标准代码替换本地实现时,很多代码的工作量会减少。

我认为我们应该计划使用_all_ range* 类型或_none_。

当然,这对于RangeRangeInclusive更难。 但是(0.0..1.0).clamp(2.0_f32) => 0.99999994_f32有一些好处。

@kennytm所以,如果我打开一个带有选项 3 的拉取请求,你认为它会被合并吗?
或者您认为下一步如何进行?

@EdorianDark为此,我们需要询问@rust-lang/libs 😃

我个人喜欢选项 2,只有RangeInclusive 。 如前所述, minmax已经存在“部分钳位”。

我同意@SimonSapin ,尽管我也可以选择选项 1。对于选项 3,我可能不会使用该功能,因为它在我看来是倒退的。 在@kennytm之前调查的其他带有钳位的语言/库中,7 个中有 5 个(除 Swift 和 Qt 外)首先具有值,然后是范围。

Clamp 现在再次掌握了!

我很高兴,尽管我仍在努力弄清楚是什么改变使现在可以接受,而它不是在 #44097 中

由于#48552,我们现在有一个警告期,而不是在稳定之前立即中断推理。

这是个好消息,谢谢!

@kennytm我只想感谢您为实现 #48552 所做的工作, @EdorianDark感谢您对此感兴趣并实施它。 很高兴看到这最终合并。

https://rust.godbolt.org/z/JmLWJi

pub fn clamped(a: f32) -> f32 {
   a.clamp(0.,255.)
}

编译为:

  vxorps xmm1, xmm1, xmm1
  vmaxss xmm0, xmm1, xmm0
  vmovss xmm1, dword ptr [rip + .LCPI0_0]
  vminss xmm0, xmm1, xmm0

这还不错(使用了vmaxssvminss ),但是:

pub fn maxmined(a: f32) -> f32 {
   (0f32).max(a).min(255.)
}

少用一条指令:

  vxorps xmm1, xmm1, xmm1
  vmaxss xmm0, xmm0, xmm1
  vminss xmm0, xmm0, dword ptr [rip + .LCPI1_0]

这是钳位实现所固有的,还是只是 LLVM 优化的一个怪癖?

@kornelski clamp NAN应该保留NANmaxmined没有,因为max / min保留 _non_- NAN

找到既满足 NAN 期望又更短的实现会很棒。 并且 doctests 展示 NAN 处理会很好。 看起来原来的 PR 有一些:

https://github.com/rust-lang/rust/blob/b762283e57ff71f6763effb9cfc7fc0c7967b6b0/src/libstd/f32.rs#L1089 -L1094

如果 min 或 max 为 NaN,为什么夹紧浮点数会恐慌? 我会将断言从assert!(min <= max)更改assert!(!(min > max)) ,这样 NaN 最小值或最大值将不起作用,就像在 max 和 min 方法中一样。

NAN 为minmax inclamp 很可能表示存在编程错误,我们认为最好尽早恐慌,而不是将未钳位的数据提供给 IO。 如果您不想要上限或下限,则此功能不适合您。

如果您不想要上限或下限,您总是可以使用 INF 和 -INF,对吗? 与 NaN 不同,这也具有数学意义。 但大多数时候最好使用maxmin

@Xeroxe感谢您的实施。

也许这可以在下一版中使用,如果它会破坏稳定的代码?

IMO 值得更详细考虑的一件事是f32 / f64的片面限制。 讨论似乎简单地触及了这个话题,但没有真正详细考虑过。

在大多数情况下,如果单侧钳位的输入是 NAN,那么结果是 NAN 比结果是钳位边界更有用。 因此,现有的f32::minf64::max函数不适用于此用例。 我们需要单独的功能来进行单面夹紧。 (参见 rust-num/num-traits#122。)

我提出这个的原因是它影响了双面clamp ,因为如果双面夹和单面夹具有一致的接口会很好。 几个选项是:

  1. input.clamp(min, max)input.clamp_min(min)input.clamp_max(max)
  2. input.clamp(min..=max) , input.clamp(min..) , input.clamp(..=max)
  3. input.clamp(min, max) , input.clamp(min, std::f64::INFINITY) , input.clamp(std::f64::NEG_INFINITY, max)

使用当前的实现( minmax作为单独的f32 / f64参数),我们将不得不选择选项 1,我认为这是完全合理的,或选项 3,IMO 过于冗长。 我们应该意识到牺牲是必须添加单独的clamp_minclamp_max函数或要求用户写出正/负无穷大。

还值得注意的是,我们可以提供

impl f32 {
    pub fn clamp<T>(self, bounds: T) -> f32
    where
        T: RangeBounds<f32>,
    {
         // ...
    }
}

// and for f64

因为对于f32 / f64我们实际上知道如何处理独占边界,不像一般的Ord 。 当然,那么我们可能希望将Ord::clamp更改RangeInclusive参数以保持一致性。 对于Ord::clamp是否更喜欢两个参数或单个RangeInclusive参数,似乎没有强烈的意见。

如果这已经是一个已解决的问题,请随时忽略我的评论。 我只是想提出这些问题,因为我在之前的讨论中没有看到它们。

分类:以下 API 目前不稳定并指向此处。 除了 NaN 处理之外还有其他问题需要考虑吗? 是否值得先稳定Ord::clamp而不在 NaN 处理上阻塞它?

```生锈
酒吧特质顺序:Eq + PartialOrd{
// ...
fn 钳子(自我,最小:自我,最大:自我)->自我其中自我:大小{...}
}
impl f32 {
pub fn 钳位(自我,最小值:f32,最大值:f32)-> f32 {…}
}
impl f64 {
pub fn 钳位(自我,最小值:f64,最大值:f64)-> f64 {…}
}

@SimonSapin我很乐意亲自稳定整个事情

+1,这通过了完整的 RFC,我认为从那时起就没有任何材料出现。 例如,在 IRLORFC 讨论中详细介绍NaN 处理。

好吧,这听起来很公平。

@rfcbot fcp 合并

团队成员@SimonSapin已提议将其合并。 下一步是由其他标记的团队成员进行审查:

  • [x] @阿曼纽
  • []@Kimundi
  • [x] @SimonSapin
  • [x] @alexcrichton
  • [x] @dtolnay
  • []@sfackler
  • [ ] @withoutboats

当前未列出任何问题。

一旦大多数审稿人批准(并且最多有 2 个未通过的批准),这将进入其最终评论期。 如果您发现在此过程中任何时候都没有提出的重大问题,请说出来!

有关标记的团队成员可以给我的命令的信息,请参阅此文档

是否就x.clamp(7..=13)x.clamp(7, 13)做出了决定? https://github.com/rust-lang/rust/issues/44095#issuecomment -533764997 提到第一个可能更好地与潜在的未来保持一致f64::clamp

我会说这是一个非常不幸的解决方案,因为.min.max经常导致错误,因为您使用.min(...)指定上限和.max(...)指定下限。 这非常令人困惑,我已经看到了很多错误。 .clamp(..1.0).clamp(0.0..)更加清晰。

@CryZe提出了一个很好的观点:即使您从未在min = upper bound, max = lower bound 上犯过错误,您仍然必须进行心理操练以记住要使用哪个。 这种认知负荷最好花在你试图解决的任何问题上。

我知道x.clamp(y, z)更受期待,但也许这是一个创新的机会;)

我在早期阶段对范围进行了大量试验,甚至将 RFC 推迟了几个月,以便我们可以试验包含范围。 (这是在它们稳定之前开始的)

我发现不可能对浮点数的独占范围实施钳位。 只支持某些范围类型而不支持其他范围类型的结果太令人惊讶了,所以即使我将 RFC 推迟了几个月以这种方式进行试验,我最终还是决定范围不是解决方案。

@m-ou-se 请参阅从 #44095(评论)和 #58710(评论)开始的讨论。

编辑:如下所述,拉取请求 (#58710) 中的讨论比跟踪问题包含更多关于设计决策的讨论。 不幸的是,这里没有传达这一点,这是设计讨论通常发生的地方,但进行了讨论。

只支持某些范围类型而不支持其他类型的结果太令人惊讶了

Rust 已经将某些范围与其他范围不同(例如将它们用于迭代),因此只允许某些范围作为clamp参数对我来说似乎一点也不奇怪。

这是对它最有用的分析: https :

@Xaeroxe仅支持某些范围类型而不支持其他范围类型,结果太令人惊讶了

如果您在稳定之前考虑过这一点,时间和一般使用情况是否改变了您的看法,或者您认为情况仍然如此?

我认为无论如何都不应该为浮点数实现独占范围,因为它们与整数会有不同的行为(范围0..10包括下限并排除上限,那么为什么假设范围0.0...10.0排除两者?)。 我不认为这会令人惊讶,至少对我来说是这样。

@varkor但是在评论中发表了一条评论后,这被改变了,没有对跟踪问题进行任何讨论。

这可能会让人觉得过于对抗,尝试诸如“当我浏览对话时,我没有找到令人信服的论据,为什么我们不应该使用范围,有人可以指出我吗?”。

我怀疑你正在寻找的论点在这里: https :

编辑@Xaeroxe打败了我:)

时间和一般用法改变了你的看法

到目前为止还没有,但范围是我在日常编码中很少使用的东西。 我很乐意被代码示例和具有部分范围支持的现有 API 说服。 然而,即使我们解决了这个问题,scottmcm 在 RFC 评论中仍然提出了其他几个需要解决的问题。 例如, Step没有在与Ord一样多的类型上实现,这个小的语法变化值得丢失这些类型吗? 此外,是否有非包含范围钳位的用例? 据我所知,没有其他语言或框架认为需要支持独占范围限制,那么我们从中获得什么好处? 以令人满意的方式实施范围要困难得多,并且带来了许多缺点和很少的好处。

如果我要使用范围来实现

所以我认为我们不应该采用这种方法有几个原因。

  1. 所需范围的选择足够新颖,需要一个新的特征,特别是排除了最常见的范围Range

  2. 我们已经在 RFC 过程中走了这么远,std 唯一从中受益的是另一种编写.max().min() 。 我真的不想为了实现我们已经可以在 Rust 中做的事情而将 RFC 推迟到流程的开始。

  3. 它使函数中发生的分支数量加倍,以适应我们仍然不确定是否存在的用例。 我无法让它出现在基准测试中。

需要进行单面夹紧操作

... std 唯一从中获得的好处是另一种编写.max().min()

我试图提出的主要观点是,我已经在讨论中多次看到.min() / .max()和单边夹钳之间的这种明显等效性,但操作并不等效对于他们应该处理的浮点数NAN

例如,将input.max(0.)视为将负数限制为零的表达式。 如果input是非NAN ,它工作正常。 但是,当inputNAN ,它的计算结果为0. 。 这几乎从来都不是我们想要的行为; 单面夹紧应保留NAN值。 (请参阅此评论此评论。)总而言之, .max()适用于取两个数字中较大的一个,但不适用于单边钳制。

因此,我们需要对浮点数进行单边钳位操作(与.min() / .max()分开)。 其他人也对非浮点类型的单边钳位操作的实用性提出了很好的论据。 下一个问题是我们想如何表达这些操作。

如何表达单面夹紧操作

.clamp()INFINITY

换句话说,不要添加单面夹紧操作; 只需告诉用户将.clamp()INFINITYNEG_INFINITY边界一起使用。 例如,告诉用户写input.clamp(0., std::f64::INFINITY)

这是非常冗长的,如果用户不了解NAN处理的细微差别,这将促使用户使用不正确的.min() / .max() 。 此外,它对T: Ord没有帮助,而且 IMO 不如替代方案清晰。

.clamp_min().clamp_max()

一种合理的选择是添加.clamp_min().clamp_max()方法,这不需要对当前提议的实现进行任何更改。 我认为这是一个合理的方法; 我只是想确保我们意识到如果我们稳定当前提议的clamp实现,我们就必须使用这种方法。

范围参数

另一种选择是让clamp接受范围参数。 @Xaeroxe已经展示了实现这一点的一种方法,但正如他所提到的,这种实现确实有一些缺点。 另一种编写实现的方式类似于当前实现切片的方式( SliceIndex trait)。 这解决了我在讨论中看到的所有反对意见,除了担心为范围类型的子集提供实现和额外的复杂性。 我同意它增加了一些复杂性,但 IMO 并不比添加.clamp_min() / .clamp_max()差多少。 对于Ord ,我建议如下:

pub trait Ord: Eq + PartialOrd<Self> {
    // ...

    fn clamp<B>(self, bounds: B) -> B::Output
    where
        B: Clamp<Self>,
    {
        bounds.clamp(self)
    }
}

pub trait Clamp<T> {
    type Output;
    fn clamp(self, input: T) -> Self::Output;
}

impl<T> Clamp<T> for RangeFull {
    type Output = T;
    fn clamp(self, input: T) -> T {
        input
    }
}

impl<T: Ord> Clamp<T> for RangeFrom<T> {
    type Output = T;
    fn clamp(self, input: T) -> T {
        if input < self.start {
            self.start
        } else {
            input
        }
    }
}

impl<T: Ord> Clamp<T> for RangeToInclusive<T> {
    type Output = T;
    fn clamp(self, input: T) -> T {
        if input > self.end {
            self.end
        } else {
            input
        }
    }
}

impl<T: Ord> Clamp<T> for RangeInclusive<T> {
    type Output = T;
    fn clamp(self, input: T) -> T {
        assert!(self.start <= self.end);
        let mut x = input;
        if x < self.start { x = self.start; }
        if x > self.end { x = self.end; }
        x
    }
}

对此的一些想法:

  • 我们可以为T: Ord + Step独占范围添加实现。
  • 我们可以每晚保留Clamp trait,类似于SliceIndex trait。
  • 为了支持f32 / f64 ,我们可以

    1. 将实现放宽到T: PartialOrd 。 (我不确定为什么clamp的当前实现是在Ord而不是PartialOrd 。也许我在讨论中遗漏了什么?看起来像PartialOrd就足够了。)

    2. 或专门为f32f64编写实现。 (如果需要,我们可以随时切换到选项 i,而无需进行重大更改。)

    然后添加

    impl f32 {
      // ...
      fn clamp<B>(self, bounds: B) -> B::Output
      where
          B: Clamp<Self>,
      {
          bounds.clamp(self)
      }
    }
    
    impl f64 {
      // ...
      fn clamp<B>(self, bounds: B) -> B::Output
      where
          B: Clamp<Self>,
      {
          bounds.clamp(self)
      }
    }
    
  • 如果需要,我们可以稍后使用f32 / f64为独占范围实现Clamp 。 ( @scottmcm评论说这并不简单,因为std故意没有f32 / f64前驱操作。我不知道为什么std没有这些操作;也许是非正规数的问题?无论如何,这可以在以后解决。)

    即使我们不为具有f32 / f64独占范围添加Clamp实现,我也不同意这会太令人惊讶。 正如@varkor指出的那样,Rust 已经针对CopyIterator / IntoIterator的目的对各种范围类型进行了不同的处理。 (IMO,这是std ,但它至少是范围类型被区别对待的一个实例。)另外,如果有人确实尝试使用独占范围,则错误消息将很容易理解( “不满足绑定的特征std::ops::Range<f32>: Clamp<f32> ”)。

  • 我已将Output设为关联类型,以便在将来添加更多实现时具有最大的灵活性,但这并不是绝对必要的。

基本上,这种方法允许我们在 trait bound 方面拥有尽可能多的灵活性。 它还可以从一组最小有用的Clamp实现开始,然后在不破坏更改的情况下添加更多实现。

比较选项

如上所述,“将.clamp()INFINITY ”方法有很大的缺点。

“现有的.clamp ” + .clamp_min() + .clamp_max()方法有以下缺点:

  • 它更冗长,例如input.clamp_min(0)而不是input.clamp(0..)
  • 它不支持独占范围。
  • 我们不能在未来添加更多.clamp()的实现(不添加更多方法)。 例如,我们不能支持使用u8边界限制u32值,这是RFC 讨论中要求的功能。 使用.saturating_into()函数可能会更好地处理该特定示例,但可能还有其他示例,其中更多的钳位实现会很有用。
  • 有人可能会在.min().max().clamp_min().clamp_max()之间混淆单侧夹紧。 (使用.clamp_min()钳位类似于使用.max() ,而使用.clamp_max()钳位类似于使用.min() 。)我们可以通过命名单面夹紧操作.clamp_lower() / .clamp_upper().clamp_to_start() / .clamp_to_end()而不是.clamp_min() / .clamp_max() ,虽然那是甚至更详细( input.clamp_lower(0)input.clamp(0..) )。

范围参数方法有以下缺点:

  • 实现比添加.clamp_min() / .clamp_max()更复杂。
  • 如果我们决定不或不能为独占范围类型实现Clamp ,这可能会令人惊讶。

我对“现有的.clamp ” + .clamp_min() + .clamp_max()方法与范围参数方法没有强烈的看法。 这是一种权衡。

@Xeroxe它使函数中发生的分支数量加倍,以适应我们仍然不确定是否存在的用例。 我无法让它出现在基准测试中。

也许额外的分支会被 LLVM 优化掉?

在一侧夹紧

因为夹持是包含在两侧的,所以可以只指定左侧/右侧的最小值/最大值以获得仅一侧的夹持行为。 我认为这是完全可以接受的,并且可以说比.clamp((Bound::Unbounded, Inclusive(3.2)))更好,因为Range*没有适合的类型:

x.clamp(i32::MIN, 10);
x.clamp(-f32::INFINITY, 10.0);

没有性能损失,因为 LLVM 可以轻松优化死角: https :

范围语法会很酷,但clamp足够基本,两个单独的参数很好且易于理解。

也许min / max NaN处理可以自行修复,例如通过更改f32的固有方法的实现? 或者专攻PartialOrd::min/max ? (带有版本标志,假设 Rust 设法找到一种在 libstd 中切换事物的方法)。

@scottmcm你应该查看RangeToInclusive

在进一步思考之后,我发现稳定是永远的,所以我们不应该将“重置 RFC 流程”视为不进行更改的理由。

为此,我想回到实现这一点时的心态。 Clamp 概念上在一个范围内操作,因此使用 Rust 已有的词汇表来表达范围是有意义的。 那是我的下意识反应,这似乎也是许多其他人的反应。 所以让我们再次重申不这样做的论点,看看我们是否可以反驳它们。

  • 所需范围的选择足够新颖,需要一个新的特征,特别是排除了最常见的范围Range

    • 使用@jturner314提供的新实现,我们现在可以对特定的Range*类型添加更多限制,例如Ord + Step以便正确返回互斥范围的值。 因此,即使通常并不真正需要专用的范围钳位,我们实际上可以在这里接受整个范围的范围,而不会影响没有这些技术限制的范围的接口。
  • 我们可以只使用 Infinity/Min/Max 进行一侧夹紧。

    • 这是真的,而且在我看来,为什么这种变化并不是真正的强有力的授权的很大一部分。 我对此只有一个真正的答案,那就是Range*语法在此用例中涉及更少的字符和更少的导入。

既然我们已经驳斥了不这样做的理由,这个评论就缺乏做出改变的任何动机,因为选项看起来是等效的。 让我们找到一些做出改变的动机。 我只有一个原因,即该线程中的一般观点似乎是基于范围的方法改进了语言的语义。 不仅适用于包含双端范围限制,还适用于.min().max()等函数。

我很好奇这个思路是否对其他支持稳定 RFC 的人有任何吸引力。

我认为最好保留当前形式的 Clamp,因为它现在与其他语言非常相似。
当我处理我的拉取请求 #58710 时,我尝试使用基于范围的实现。
但是rust-lang/rfcs#1961 (comment) ) 说服了我,标准形式更好。

我认为在函数上有一个#[must_use]属性是合乎逻辑的,以免混淆那些不习惯 Rust 数字如何工作的人。 也就是说,我可以很容易地感觉到有人在写以下(不正确的)代码:

let mut x: f64 = some_number_source();
x.clamp(0.0, 1.0);
//Proceeds to assume that 0.0 <= x <= 1.0

一般来说,rust 对数字采用(number).method()方法(而其他语言使用Math.Method(number) ),但即使牢记这一点,这也是一个合乎逻辑的假设,即这可以修改number 。 这比任何事情都更符合生活质量。

最近添加了[must_use]属性。
@ Xeroxe 你有没有想出一些基于范围的钳位?
我认为现在的函数最适合 rust 的其他数字函数,并希望再次开始稳定它。

目前,我认为没有任何理由采用基于范围的钳位。 是的,让我们添加 must_use 属性并努力实现稳定性。

@SimonSapin @scottmcm我们可以重新开始稳定过程吗?

正如@jturner314所说,钳制 PartialOrd 而不是 Ord 会很棒,因此它也可以用于浮点数。

我们已经在这个问题中专门提供了f32::clampf64::clamp

这就是我想要做的:

use num_traits::float::FloatCore;

struct Foo<T> (T);

impl<T: FloatCore> Foo<T> {
    fn foo(&self) -> T {
        self.0.clamp(1, 10)
    }
}

fn main() {
    let foo = Foo(15.3);
    println!("{}", foo.foo())
}

链接到游乐场。

PartialOrd不是仅浮动的特征。 具有特定于浮点数的方法不会使自定义PartialOrd类型的钳位可用。

当前的实现需要Eq ,即使它不使用它。

PartialOrd的主要问题是它提供了较弱的保证,这反过来削弱了钳位的保证。 希望将其放在PartialOrd可能对我编写的另一个函数感兴趣https://docs.rs/num/0.2.1/num/fn.clamp.html

这些保证是什么?

一个相当自然的期望是iff x.clamp(a, b) == x然后a <= x && x <= bPartialCmp不能保证这一点,其中x可能无法与ab

今天来到这里寻找模糊记得的clamp()并感兴趣地阅读讨论。

我建议使用“选项技巧”作为允许任意范围和具有多个命名函数之间的折衷。 我知道这在某些人中并不流行,但它似乎在这里很好地捕捉了所需的语义:

#![allow(unstable_name_collisions)]

pub trait Clamp: Sized {
    fn clamp<L, U>(self, lower: L, upper: U) -> Self
    where
        L: Into<Option<Self>>,
        U: Into<Option<Self>>;
}

impl Clamp for f32 {
    fn clamp<L, U>(self, lower: L, upper: U) -> Self
    where
        L: Into<Option<Self>>,
        U: Into<Option<Self>>,
    {
        let below = match lower.into() {
            None => self,
            Some(lower) => self.max(lower),
        };
        match upper.into() {
            None => below,
            Some(upper) => below.min(upper),
        }
    }
}

#[test]
fn test_clamp() {
    assert_eq!(1.0, f32::clamp(2.0, -1.0, 1.0));
    assert_eq!(-1.0, f32::clamp(-2.0, -1.0, 1.0));
    assert_eq!(1.0, f32::clamp(2.0, None, 1.0));
    assert_eq!(-1.0, f32::clamp(-2.0, -1.0, None));
    assert_eq!(2.0, f32::clamp(2.0, -1.0, None));
    assert_eq!(-2.0, f32::clamp(-2.0, None, 1.0));
}

如果这包含在std ,也可以为T: Ord包含一个全面的实现,这将涵盖对一般PartialOrd实现提出的担忧。

鉴于在用户代码中定义clamp()函数当前默认会生成有关不稳定名称冲突的编译器警告,我认为名称“clamp”适合该函数。

我认为, clamp(a,b,c)行为应该与min(max(a,b), c)
由于maxmin对于不落实PartialOrd也不应该clamp
NaN已经讨论过了

@EdorianDark我同意。 min, max 也应该只需要 PartialOrd。

@noonien minmax是从 Rust 1.0 开始定义的,它们需要Ord并且有f32f64
这不是讨论这些功能的合适地方。
在这里,我们只能注意minmaxclamp行为相当,这并不奇怪。
编辑:我不喜欢PartialOrd ,宁愿让float实现Ord ,但这在 1.0 之后不可能再改变了。

这已经合并并且不稳定了大约一年半了。 我们如何看待稳定这一点?

我很想稳定这个!

如果clamp方法名称冲突听起来像是一个问题,我建议在https://github.com/rust-lang/rust/pull/66852#issuecomment -561667812 中的某一点更改名称解析,这会有所帮助也有这个。

@Xeroxe认为这个过程是提交稳定 PR 并要求 libs 团队就此达成共识。 看来 t-libs 超载了,跟不上非 fcped 的东西。

@matklad实际上去年已经在https://github.com/rust-lang/rust/issues/44095#issuecomment -544393395 开始了一项 FCP 提案,但它被卡住了,因为还有一个复选框。

在那种情况下,我认为每年在某个问题上被 ping 一次是可以容忍的。

@Kimundi
@sfackler
@没有船

https://github.com/rust-lang/rust/issues/44095#issuecomment -544393395 还在等待你的关注

自 FCP 启动以来,libs 团队发生了很大变化。 你们对在稳定 PR 中开始新的 FCP 有什么看法? 感觉这不应该比等待此处剩余的复选框花费更长的时间。

@LukasKalbertodt对我很好,你介意开始吗?

在这里取消 FCP,因为 FCP 现在发生在稳定 PR 上: https :

@fcpbot取消

@rfcbot取消

@m-ou-se 提议被取消。

此页面是否有帮助?
0 / 5 - 0 等级