Pycub
13 subscribers
3 photos
2 files
10 links
Experiences of a purely functional mind in a statically typed body.

https://github.com/pycub
Download Telegram
# Rust module system

What is a module?
A module is a collection of items: functions, structs, traits, impl blocks, and even other modules.

Visibility
By default, the items in a module have private visibility, but this can be overridden with the pub modifier.
Only the public items of a module can be accessed from outside the module scope.
Box<T>

The most straightforward smart pointer is a box. Boxes allow you to store data on the heap rather than the stack. What remains on the stack is the pointer to the heap data.
Cases you would use Box smart pointer:
- When you have a type whose size can’t be known at compile time and you want to use a value of that type in a context that requires an exact size
- When you have a large amount of data and you want to transfer ownership but ensure the data won’t be copied when you do so
- When you want to own a value and you care only that it’s a type that implements a particular trait rather than being of a specific type

fn main() {
let b = Box::new(5);
println!("b = {b}");
}


use crate::List::{Cons, Nil};

fn main() {
let list = Cons(1, Cons(2, Cons(3, Nil)));
}


#Rust #smartpointer
Computing the Size of a Type

Non-Recursive
Given
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}

To determine how much space to allocate for a Message `value, Rust goes through each of the variants to see which variant needs the most space. Rust sees that `Message::Quit `doesn’t need any space, `Message::Move needs enough space to store two i32 values, and so forth. Because only one variant will be used, the most space a Message value will need is the space it would take to store the largest of its variants.

Recursive
Contrast this with what happens when Rust tries to determine how much space a recursive type like the List enum needs. The compiler starts by looking at the Cons variant, which holds a value of type i32 and a value of type List. Therefore, Cons needs an amount of space equal to the size of an i32 plus the size of a List. To figure out how much memory the List type needs, the compiler looks at the variants, starting with the Cons variant. The Cons variant holds a value of type i32 and a value of type List, and this process continues infinitely

Because Rust can’t figure out how much space to allocate for recursively defined types, the compiler gives an error with this helpful suggestion:

help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to break the cycle
|
2 | Cons(i32, Box<List>),
| ++++ +


enum List {
Cons(i32, Box<List>),
Nil,
}

use crate::List::{Cons, Nil};

fn main() {
let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));
}


# Rust
On one hand, I have always had this tendency to don't skip leg days!
On the other hand, the whole point of life is how you manage trade-offs! To me, writing a code that I don't fully understand, is sort of a leg day; hate to skip it. However, an engineer, at its most shallow trait layers, must be capable of examining the cost he is gonna face. Everything is cost though.
And now I've got this urge to move on to web3 integration in Rust to handle a project that is so close to its deadline. I have to put off the above bookmark 🤦🏻‍♂️
"...Break the cycle, Morty. Rise above. Focus on science."
It's Sep 22 23:36 and I have witnessed the most weird debugs of my life.
OT was hammering the keyboard for almost 5 hours which we were surprised by. It's a funny and ridiculous bug in the Jalali Calendar of a famous CRM.
Apart from the actual bug, the funny part is, that this bug would never have occurred if OT had not been testing it today cause 22 Sep was the edge case to produce this bug, let's just imagine, for a sec, what would it take for a customer of this CRM to realize this bug? let alone the report? Who would believe such a bug would exist out of nowhere?

Moral story: some bugs live for a day, seize the day and fix them 😂
https://github.com/pycub/thorax

I just finished the 0.1.0 version of Thorax.
Thorax is a tiny tool in the large system of a healthcare project. HL7 information needs to be encrypted/decrypted and here is the Thorax.
Tomorrow is a great day in which I'm going to develop this sunshine further.
(Writing test is a must!)
A development container allows you to use a container as a full-featured development environment. It can be used to run an application, to separate tools, libraries, or runtimes needed for working with a codebase, and to aid in continuous integration and testing.

https://containers.dev
Cargo Watch watches over your project's source for changes, and runs Cargo/Shell commands when they occur.

- run cargo test after each change:
cargo watch -x test


- run cargo run after each change:
cargo watch -x run


OPTIONS:
-x Cargo command(s) [default: check]
-s Shell command(s)


https://crates.io/crates/cargo-watch
Let’s talk a little bit about pytest and Django.

To me the why behind using pytest is:
- empowering fixture mechanism
- assertion introspection
- parameterized tests
- rich test discovery

If you don’t know how exactly these items are getting to work you better take a close look at pytest’s source code. It’s cool and awesome to make you a better Python code reviewer.

