Skip to content

SliceExt: More flexible binary_search_by and binary_search_by_key #34683

Closed
@pnkfelix

Description

@pnkfelix

Someone in #rust-beginners asked why the following code (or something similar) does not compile (playpen):

#[derive(Debug)]
struct Assignment {
    topic: String,
    partition: i32,
}

fn main() {
    let xs = vec![
        Assignment { topic: "abc".into(), partition: 1 },
        Assignment { topic: "def".into(), partition: 2 },
        Assignment { topic: "ghi".into(), partition: 3 },
    ];

    let key: &str = "abc";
    let r = xs.binary_search_by_key(&key, |e| &e.topic);
    println!("{:?}", r.map(|i| (i, &xs[i])));
}

The answer, AFAICT, is that the signature for the fn binary_search_by and fn binary_search_by_key methods in SliceExt are a bit more restrictive to their callers than warranted.

Here is their current signature:

#[unstable(feature = "core_slice_ext",
           reason = "stable interface provided by `impl [T]` in later crates",
           issue = "32110")]
#[allow(missing_docs)] // documented elsewhere
pub trait SliceExt {
    type Item;
    ...
    #[stable(feature = "core", since = "1.6.0")]
    fn binary_search_by<F>(&self, f: F) -> Result<usize, usize>
        where F: FnMut(&Self::Item) -> Ordering;
    #[stable(feature = "slice_binary_search_by_key", since = "1.10.0")]
    fn binary_search_by_key<B, F>(&self, b: &B, f: F) -> Result<usize, usize>
        where F: FnMut(&Self::Item) -> B,
              B: Ord;
    ...
}

Some things to note:

  • The methods are stable, but the trait (and its impl on [T]) are not. This may mean we have room to breathe here.

  • The reason the code is not compiling, as I understand it, is that the bounds on the F type parameter have their lifetimes elided and can be expanded effectively to this:

    F: for <'b> FnMut(&'b Self::Item) -> B,
  • The bound above on F is actually quite a strict restriction on the closure F. It is saying that the closure cannot assume that the given reference to an item will live any longer than the invocation of that closure.

    • The reality about how the binary search procedure works means that in practice one can assume something stronger: one can assume that the references passed to the callback actually live as long as the self reference of the overall binary search invocation.

    • In other words, I am suggesting that the signature could (and perhaps should) look like this:

      fn binary_search_by_key<'a, B, F>(&'a self, b: &B, f: F) -> Result<usize, usize>
      where F: FnMut(&'a Self::Item) -> B, B: Ord, Self::Item: 'a;
    • In this revised signature, the lifetime of each Self::Item reference is quite long-lived.

    • This is enough to allow the original code posted to #rust-beginners to compile.

I have a demonstration of the above in the playpen here: https://is.gd/ocGRgw


I am pretty sure that if we can make the above change to the signature of fn binary_search_by_key, we probably should.

  • We should probably leave the signature of fn binary_search_by alone, since I am not convinced there is much benefit to generalizing that, though it should, theoretically speaking, be possible to make a test case that would only start compile with an analogous generalization.
    • (As demonstrated in the playpen https://is.gd/ocGRgw , both methods can be implemented in terms of an appropriately generalized binary_search_by.)

The only question I have at this point is if we can make the change e.g. in 1.11 or 1.12, if 1.10 is released as is.

(I suspect we can make the change. If I understand the stability rules correctly, no one is allowed to implement or even extend the SliceExt trait outside of libstd, so in changing the signature of a method like this is only going to affect the code making calls to these methods, and I think that the change suggested here will only make code start compiling; it shouldn't cause any code to stop compiling. Unless I am overlooking some subtle time inference issue or some other problem...)

Cc #32110

Metadata

Metadata

Assignees

No one assigned

    Labels

    E-easyCall for participation: Easy difficulty. Experience needed to fix: Not much. Good first issue.I-needs-decisionIssue: In need of a decision.T-libs-apiRelevant to the library API team, which will review and decide on the PR/issue.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions