// archive
Writings
Rust, ownership, borrowing and lifetime
It has been three months since I began my journey with Rust. I started by watching a course video on freeCodeCamp, then I continued by building some simple projects and testing out some frameworks to hone my skills in Rust.Well, yes, Rust has some new concepts that we will talk about in this article and a unique learning curve, which makes it more interesting. However, it is still the same as the other programming languages. You do not have to be scared to learn it.Now, let’s move to some new concepts that were introduced by Rust. OwnershipLet’s say we have thislet num = 1; let word = String::from("Hello"); let num2 = num1; let word2 = word; println!("{}", num); println!("{}", word);If we run this code, Rust will complain in line 8, the word doesn’t own the value anymore. Why is this happening? In line 5, we pass the ownership of the variable to word2.To illustrate it, imagine you have a brother. At your brother’s birthday, you gave him your favourite toy. This process is equivalent to changing ownership in Rust, like how word passes it to word2. Hence, you won’t be able to play with that toy anymore unless you borrow it, which leads to another concept in Rust.After reading the code, you might have a question. Why can the variable num still have a value? For the detailed answer, we might have to understand how Rust stores its data in memory, but let's keep that for another day. To make it short, the process that happens in line 4 is copying the data. Unlike variable word that have type String, num has a type of integer, so copying data is cheap. BorrowingFrom the story, we know that you can borrow the toy from your brother. So does the variable in Rust. There is a concept called borrowing. Let’s take a look at the code below:let word = String::from("Hello"); let word2 = &word; println("{}, {}", word, word2);We can see in line 2 that instead of giving the ownership, we pass the reference to word2. That’s called borrowing in Rust. You can lend your toy without giving it away forever.But take note. There are certain rules in Rust related to borrowing:You can have many variables reading the value at the same time.But only one variable can modify it exclusively if they want to change it. LifetimeNow, imagine if there’s a timer. Rust wants to know, “How long will that toy be around?”That’s called a lifetime.fn playground() -> &String { let toy = String::from("Hello"); &toy }To illustrate, if you are in a playground that borrows you a toy. You can play with the toy in the playground. However, you cannot take it home.In Rust, the variable is gone when the function ends. So you cannot give a reference anymore since the data has already disappeared from memory.
Is Redis the right call?
I've been using Redis for quite a while, and I've been treating it as magic. Database is slow? Add Redis.API is slow? Add Redis.Are sessions slow? Add Redis.Is everything slow? Have you tried Redis?It is like we're using Redis as a performance insurance, but that's not the case.It is indeed that Redis is fast, but it is expensive since it stores data in memory. Well, let's talk about several cases where we shouldn't rely on Redis. Your queries are not optimizedInstead of immediately implementing Redis, why don't we start by optimizing our database queries?Redis can boost application performance, but it won't solve inefficient queries. It may only hide the underlying problem.For example, you might discover that your application is suffering from the N+1 query problem. Before adding Redis, we should first ask: can we reduce the number of queries, add the right indexes, or optimize how we fetch the data?Fix the query first. Then, if the database is still a bottleneck, consider Redis. You have complex queriesAn example of this is a listing page in e-commerce. You are building a page that requires you to query multiple tables, join them, and add where statements. Then, suddenly you found out that the queries are so slow. You started to think about adding Redis as a cache. That might work, but you can quickly end up dealing with complicated cache keys, invalidation logic, and difficult debugging, especially when your listing page has many filters and facets.In this scenario, the better solution might not be Redis. You may need to optimize your database queries or, if the read workload has genuinely outgrown your current database, consider a read-optimized database with CQRS. I want to tell you about this one in this section, but you can find a lot on the internet about the usage of OpenSearch, Algolia, or TypeSense. You use Redis for heavy computationYes, Redis can support some computation using LUA scripts or Redis functions, but it doesn't mean you can put a heavy operation in Redis. Why is this bad? Redis is single-threaded when executing a command operation. Therefore, if you have a heavy operation, it will block other operations, causing a bottleneck. In fact, you should move that operation to another service that runs asynchronously, so it won't block your other operations. I am writing this because I hate Redis. In fact, I love using Redis in my projects since Redis is a great tool and almost helps me with every performance problem. But sometimes we are not using the tool correctly.
What Happens When Rust Compiles a Generic Function?
Like other programming languages, Rust allows us to work with generics and abstractions.In Rust, there are two ways we can handle types that implement a particular trait.The first is when we use a generic type as a function parameter or return value.The second is when we need to work with a concrete type via a single interface, such as a Vec.Although both serve the same purpose, Rust handles them differently.The first approach uses static dispatch, which requires the compiler to know the concrete type at compile time. Meanwhile, the second approach uses dynamic dispatch. We use trait objects to handle this one since the compiler doesn't know the concrete type and size at compile time. However, we won't go deep on this one.Let's get back to the first approach. So, what does it mean for the compiler to know the concrete type at compile time, and what does the compiler actually do?Let's take a look at this example:fn addition(a: T, b: T) -> T where T: Add, { a + b } fn main() { println!("{}", addition(10, 20)); println!("{}", addition(10.5, 20.5)); } When the compiler sees addition(10, 20), it can determine that both arguments are i32. Therefore, for this call, the compiler knows that T = i32. At this point, the compiler knows the exact types used for each call.After the compiler gathers this information, it will continue with a process called monomorphization.The idea is simple. For each function that uses generic types, Rust will generate a specialized version for the concrete types that are actually used.Therefore, for the previous example, conceptually, the compiler will produce something similar to:fn addition_i32(a: i32, b: i32) -> i32 { a + b } fn addition_f64(a: f64, b: f64) -> f64 { a + b } fn main() { println!("{}", addition_i32(10, 20)); println!("{}", addition_f64(10.5, 20.5)); } The advantage of using this is that there is no runtime lookup cost, but it will make the compiled code bigger.