Great! So let’s put pytest working in integration with Django. An interesting tool called ‘pytest-django’ bridges the use of pytest in conjunction with Django.

Two major tools I use often:
1. django_db_setup fixture that is automatically invoked before any tests that require access to the database. It is session-scoped which means it only gets fired once for the entire test session.
2. pytest.mark.django_db This is used to mark a test function as requiring the database. It will ensure the database is set up correctly for the test. Each test will run in its transaction which will be rolled back at the end of the test.

With these tools in your pocket, you can use django_db_setup to pre-populate your test database and use pytest.mark.django_db to run each of your tests within its transaction and roll back the changed data. It's super interesting to me!!

Later on, when I get adequate time, I will talk about how marks are different from fixtures.
And remember, as an engineer nothing can help you like tests. Embrace them!
Rust Type system

When reading the official Rust reference, you will be faced with a word item. To fully grasp the idea behind the Rust type system, you need to understand what are items in the first place.
An item is simply any declaration that could appear globally in a program or module, such as a fn, struct, or use.

items are:
- entirely determined at compile-time
- generally remain fixed during execution
- and may reside in read-only memory

A very good question to ask yourself is: what does type tell me?
- the interpretation of the memory holding it
- the operations that may be performed on the value

Huh? Isn't `interpretation of the memory` fancy?
I'm a true believer in simplicity, however, there are times when nice people out there tend to point to a complex concept with a name. `interpretation of the memory` is one of them. Lemme put the light on it.

The phrase "interpretation of the memory" refers to how the raw bytes stored in memory are understood and treated by the program. This concept is fundamental!
At the lowest level, computer memory is just a sequence of bytes. These bytes don't inherently have any meaning - they're just numbers from 0 to 255.
The type system provides rules for how to interpret these raw bytes as meaningful data. Different types have different memory layouts.

In essence, "interpretation of the memory" refers to the rules and mechanisms that Rust uses to ensure that the raw bytes in memory are consistently and correctly understood as the data types they represent, maintaining safety and correctness throughout the program's execution.

Next time a geek talking about type system, you know what are they talking about :)
Today, a nerd colleague interested in Rust saw a code I wrote for AdventOfCode2024. He asked me about Ok(()) in the return of function.
As this topic is something I really wanted to write a mini blog for, here we go then:

In Rust, the
Result<T, E>

enum is used to represent either a successful computation
Ok(T)

or an error
Err(E)


It's a super common pattern for error handling.
How can this help me handle my errors?
Take a look at the following code:

fn foo() -> Result<(), Box<dyn Error>> {
Ok(())
}


First, I should explain what dyn Error means.
It's a trait object, allowing us to handle any error that implements the Error trait without knowing the specific type. super cool huh?

However, trait objects can't be used directly as return types because they have dynamic sizes. That's where Box comes in. By boxing the trait object, we get a managed pointer to it, which has a known size and can be returned.

So, after all these explanations, we can defer why we use Ok(()) instead of () right? It's about making a tuple (), a Result enum by wrapping the Ok.

This pattern of wrapping values and chaining operations might remind functional programming enthusiasts of something familiar. In functional languages like Haskell, we often work with monadic structures that allow us to seamlessly chain computations while managing potential errors. The Result type in Rust provides a similar elegant approach to error handling, letting us compose functions that can gracefully propagate and handle errors.
...make invalid states inexpressible in
your types.

OK. Let's break it down.
What does `invalid states` mean? Invalid states are conditions that should not logically exist in a program, as they can lead to errors or unexpected behavior.
By preventing these states from being represented, you ensure that the program remains in a consistent and safe condition.

Suppose you're modeling a `Payment` type that can either be in a Pending or Completed state.

If you want to ensure that a Completed payment always has an associated amount, you can make invalid states inexpressible by refining the type:
enum Payment {
Pending,
Completed { amount: u64 },
}

Now, it's impossible to create a Completed payment without specifying an amount. This makes the code safer and more expressive.

By following this paradigm you can leverage the great Rust compiler to even check your business logic :)
It is a bright idea to use compiler this way!
Some interesting notes about Cell type in Rust

The Cell type in the standard library is an interesting example of safe interior mutability through invariants.

It is not shareable across threads and never gives out a reference to the value contained in the Cell. Instead, the methods all either replace the value entirely or return a copy of the contained value.

Since no references can exist to the inner value, it is always okay to move it. And since Cell isn’t shareable across threads, the inner value will never be concurrently mutated even though mutation happens through a shared reference.

