13 KiB
So, now I'll go through everything I changed and did to make your app faster. Let's start at the top of main.rs
main.rs
#[derive(Parser)]
#[clap(author, version, about, long_about = None)]
#[clap(group(ArgGroup::new("uwu").required(true).args(& ["text", "infile"]),))]
struct Args {
/// Text to uwu'ify
#[clap(short, long, required_unless_present_all = ["infile", "outfile"], display_order = 1)]
text: Option<String>,
/// The file to uwu'ify
#[clap(short, long, parse(from_os_str), conflicts_with = "text", requires = "outfile", value_name = "FILE", value_hint = clap::ValueHint::FilePath, display_order = 2)]
infile: Option<std::path::PathBuf>,
/// The file to output uwu'ified text
#[clap(short, long, value_name = "FILE", value_hint = clap::ValueHint::FilePath, display_order = 3)]
outfile: Option<String>,
/// The modifier to determine how many words to be uwu'ified
#[clap(short, long, value_name = "VALUE", default_value = "1", validator = is_between_zero_and_one, display_order = 4)]
words: f32,
/// The modifier for uwu faces e.g hello -> hewwo
#[clap(short, long, value_name = "VALUE", default_value = "0.05", validator = is_between_zero_and_one, display_order = 5)]
faces: f32,
/// The modifier for actions e.g *shuffles over*
#[clap(short, long, value_name = "VALUE", default_value = "0.125", validator = is_between_zero_and_one, display_order = 6)]
actions: f32,
/// The modifier for stutters e.g b-baka!
#[clap(short, long, value_name = "VALUE", default_value = "0.225", validator = is_between_zero_and_one, display_order = 7)]
stutters: f32,
/// Flag to enable/disable random uwu'ifying
#[clap(short, long, display_order = 8)]
random: bool,
}
clap
works great using it's derive macro implementation, however it's much lighter on resources if you make the clap::App
manually
macro_rules! app {
() => {
clap::App::new(env!("CARGO_PKG_NAME"))
.author(env!("CARGO_PKG_AUTHORS"))
.version(env!("CARGO_PKG_VERSION"))
.about(env!("CARGO_PKG_DESCRIPTION"))
.long_about(None)
.group(
ArgGroup::new("uwu")
.required(true)
.args(&["text", "infile"]),
)
.arg(
Arg::new("text")
.help("Text to uwu'ify")
.short('t')
.long("text")
.required_unless_present_all(["infile", "outfile"])
.display_order(1),
)
.arg(
Arg::new("infile")
.help("The file to uwu'ify")
.short('i')
.long("infile")
.conflicts_with("text")
.requires("outfile")
.value_name("FILE")
.value_hint(clap::ValueHint::FilePath)
.display_order(2),
)
.arg(
Arg::new("outfile")
.help("The file to output uwu'ified text")
.short('o')
.long("outfile")
.value_name("FILE")
.value_hint(clap::ValueHint::FilePath)
.display_order(3),
)
.arg(
Arg::new("words")
.help("The modifier to determine how many words to be uwu'ified")
.short('w')
.long("words")
.value_name("VALUE")
.default_value("1")
.validator(is_between_zero_and_one)
.display_order(4),
)
.arg(
Arg::new("faces")
.help("The modifier for uwu faces e.g hello -> hewwo")
.short('f')
.long("faces")
.value_name("VALUE")
.default_value("0.05")
.validator(is_between_zero_and_one)
.display_order(5),
)
.arg(
Arg::new("actions")
.help("The modifier for actions e.g *shuffles over*")
.short('a')
.long("actions")
.value_name("VALUE")
.default_value("0.125")
.validator(is_between_zero_and_one)
.display_order(6),
)
.arg(
Arg::new("stutters")
.help("The modifier for stutters e.g b-baka!")
.short('s')
.long("stutters")
.value_name("VALUE")
.default_value("0.225")
.validator(is_between_zero_and_one)
.display_order(7),
)
.arg(
Arg::new("random")
.help("Flag to enable/disable random uwu'ifying")
.short('r')
.long("random")
.display_order(8),
)
};
}
Here, I've created a declarative macro describing your clap::App
. You can think of a declarative macro like using the compiler to copy and paste something in your code. You can read more on them here. You'll notice that for some of the attributes of your clap::App
, I'm using the env!()
macro in place of literal strings. All the env!()
macro does is it grabs an environment variable at compile time, and pastes it into wherever the macro is located in the code. You can find docs for all the environment variables Cargo sets here. Now, we move on to fn main()
fn main() {
let matches = app!().get_matches();
match UwUify::new(
matches.value_of("text"),
matches.value_of("infile"),
matches.value_of("outfile"),
matches.value_of("words"),
matches.value_of("faces"),
matches.value_of("actions"),
matches.value_of("stutters"),
matches.is_present("random"),
)
.uwuify()
{
Ok(_) => (),
Err(err) => {
app!().error(ErrorKind::DisplayHelp, err).exit();
}
}
}
This is how I've implemented the main function for this app. You'll notice a few things are different about it.
- I'm using my
app!()
macro in place ofArgs::parse()
UwUify::new()
is taking exclusivelyOption<&str>
values and 1 bool for therandom
variable (we'll get to that later)- If the app fails, and I want to print the
err
message, instead of callingerr.to_string()
, I'm simply passingerr
into theapp!().error()
function.
I'm doing this using a Rust feature called Traits. Traits describe the capabilities of a given type. Instead of app!().error()
taking a String
, app!().error()
actually takes any type that implements Display
. This trait suggests that the type has the capability of expressing itself as a human-readable string. In your original implementation, UwUify.uwuify()
returned a Result<(), Box<dyn Error>>
. You'll notice that your error type is dyn Error
, which translates to any type that implements the Debug
trait and the Display
trait, that can also produce the source of the error, a backtrace, a description of what happend, as well as the cause of the error. The key thing to note is that according to this description, all types that implement Error
also implement the Display
trait, and can therefore be passed to app!().error()
without any conversion.
lib.rs
If you're making an application that is modular in the way that yours is, it's standard that your external module be called lib.rs
, and that it be annotated in Cargo.toml
like so
[lib]
name = "uwuify"
Doing this allows for 2 things
- People can embed your app's functionality into their own apps through
crates.io
- This improves compile times significantly depending on how big your modules actually are
We'll start with your actual UwUify
struct
#[derive(Debug)]
pub struct UwUify<'a> {
text: &'a str,
input: &'a str,
output: &'a str,
words: f64,
faces: f64,
actions: f64,
stutters: f64,
random: bool,
linkify: LinkFinder,
}
impl<'a> Default for UwUify<'a> {
fn default() -> Self {
Self {
text: "",
input: "",
output: "",
words: 1.0,
faces: 0.05,
actions: 0.125,
stutters: 0.225,
random: false,
linkify: LinkFinder::new(),
}
}
}
Here, what I've done is I've removed your Modifiers
sub-struct (for reasons that will become clear later), I've implemented the Default
trait (The Default
trait just describes what your struct should look like in it's default state), and I've changed the fields that used to take String
types into taking &str
types. This was one of the factors slowing down your code.
Let's talk about allocations
In Rust, there are 2 types of memory
The stack is where the majority of your variables are located. This is one way to visualize the stack
[]
[]
[]
[]
On the stack, variables can be pushed onto it
[]
↓
[]
[]
[]
[]
And removed by popping them off
[]
↑
[]
[]
[]
[]
This is the best of memory that Rust offers. Not only is the heap fast, but it's also very efficient. You can think of the heap like New York City, where everyone is packed in shoulder to shoulder. Every variable is put on the stack automatically by Rust, except for some exceptions, which are allocated on the heap
The heap is like a desert, when you put a variable on the heap, it's put in a space in memory where it isn't packed tight between other variables.
Consider the following slice of memory
00000000000[00100100011111010010]0000000000000000000
Annotated in brackets is where are variable resides. You'll notice that it has room to grow if we need to make it bigger
00000000000[001001000111110100100100010010110]000000
The heap exists for types that don't have a known size at compile time.
- Because the amount of elements on a
Vec
can change, they are allocated on the heap - Because we know that
i32
takes up 32 bits in memory, and it can never take more memory than that, it's pushed onto the stack.
If you want your app to be FAST, your best bet is to avoid the heap as much as possible.
Now, let's get into the 2 types of strings in Rust, String
and &str
. A String
is a growable array of charecters while a &str
describes the location of an array of charecters.
Say theoretically, you were to enter a chatroom with one of these types, and you needed to know their value, this is how each type would respond
/---------------------------------------\
| Yo, &str what's you value? |
\---------------------------------------/
/---------------------------------\
| Idk, go ask the String |
| at [location] |
\---------------------------------/
/-----------------------------------------\
| Yo, String what's you value? |
\-----------------------------------------/
/---------------------------------\
| My value is "UwU |
| *nuzzzles you" |
\---------------------------------/
You get the idea, the point is, the String
type is growable, so it's allocated on the heap. And because the size of pointers (pointers point at locations in memory) are known at compile time the &str
type is pushed onto the stack. Therefore, you should use &str
over String
whenever possible.
Let's look back at your UwUify
struct
#[derive(Debug)]
pub struct UwUify<'a> {
text: &'a str,
input: &'a str,
output: &'a str,
words: f64,
faces: f64,
actions: f64,
stutters: f64,
random: bool,
linkify: LinkFinder,
}
You'll notice that not only does it take the &str
type over String
now, it takes a type of `&'a str. This is because you cannot have borrowed types in structs without first annotating the lifetime of the reference. Lifetimes and ownership are waaaaayyy to complicated to explain in a PR, so I recommend watching this video if you want to understand more about them.