Make a function accept either &[&str] or &str as an argument

I'd like to be able to create a function that accepts either

  • a collection of strings &[&str] (an array/vector of strings) or
  • the &str itself

but I don't understand how to achieve both at the same time, below is an example of a function argument with trait bounds accepting slices of strings, but that it's a slice, so str isn't accepted as it's a string slice.
Is there a way to force the function to somehow automagically convert the incoming &str to &[&str], which would make it a single-element slice and make the operations on it within the function the same?

There is this forum post that explains how to limit a generic type to either A or B specific type, is this the only option here or is there something more elegant?

use std::fmt::Debug;
fn main() {
  let arr1      	:   [&str;3]	=  ["ar1", "ar2", "ar3"]; let arr1_slice = &arr1;
  let vec1      	:Vec<&str>  	= vec!["v1", "v2", "v3"]; let vec1_slice = &vec1[..];
  let str1_slice	:  &[&str]  	= &["s1" , "s2" , "s3" ];
  let str2      	:    &str   	= "str2"                ; let str2_slice  = &[str2];
  let str3      	:    &str   	= "str3"                ;
  str_gen(arr1_slice);
  str_gen(vec1_slice);
  str_gen(str1_slice);
  str_gen(str3);  // ERROR
}
fn str_gen<T>(slice:&[T])
where T: AsRef<str> + Debug {
  // ↑ how to make it also accept str3? Ok if it's AUTOconverted to &[str3]: how is this type of conversion called (not casting since collections are not primitive type? just type coercion?) and how to implement it here?
  // Or is this the only way: Limit a generic type to be either A or B users.rust-lang.org/t/limit-a-generic-type-to-be-either-a-or-b/66367/8
  println!("{:?}", slice)
}

Rust playground link

Thanks!

Note that &str is unsized (or more exactly, dynamically sized), so it's not possible to accept (or create) a value of type &[str]. You are probably thinking about &[&str].

Also note that you can very easily convert a reference to a value to a single-element slice using std::slice::from_ref(), so you could just accept &[&str] unconditionally, and then the caller could pass in slice::from_ref(the_ref_to_str).

3 Likes

that would be similar to let str2_slice = &[str2]; (where str2 is &str)?
But anyway, that's precisely what I'd like to (understand how to) avoid — forcing the caller do any conversions instead of having a very simple API in the same function: passing either a single string or a collection of strings

yes, thanks!

I personally wouldn't call that "simple".

In this case, the user has to understand what types exactly are permitted here, and, since &str and &[&str] doesn't have really much in common, the function signature would be... weird, in the best case. It would be hard to understand whether you can expect only these two types, or you can use something else. Or it will be fairly boilerplate-y on your side. Or both.

The best bet might be to make an enum with two variants, implement From<&str> and From<&[&str]> for it (each using the correspondent variant), and then accept impl Into<YourEnum> - this might be more or less understandable.

8 Likes

Alternative proposal:
It might be possible to do something like this:

fn do_the_thing<'s>(
     strings: impl IntoIterator<Item = &'s str>
) {
    for string in strings {
        // use each string 
    }
} 

fn main() {
    do_the_thing([]); // 0 strings
    do_the_thing(["hello"]); // scales to 1 string...
    do_the_thing(["hello", "world"]); //... and beyond. But not to infinity. That's for Buzz Lightyear and true Turing Machines. 
}

This works because [T; N] has an impl IntoIterator<Item = T> and so conveniently it will work with arrays of any length.

2 Likes

this would still allow the caller to use str_gen("some_string") and then seems to be identical to the solution in the forum post link from my initial post?

I was experimenting with that alternative earlier (before trying to answer the current question) and it seems fine with accepting &str or i32 (see playground link with the &str converted to an owned String (in this example an array of [String;1])

But now when trying to accept slices e.g. an array of &str then after sprinkling compiler-suggested lifetimes throughout, I still get in trouble with the complire unable to infer a lifetime due to conflicting requirements
(see this playground example)

I guess all the &str must be converted to owned values to fix it, right? Or is there some lifetime magic that would allow not to do that and still force the either &str or a slice of &str types?

Thanks for the alternative, though this one doesn't accept &str, is it possible to impl IntoIterator for &str manually that would just convert a single &str to e.g. an array with one element [&str]?

fn do_the_thing<'a>( strings: impl IntoIterator<Item = &'a str> ) {
  for string in strings { println!("{:?}", &string); }
}

fn main() {
  do_the_thing("1&str"); // not an iterator
}

No. You can't implement a trait for a type if either the type or the trait isn't your own.

1 Like

No, but you can implement your own trait for &str and accept that. Like this:

trait AsStrSlice {
    fn as_slice(&self) -> &[&str];
}

impl AsStrSlice for &str {
    fn as_slice(&self) -> &[&str] {
        std::slice::from_ref(self)
    }
}

impl<const N: usize> AsStrSlice for [&str; N] {
    fn as_slice(&self) -> &[&str] {
        self
    }
}

impl AsStrSlice for &[&str] {
    fn as_slice(&self) -> &[&str] {
        self
    }
}

fn print_as_slice(slice: impl AsStrSlice) {
    let slice = slice.as_slice();
    println!("{:?}", slice);
}

fn main() {
    print_as_slice("single string");
    print_as_slice(["string one", "string two"]);
    print_as_slice(vec!["string one", "string two"].as_slice());
}
["single string"]
["string one", "string two"]
["string one", "string two"]

Playgound

If you want to accept Vec<&str> as well you can just provide an implementation of AsStrSlice for Vec<&str>.

The good thing about this approach is that your users can implement the trait for their own types as well.

9 Likes

Oh, thank you, that's exactly it!!
I was mentally stuck on trying to find an existing unknown to me "converter" only for a &str input and missed the more general and solid (and more extensible) approach of using a custom converter via a user trait for each input!
Last thing - if I wanted to also allow String in there, I'd just have to just add another impl, or can I snuck in some where T: AsRef<str> somewhere?

I think you'll have to implement it specifically for String since a generic implementation for AsRef<str> will likely produce conflicts.

1 Like

Where do you expect your users to get a &[&str]? In other words, what are you trying to do, with more context?

I'm toying with wasm-bindgen and am translating a simple web extension template to Rust.
This template uses async web extension storage local APIs (storage.local - Mozilla | MDN) to get a key/value pair (key is hardcoded and also set by the extension template as well)
This API to get values from storage local accepts either

- key           string  or
- keys array of strings or
- keys object specifying default values

I thought it'd be great to mimic the API and create a function that also accepts either

  • a &str or
  • a collection &[&str]

(I've ignored the last part with an object as it seems too complicated to implement it in the same function,
I'm only accepting either a HashMap/BTreeMap (via a GenericMap trait from this SO answer) that is then translated to a JS object in the set local storage API that only accepts an object, not keys; but if you have an idea how to cram a HashMap in there as well, I'm curious :slight_smile: )

Hence my original question.

Currently the user is also me :slight_smile: and I'm hardcoding the keys, but the hypothetical actual user could also do something similar — define a list of keys that would be needed for his extension and then create (sub)collections of these keys to retrieve their corresponding values
For example, to show current configuration values he'd

  • submit the whole collection of keys to the storage local API via one call
  • parse the retrieved serde Map
  • show the retrieved values

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.