Writing code is my passion. I’ve been doing it for a long time, but there is something basic that still makes me scratch my head, and that is function definition. Many young and beginner developers don’t know about function philosophy, how to use it, and how to define it properly.
I see so many young developers who can copy patterns, follow tutorials, and ship working code. But when I ask them why they defined a function a certain way, they go quiet. Sometimes they think they understand, but they don’t fully comprehend it.
That’s what this story is about. No steps. No textbook. Just my experience, my mistakes, and what I’ve learned about writing functions that actually make your code easier to live with.
Understand The Function

I don’t wanna go with the formal function definition, but I want you to truly understand what a function is. Understand it like you know the philosophy, until you can feel it. Like, “Oh, this is the function!”
Think of a function like a mini machine, a helpful mini-robot built to do one specific job. When you feed it an ingredient (the input), the machine follows a hidden set of instructions to do its work, and then hands you back a finished result (the output).
For example, if you build a makeJuice function, you can just drop an apple into it, and the machine will automatically wash, cut, and squeeze it to give you back apple juice.
Programmers use functions so they don’t have to write the exact same instructions over and over again, saving them time and keeping their code perfectly neat.
// Define
fn make_juice(fruit: &str) -> String {
// Compute
let washed = wash(fruit);
let cut = cut(washed);
let squeezed = squeeze(cut);
// Result
squeezed
}
// Usage
let result = make_juice("apple");
// result = "apple juice"
That’s the whole point of a function. You write the logic once, give it a good name, and reuse it everywhere without rewriting the same instructions over and over. It keeps your code clean, readable, and consistent.
The function does one job: make juice. It has multiple steps inside to finish the job. That is correct, that’s a solid function.
Function Rule
These are the rules you must understand before writing a function. Without these rules, believe me, you will ruin your entire codebase.
Keep Argument Low. Zero Is The Goal
The more arguments a function takes, the harder it is to use correctly. Every argument is something the caller has to remember. Every argument is one more thing that can be passed in the wrong order.
Zero arguments is the ideal. If you can write a function that takes nothing and still does useful work, that is a beautiful thing.
Here is what bad looks like:
// BAD EXAMPLE
// Too many arguments. Hard to call. Easy to mess up the order.
fn create_user(
name: &str,
email: &str,
age: u32,
country: &str,
referral_code: &str,
) -> User {
// ...
}
And here is the fix. When arguments pile up, they are telling you something. They want to become a struct.
// GOOD EXAMPLE
// Group them. Give the group a name. Pass the idea.
struct NewUserParams<'a> {
name: &'a str,
email: &'a str,
age: u32,
country: &'a str,
referral_code: &'a str,
}
fn create_user(params: NewUserParams) -> User {
// ...
}
Now the call site is readable. The compiler catches mistakes. And when you need to add a new field later, you just update the struct. The function signature stays clean.
My personal rule: zero arguments is ideal, one is clear, two is acceptable. Three or more? Just trash your function.
Focus Responsible
I hear a lot of developers repeat “single responsibility principle” like it is a magic spell. And yes, I agree with it. But I want to be clear about what it actually means, because I see people get it wrong.
Single responsibility does not mean one line. It means one job. Look at this. This is technically a function, but it is useless:
// Single line, but what is the point?
fn get_name(person: &Person) -> &str {
&person.name
}
Compare that to this, which is a real single-responsibility function:
// One job: sanitize data. Multiple steps inside. That is fine.
fn clean_data(raw_input: String) -> CleanRecord {
let trimmed = raw_input.trim();
let sanitized = remove_special_characters(trimmed);
let formatted = parse_to_json(sanitized);
formatted
}
A function must have computation inside it. That is what makes it correct and useful. Steps that are grouped together so the code stays modular.
Keep Small
If your function is scrolling past your screen, it is doing too much. A good function should be readable in one glance.
Here is a function that has grown too big. It fetches a user, validates the payment, processes it, updates the balance, and logs everything all in one place:
// This function is doing too much
async fn handle_payment(user_id: &str, amount: f64) -> Result<(), Error> {
let user = db::find_user(user_id).await?;
if user.is_none() {
return Err(Error::UserNotFound);
}
let user = user.unwrap();
if user.balance < amount {
return Err(Error::InsufficientFunds);
}
let new_balance = user.balance - amount;
db::update_balance(user_id, new_balance).await?;
db::insert_transaction(user_id, amount).await?;
log::info!("Payment of {} processed for {}", amount, user_id);
Ok(())
}
Split it. Give each piece its own name:
// Small, focused functions that compose cleanly
fn has_enough_balance(user: &User, amount: f64) -> bool {
user.balance >= amount
}
async fn deduct_balance(user_id: &str, amount: f64) -> Result<(), Error> {
let new_balance = fetch_user(user_id).await?.balance - amount;
db::update_balance(user_id, new_balance).await
}
async fn handle_payment(user_id: &str, amount: f64) -> Result<(), Error> {
let user = fetch_user(user_id).await?;
if !has_enough_balance(&user, amount) {
return Err(Error::InsufficientFunds);
}
deduct_balance(user_id, amount).await?;
db::insert_transaction(user_id, amount).await
}
When Define Function
This is the question beginners almost never ask, and it is the right one to start with. You cannot just define functions everywhere. You need a reason.
Here are the three situations where I always reach for a new function, based on my experience:
-
Repetitive code. If you find yourself writing the same block of logic in two or more places, stop. Extract it into a function with a clear name. Now you fix it once and the fix applies everywhere.
-
Complex logic. If you have a block of code that requires a comment above it just to explain what it does, that block deserves to be a function. The function name becomes the comment.
-
Documented logic. When a piece of logic represents a real business concept, like checking if a user qualifies for a discount, or validating a wallet address, it deserves its own function with its own name. Future readers will thank you.
Conclusion
After sharing my story and examples, I hope you understand what a good function looks like, what a bad one looks like, and most importantly, when you actually need to define one.
With this understanding, you can define your own functions properly. And with this story, I hope you have a better view of what function philosophy really means.
A function is not just a piece of code you define wherever you want. A function is what makes things in your code easier. Use it right.