I have been learning C and it has come to my notice that there are a lot of good things about C that modern languages seemed to have ditched and they are just better in C. Let's go over them one by one.
Errors as values
In languages like JavaScript and Python we have try-catch
or try-except
block that is used to handle errors which seems smart to do until you realise how badly it scales. Creating a block doesn't make sense for every function that can error and not creating multiple blocks would lead you to have bad error handling.
Seeing this problem, languages like Go & Zig have introduced a magical thing called errors as values and I want you to sense my sarcasm as I am saying this.
Let's see how C handles errors straight up by using the linux man pages.
Let's look at another example but this time something more epic.
Why is this more epic? Because there are functions like this and you never know about them because your language hides everything and all you know is that the function failed.
Every error in C is a value that is returned from the function and it is invalid value that can't be used. By doing this, we don't need any fancy underlying system. This makes it simple for even the compiler. Do you know why?
Imagine if you have to return a fancy error that screams that I am an error, you'll probably have to do something like this (using zig as an example):
const IntOutput = struct {
type: enum {
Error,
Ok,
},
value: union {
success: i32,
failure: i32,
},
};
Now we have predefined errors in Zig and you'll probably not have to write errors like this because there is better syntax for this in Zig and the code above is just to show how the language works internally. When your function throws an error, this is how the program knows that it has throwed an error, using a union
.
This is clearly a lot more work compared to just returning a value and knowing what the error is.
Pointers and References
For this one, let me test you first. You have to tell in the snippet below whether this function body is using the variable coordinate
as a reference or value.
struct Coordinates {
int x;
int y;
};
// function body start
{
// coordinate is of type Coordinates
coordinate.x += 1;
some_other_function(coordinate);
// rest of the body
}
If I don't show you the function's arguments, you can never know, whether this function is modifying the original coordinate
or a copy of it. By seeing the first line you may assume that we must be using reference since we are updating the coordinate
, but as you see the second line, now you may think that maybe this update is just for this second function. You can never know without seeing the function argument.
Let's C the same code in C.
typedef struct {
int x;
int y;
} Coordinates;
// function body start
{
// coordinate is of type Coordinates
coordinate -> x += 1; // if it a reference
coordinate.x = 1; if it a value
some_other_function(coordinate); // if coordinate is a reference and both functions use reference, or if it's a value and both functions use value
some_other_function(*coordinate); // if coordinate is a reference and this function wants value
some_other_function(&coordinate); // if coordinate is a value and this function wants a reference
}
This is so much more clear. In every line of use, I know if the variable is a value or a reference. C is so ridiculously clear about each and every operation. Now, Rust does kind of make it better because when you are passing the function argument you have to pass it using &
if you are passing a reference, but in the function body itself, it's the same as C++.
Dynamic languages are even worse which just always pass a reference and never actually allow you to pass value which is annoying when you have to do it.
Generics
Now we enter a serious domain. C supports generics using void *
which basically means that it doesn't support generics and it just telling you to handle everything by yourself. Experts will argue that C23 has _Generic
, which as far as I have seen is overloading and not generics. I could do what that does with enum
and union
. I haven't used it in any code yet and I don't know when I will.
But C not having generics like in some languages like TypeScript is actually very awesome. Generics can often make you lazy because you write a small piece of code that has no type information and hence can not be optimized at compile time and if you really think very hard about it, maybe, you didn't need it.
Supporting any type makes the support for any one type, weak. You don't realize the cost of this generic function running because it was so easy to type.
C made me realize, my software doesn't need to solve 100 problems, it just needs to solve 1 million cases of 1 problem.
I don't need to write a generic that can support any type, when in my code the generic function will only ever interact with at max 3 types. I can write an overloaded code for those three types (which is still kind of a problem but I have a love hate relationship with overloading so I'll let it go).
Async / await is the bane of software
I first want you to notice that this is the only title with a personal comment, this is how serious I am about this one. Let's see why.
Tell me, is this TypeScript code, sync or async:
const result = await fetch("i_hate_promises.ai")
console.log(result);
Some people are going to say, this is asynchronous code as we have used await
. But the whole point of asynchronous is to do it later, but I did it instantly, until the fetch
doesn't return, the thread is blocked (i.e. the program is stopped) which clearly means this is a synchronous piece of code.
This is the issue number 1, even though the code is synchronous, it looks asynchronous because of the await
keyword. This is so confusing. This is how most people write code and they think they are using asynchronous programming, no, you are still working in synchronous domain, because the thread is blocked when you use await
.
Issue number 2, which is even greater. await
doesn't stop the runtime from creating a promise
. fetch()
is not going to be executed in async clearly, it is returning and the output is used, until then, the thread is blocked, so why are you creating a promise
object, just skip that and return the actual value. Instead, fetch()
returns a promise
, await
then extracts the output from this promise
and destroys this promise
. You created an object and destroyed in the next millisecond. Why was that object created when it was never required?
In C, you have none of this because to do async, you have to implement an async specific to your issue. You will not create any redundant objects, by default, everything is going to block and that's the behavior you actually want most of the time.
I am not saying use C for everything, but clearly the modern languages have some wrong ideas about how things need to be done and it's half because the creators made it too easy for you and half because no one questions this.
Understand this, try C, follow me.
Note: This is a raw article, I am not going to reread or edit it, so if you find any faults in it, just create a PR.
Top comments (0)