More on:
- Interior mutability
Marker Traits

Usually, we use traits to denote functionality that multiple types can support; a Hash type can be hashed by calling hash, a Clone type can be cloned
by calling clone, and a Debug type can be formatted for debugging by calling fmt.

"But not all traits are functional in this way."
Wow!! How?

Some traits, called marker traits, instead indicate a property of the implementing type.
Marker traits have no methods or associated types and serve just to tell you that a particular type can or cannot be used in a certain way.

Marker traits serve an important purpose in Rust - they allow you to write bounds that capture semantic requirements not directly expressed in the code.

There is no call to send in code that requires that a type is Send.

Instead, the code assumes that the given type is fine to use in a separate thread, and without marker traits the compiler would have no way of checking that assumption. It would be up to the programmer to remember the assumption and read the code very carefully, which we all know is not something we’d like to rely on. That path is riddled with data races, segfaults, and other runtime issues.

So...
Rust compiler supports you in a way that it checks your types semantics to see whether the type support Send or not. Without specifically implementing the Send; just by using marker traits!
Lovely isn't it?

#RustForRustaceans #Types
When writing a library in Rust, there are many strict rules you better be aware of.
What we don't really want is to break the downstream code.

One way of causing these breaking changes is exposing foreign types.

Your library be like:
// Your Library Crate 1.0 (Cargo.toml)
// [dependencies]
// uuid = "1.0"

pub struct UserRecord {
pub id: uuid::Uuid, // Exposing foreign type
pub name: String,
}

impl UserRecord {
// Return type uses foreign type
pub fn generate_new() -> uuid::Uuid {
uuid::Uuid::new_v4()
}
}


Your library code user be like:
// User's Crate (Cargo.toml)
// [dependencies]
// your_lib = "1.0"
// uuid = "1.0" // Same version you use

fn main() {
let id = your_lib::UserRecord::generate_new();

// User can freely mix types because versions match
let their_id = uuid::Uuid::new_v4();
let combined = id.merge(their_id); // Works fine
}


Then, you decide to to upgrade the uuid package like:
// Your Library Crate 2.0 (Cargo.toml)
// [dependencies]
// uuid = "2.0" // Major version update

// Same code as before, but using uuid v2.0
pub struct UserRecord {
pub id: uuid::Uuid, // Now v2.0 type
pub name: String,
}

impl UserRecord {
pub fn generate_new() -> uuid::Uuid {
uuid::Uuid::new_v4()
}
}


Now your library user code is broken! See:
// User's Crate (UNCHANGED)
// [dependencies]
// your_lib = "2.0"
// uuid = "1.0" // User hasn't updated

fn main() {
let id = your_lib::UserRecord::generate_new();

// TYPE MISMATCH ERROR!
let their_id = uuid::Uuid::new_v4();
let combined = id.merge(their_id);
// ^^^
// Error: expected `uuid::v2::Uuid`, found `uuid::v1::Uuid`
}


Let's process why this is a breaking change:
1. Your interface exposed uuid::Uuid (a foreign type)
2. You updated to uuid v2.0 (new major version = different type)
3. User's code still uses uuid v1.0 (different type despite same name)
4. Now there's a type mismatch between your Uuid (v2.0) and theirs (v1.0) Boom!


So by now we learned that Rust treats different major versions as entirely distinct types! Meaning, Even if the type name is identical (uuid::Uuid), v1 and v2 are incompatible!


Well, I don't like not to upgrade my external crates just because it may break downstream codes. At the end, they are dependent of my code, not my code dependent on their code. LOL,

Solution Strategies:
1. Hide foreign types behind your own types (newtype pattern).
2. Re-export dependencies (pub use uuid::Uuid) to enforce version matching.
3. Be cautious with major version updates if exposing foreign types.
4. Treat dependency updates as breaking changes if foreign types are exposed!
I have been reading the Jon Gjingset awesome book lately and he recommended writing changelogs.

So here we go:

What is a changelog?

A changelog is a file which contains a curated, chronologically ordered list of notable changes for each version of a project.

Why keep a changelog?
To make it easier for users and contributors to see precisely what notable changes have been made between each release (or version) of the project.

Who needs a changelog?
People do. Whether consumers or developers, the end users of software are human beings who care about what's in the software. When the software changes, people want to know why and how.

Here is a great guideline on how to write a simple, practical, and good-format changelogs.
https://keepachangelog.com/en/1.1.0/