Follow

Sometimes programming jargon is weird. I just told my wife "I realized I was having a problem because I'd forgotten to put my strings in a box".

@codesections uhm… But strings are already boxed, it's what they are as opposed to slices.

More details than the joke deserves 

@isagalaev

Disclaimer: I don't actually know Rust—this came up while I was working on Ch12 of The Book, and I'm still only on Ch15.

Yeah, that wasn't a very accurate way to put it, as it turned out. Here's how it came up: I was working with a function with a string slice (&str) `input` parameter. What I *wanted* to do (in pseudo code) is:

if the_case_insensitive_flag_is_set {
input = input.to_lowercase();
}

(1/2)

rust code 

@isagalaev
But I can't do that, because `input.to_lowercase()` results in a String instead of an &str—which feels suspiciously like silent type coercion, even if it isn't.

I have the intuition that Boxes are involved in my error, even if it's not right to say that I need to put my String in a Box. But I also haven't figured out exactly how to do it that way (I went with a structure more like the example, though I'd still like to figure out how to do it my way when I know Rust better.)

rust code 

@codesections I think I can explain that :-)

It's not a silent type coercion because to_lowercase explicitly accepts &str and returns a String. It has no other choice, really, because it has to create new data, it can't change the input slice in place. String is an object that owns this new data (allocated on the heap, aka boxed), as opposed to &str which would is only a reference to someone else's data.

1/2

rust code 

@codesections now, if your function wants to do an optional lowercase, you'll have to reassign your input argument, to avoid ambiguity of it being either an original &str or a new lowercased String. A simple way to do that is to ensure it's always a String:

let input = if case_insensitive {
input.to_lowercase()
} else {
input.to_owned()
};

.to_owned() does what it says, it creates a new owned data (String) out of an &str.

2/2

rust code 

@codesections … however we lose some nits of performance here because we copy data even when we don't have to (.to_owned()), just to make it the same type. We could unify both cases to &str instead and avoid copying when possible:

let tmp_string;
let input = if case_insensitive {
tmp_string = input.to_lowercase();
&tmp_string
} else {
input
};

Note we need to declare tmp_string outside of the `if` block because if declared inside it won't survive past it.

rust code 

@codesections this, in a nutshell, why #rust is harder than any high-level dynamic language: it wants you to think about how things live in memory. In something like Python/Ruby/JavaScript it's all simplified to "everything in heap, always copy".

rust code 

@isagalaev

After getting some help in the -beginners/understanding how rust handles strings a bit better, I ended up going with:

target
.lines()
.enumerate()
.filter(|line| match case_sensitive {
true => line.1.contains(query),
false => line.1.to_lowercase().contains(&query.to_lowercase()),
})
.collect()

which avoids all the reference issues and—at least to my eye—is a lot more expressive to boot.

rust code 

@codesections sure! I didn't see your entire problem, but I suspected that a pipeline of iterators would be a better solution :-)

One thing that irks me aesthetically here is the fact that you have two identical applications of .contains() differing only in two different types. Which could be worked around by introducing a generic wrapper function.

rust code 

@isagalaev
I thought about that. But I concluded that any complexity I'd save here would be made up for by having the same logic in a wrapper function—and I'd pay the complexity of additional indirection. But maybe it'd be worth it ¯\_(ツ)_/¯

rust code 

@isagalaev
(Just to be clear, is something like this what you meant by a wrapper fn?):

let search_filter = |text_line: (usize, &str)| match case_sensitive {
true => text_line.1.contains(query),
false => text_line.1.to_lowercase().contains(&query.to_lowercase()),
};
target
.lines()
.enumerate()
.filter(|&line| search_filter(line))
.collect()

rust code 

@codesections no, I meant writing a function like `fn contains<T>(input: T)`, where T might be either &str or String. It should probably be possible with Cow<> (doc.rust-lang.org/stable/std/b) but I haven't worked enough with it myself to write it quickly :-)

rust code 

@isagalaev

Thanks! That's similar to a something else I heard in —that, once I get a bit more comfortable, I should learn about doing this with a Cow (which either isn't covered in The Book or, anyway, I haven't gotten to yet). I'm going to re-listen to the Cow episode of the New Rustacean later today and see if I can take another crack at more refactoring. This is fun!

Thanks again for your tips :)

@codesections My wife has that same problem with all of her knitting stuff. I always find it on the couch.

Sign in to participate in the conversation
Fosstodon

Fosstodon is an English speaking Mastodon instance that is open to anyone who is interested in technology; particularly free & open source software.