search


Understanding Rust LifeTime
Thu Jul 04 00:00:00 UTC 2024

Rust’s lifetimes can be a bit challenging at first, but they are a powerful feature that ensures memory safety without needing a garbage collector. Let’s dive into the concept with some explanations and examples.

What are Lifetimes?

Lifetimes are a way of expressing the scope during which a reference is valid. In Rust, every reference has a lifetime, which is the span during which that reference is allowed to be valid. The Rust compiler uses lifetimes to ensure that you never have invalid references, preventing issues like dangling pointers.

Why Lifetimes?

Lifetimes prevent data races and ensure that references do not outlive the data they point to. This is particularly important in a systems programming language like Rust, where direct memory management is involved.

Stack vs Heap data

Rust lifetime make sure stack allocated memory that are referenced outside of its scope results in a compile time error.



What is good code?
Wed Jun 26 00:00:00 UTC 2024

I once asked a friend what is good code. He said he doesn't know but bad code is always code someone else wrote. This was said in jest, but it demonstrates an underlying truth: it is subjective. What is easy to understand to one person is incomprehensible to another person because they don't share the same mental models. This problem compounds in teams of people who don't share common mental models. Domain Driven Design (DDD) can mitigate some subjectivity by providing a shared vocabulary and model for the domain, but there are many ways to decompose a domain. Thus, DDD is still subjective. An objective way to measure good/bad code is by analyzing the dependency graphs. If the dependency graph looks like spaghetti, it's bad code because small changes have a cascading domino affect through the whole system. The dependency graph of good code will have clear layers

This metric of measuring the complexiety of the dependency graph is encapsulated in the mantra: 'High Cohesion And Loose Coupling'. This design principle is explained well in many software design books so I will be brief. Coupling is the molasses that prevents software system from adapting quickly to changing requirements. A small change in one module can have a cascading domino effect other modules because of tight coupling. This is why we want loose coupling between modules. Cohesion, on the other hand, is tight coupling within a module. We want to maximize coupling within a module but minimize coupling between modules. High cohesion (or tight coupling within a module) means small changes within a module does not cascade outside the module. Another name of this is encapsulation but this is not the encapsulation espoused in OOP of hiding data behind getters and setters which is a bad idea

It is easy to say 'High Cohesion And Loose Coupling' but this is not easy to do. No software engineer would disagree with this principle but why do we still build systems with dependency graphs that look like spaghetti? I will borrow the statistical mechanics explaination of why entropy of a closed system is more likely to increases over time: There are many more ways to write tightly coupled spaghetti code then there are in writing loosely coupled layered code. Statistically, spaghetti code (high entropy) is more likely because there are just more ways of getting to a spaghetti state than to a loosely coupled layered state.

I think there's another reason spaghetti is more likely: naming before structure. Naming things is very important but too much emphasis is put in naming things. When names are assigned too early in the development process, they can impose a preconceived organizational structure that may not align with the actual dependencies and relationships within the code. Structure should dictate names. Names should not dictate structure. For example, in Ruby on Rails everything is put into a MVC structure. The code quality of a system can be evaluated without even knowing the names of modules, functions or understanding the domain simply by visually inspecting the dependency graph. Structures and names will change and evolve. I first focus on structure and will shift priority to names when I am happy with structure. I don't let names bias and dictate structure. My heuristic for evolving structures is start with a simple low entropy structure. Entropy will naturally increase over time so its important to start with low entropy. In concrete terms start with one module (aka namespace). Put all the code in one namespace. Don't worry at the initial stages whether its a controller, a domain entity, a view or a bunch of other names. Keep everything together. Resist the temptation to create multiple namespaces until you have more code. This is low entropy. When the namespace gets too large, find code that have dependencies on each other (high cohesion) and move that cohesive code into its own namespace. The entropy of your system has increased because now you have two namespaces instead of just one. However the entropy within your namespaces has decreased because now you have less code in each namespaece. Coincidentally (or not), biological systems decrease internal entropy by increasing entropy in the universe. Perhaps using biology as a guide of reducing internal entropy while increasing entropy in the universe, our code can be as adaptive and responsive to change as biological systems.

Taking the biology metaphor farther, good code must have tests. In evolution, natural selection tests the fit of organisms. This selection process is a critical piece of how organisms evolve to adapt to its environment. The Clojure REPL allows me to run experiments and test with low ceremony and cost. Sometimes I take these experiments and refactor them into a formal test. Tests are a critical part in evolving code and another objective metric for judging code quality.