Background
I'm trying to write a helper function for constructing repeating strings with a separator.
// "hello world hello world hello"
"hello".repeat_and_join(3, " world ");
// "?,?,?"
'?'.repeat_and_join(3, ',');
The intended use case is to construct placeholders in sqlx query strings.
let ids = vec![52, 36, 29];
query_as::<_, MyEntity>(
format!(
r#"SELECT * FROM my_entities WHERE id IN ({})"#,
'?'.repeat_and_join(ids.len(), ',')
)
.as_str()
)
Isn't this already supported by sqlx? Unfortunately, not at the time of writing.
Code
I wanted to create a generic method similar to join
, supporting String
, &str
and char
. Unfortunately, rust does not support method overloading - and I don't fully understand the syntax used to implement join
. My attempt therefore makes use of a public method with an enum
parameter and private implementation functions. However, I think this looks a bit ugly on the caller side (having to create the sep
argument using e.g. StringOrChar::Char(',')
) and causes a lot of repetition on the implementation side.
Is there a way to (i) make the caller API not have to use StringOrChar::X()
and (ii) reduce the repeated code?
pub(crate) enum StringOrChar<'a> {
String(String),
Str(&'a str),
Char(char),
}
pub(crate) trait StringExt {
fn repeat_and_join(&self, n: usize, sep: StringOrChar) -> String;
}
impl StringExt for String {
fn repeat_and_join(&self, n: usize, sep: StringOrChar) -> String {
match sep {
StringOrChar::String(sep) => repeat_and_join_str_str(n, self.as_str(), sep.as_str()),
StringOrChar::Str(sep) => repeat_and_join_str_str(n, self.as_str(), sep),
StringOrChar::Char(sep) => repeat_and_join_str_char(n, self.as_str(), sep),
}
}
}
impl StringExt for &str {
fn repeat_and_join(&self, n: usize, sep: StringOrChar) -> String {
match sep {
StringOrChar::String(sep) => repeat_and_join_str_str(n, self, sep.as_str()),
StringOrChar::Str(sep) => repeat_and_join_str_str(n, self, sep),
StringOrChar::Char(sep) => repeat_and_join_str_char(n, self, sep),
}
}
}
impl StringExt for char {
fn repeat_and_join(&self, n: usize, sep: StringOrChar) -> String {
match sep {
StringOrChar::String(sep) => repeat_and_join_char_str(n, *self, sep.as_str()),
StringOrChar::Str(sep) => repeat_and_join_char_str(n, *self, sep),
StringOrChar::Char(sep) => repeat_and_join_char_char(n, *self, sep),
}
}
}
fn repeat_and_join_str_str(n: usize, rep: &str, sep: &str) -> String {
let mut s = String::with_capacity(rep.len() * n + sep.len() * (n.max(1) - 1));
for _ in 0..(n.max(1) - 1) {
s.push_str(rep);
s.push_str(sep);
}
if n > 0 {
s.push_str(rep);
}
s
}
fn repeat_and_join_str_char(n: usize, rep: &str, sep: char) -> String {
let mut s = String::with_capacity(rep.len() * n + (n.max(1) - 1));
for _ in 0..(n.max(1) - 1) {
s.push_str(rep);
s.push(sep);
}
if n > 0 {
s.push_str(rep);
}
s
}
fn repeat_and_join_char_str(n: usize, rep: char, sep: &str) -> String {
let mut s = String::with_capacity(n + sep.len() * (n.max(1) - 1));
for _ in 0..(n.max(1) - 1) {
s.push(rep);
s.push_str(sep);
}
if n > 0 {
s.push(rep);
}
s
}
fn repeat_and_join_char_char(n: usize, rep: char, sep: char) -> String {
let mut s = String::with_capacity(n + (n.max(1) - 1));
for _ in 0..(n.max(1) - 1) {
s.push(rep);
s.push(sep);
}
if n > 0 {
s.push(rep);
}
s
}
3 Answers 3
Warning: I am very new to Rust. At the moment I am not yet familiar with typical Rust habits. I am still transferring a lot of concepts from other languages I know, such as C#, C++ and Python. If you down-vote, please leave a comment so that I can learn from it.
Single responsibility
To me, the name repeat_and_join()
implies a violation of the single responsibility principle. This method does 2 things. I would split that up into 2 methods, one for repeating and one for joining. Create one trait for each of them.
trait RepeatExt {
fn times(&self, n: usize) -> Vec<String>;
}
trait JoinExt {
fn join_with<S: Into<String>>(self, separator: S) -> String;
}
Use std::iter
The iter module helps you getting rid of manually coded loops.
Bringing it together
You can now write one repetition extension for each type (String
, &str
and char
):
use std::iter;
impl RepeatExt for &str {
fn times(&self, n: usize) -> Vec<String> {
iter::repeat((*self).to_string()).take(n).collect()
}
}
impl RepeatExt for String {
fn times(&self, n: usize) -> Vec<String> {
iter::repeat(self.clone()).take(n).collect()
}
}
impl RepeatExt for char {
fn times(&self, n: usize) -> Vec<String> {
iter::repeat((*self).to_string()).take(n).collect()
}
}
and the Join for just String
:
impl JoinExt for Vec<String> {
fn join_with<S: Into<String>>(self, separator: S) -> String {
self.join(&separator.into())
}
}
Testing
All 9 combinations work for me:
fn main() {
println!("{}", "?".times(3).join_with(','));
println!("{}", "?".times(3).join_with(","));
println!("{}", "?".times(3).join_with(",".to_string()));
println!("{}", '?'.times(3).join_with(','));
println!("{}", '?'.times(3).join_with(","));
println!("{}", '?'.times(3).join_with(",".to_string()));
println!("{}", "?".to_string().times(3).join_with(','));
println!("{}", "?".to_string().times(3).join_with(","));
println!("{}", "?".to_string().times(3).join_with(",".to_string()));
}
Evaluation
Is there a way to (i) make the caller API not have to use StringOrChar::X()
I removed that completely.
and (ii) reduce the repeated code?
Well, the 3 repetition methods are still similar, but a lot of duplication was removed using the iteration module. All in all, that's just 33 lines of code, compared to your original 96 lines.
-
4\$\begingroup\$ I believe you are right about the single responsibility principle - I just got upset that both
repeat
andjoin
exist but not a way to bridge them. However, based on your answer I was able to approach the problem differently and findintersperse
. This allows me to writerepeat_n('?', ids.len()).intersperse(',').collect::<String>().as_str()
, without custom implementations \$\endgroup\$Felix ZY– Felix ZY2025年08月12日 18:08:23 +00:00Commented Aug 12 at 18:08 -
\$\begingroup\$ @FelixZY: intersperse is actually a nice name. I didn't know that one yet. \$\endgroup\$Thomas Weller– Thomas Weller2025年08月12日 18:24:28 +00:00Commented Aug 12 at 18:24
-
1\$\begingroup\$ @ThomasWeller: Rust developers tend to be obsessed with performance, so we do not like the idea of avoidable allocations... and therefore returning
Vec<String>
intermediaries when iterators (forVec
) or&str
(forString
) would suffice. \$\endgroup\$Matthieu M.– Matthieu M.2025年08月13日 13:20:17 +00:00Commented Aug 13 at 13:20
Disclaimer: this answer focuses on performance.
Avoid creating a String for format!
The format!
macro does not preserve any of its arguments, and therefore any allocation made just to format are gratuitous and best avoided.
Therefore, rather than formatting a String
and pass it to format, it would be more idiomatic to create an object which implements Display
.
The simplest approach is to stick to a single object, and ignore violations the Single Responsibility Principle.
struct RepeatJoin<R, J> {
count: usize,
repeated: R,
joiner: J,
}
impl<R, J> RepeatJoin<R, J> {
fn new(count: usize, repeated: R, joiner: J) -> Self {
Self { count, repeated, joiner }
}
}
impl<R, J> Display for RepeatJoin<R, J>
where
R: Display,
J: Display
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
let Some(count) = self.count.checked_sub(1) else {
return Ok(());
};
write!(f, "{}", self.repeated)?;
for _ in 0..count {
write!(f, "{}", self.joiner)?;
write!(f, "{}", self.repeated)?;
}
Ok(())
}
}
Which allows you to write your query as:
format!(
r#"SELECT * FROM my_entities WHERE id IN ({})"#,
RepeatJoin::new(ids.len(), '?', ',')
)
And for which a single String
will be allocated, which the performance conscious may even pre-allocate by using write!(&mut my_string, ...)
instead.
Single Responsibility Principle
While one can see the repetition and join as two distinct operations, in this specific case I would just outright ignore the criticism.
Joiners only make sense in the context of repetitions, anyway.
Still, the abstraction can be split if one wishes to.
struct JoinDisplay<I, J> {
iterator: I,
joiner: J,
}
impl<I, J> JoinDisplay<I, J> {
fn new(iterator: I, joiner: J) -> Self {
Self { iterator, joiner }
}
}
impl<I, J> Display for JoinDisplay<I, J>
where
I: Clone + Iterator<Item: Display>,
J: Display
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
let iterator = self.iterator.clone();
for (i, e) in iterator.enumerate() {
if i > 0 {
write!(f, "{}", self.joiner)?;
}
write!(f, "{e}")?;
}
Ok(())
}
}
Your extension trait can be replaced by using the standard library and the well-known itertools
crate:
use itertools::Itertools;
use std::iter::repeat;
fn main() {
let hello_world = repeat("hello").take(3).join(" world ");
println!("{hello_world}");
let question_marks = repeat('?').take(3).join(",");
println!("{question_marks}");
}
No custom extension trait is needed.
The only downside is, that Itertools::join
requires the separator to be a &str
, so you cannot pass in a char
directly, but given your use case, YAGNI.