Jay's blog

Am I A "Real Developer" Yet?

I've fallen into the "real programmer" trap. Despite being gainfully employed as a software developer for all of the past 13 years, I'm finally feeling that impostor syndrome everyone keeps talking about.

I started learning the Rust programming language maybe six years ago or so. I had mostly overcome that steep learning curve. I was starting to actually write non-trivial programs. I chose to use Rust to do my coding interview for a job I landed in early 2020. I was getting comfortable with it.

Things changed, though. I went to a meetup of Rust users and found them to be just intolerable as a community. A new major version of Rust landed and my old code needed a some changes to run again, and a lot more changes to be considered idiomatic. The new async features landed. It felt like I had to learn a new language all over again.

Maybe this would be acceptable if Rust was my daily language. Coding in Rust was a hobby. Depending on what is happening in my life, I may go weeks or even months between opportunities to return to my hobby projects. It wasn't a tenable choice if I wanted to get things done.

I resolved to learn C. Changes in C are infrequent. The language changes at a glacial pace. But it retains some of the features I found desirable in Rust. It runs very close to the metal compared to other languages, so it tends to be fast. Deployment of artifacts is as simple as sharing a single binary, assuming your dependencies are statically linked.

The truth is, I'm struggling. And it's not the language itself. C as a language is pretty reasonable. Sure, there are some rough edges. Pointers aren't hard, but you can get tangled up in them pretty fast. Implicit integer conversion can be a real headache. Overall, though, it's pretty straightforward.

What hamstrings C is not the language itself but rather its tooling, or lack thereof. I'm used to relying on a variety of tools when I develop software: formatters, linters, type checkers, unit test runners, package managers, task runners, etc. C's tooling story is spartan, and it really hinders my progress with the language. My experience developing in C is death by a thousand cuts.

Let's start with compilation. Compiling a single C source file into an executable that only relies on the standard library can be done with a single command. As a program grows, I'm naturally going to want to separate my code into units in the form of files. There are tons of tools out there to help with this, but the most ubiquitous is Make.

I struggle with Make. I've read multiple books, whole-ass books, about Make and I still can't fully understand it. At its most basic, it's a language-agnostic tool for taking files as input and doing some process to them which then creates new files. Make is brilliant at this use case. It tracks timestamps on files to do the least amount of work necessary when you run your Makefile. I think my main problem with Make is due to how agnostic it is. I'm used to build systems where I only need to point the tool at my entry point and it can figure out the rest. Make can't do that because it doesn't know anything about C. That puts the responsibility on me to code my dependency graph into my Makefile. That's a lot of work! I'm not used to that amount of work to set up a build process. It's very frustrating when I decide to factor certain bits of code out into their own units because I have to update my Makefile as well. I haven't found any way to make this trivial yet. Ideally, I want my build tool to understand C source code enough that I can point it at my entry points and it can build the dependency graph for me.

C predates the concept of unit tests. That's kind of an insane statement. It explains why there are no default unit test facilities that ship with C's standard library. I'd expect unit tests to be a solved problem for any decently popular language by now. I can't seem to find any unit test libraries or frameworks for C that don't have a huge red flag. There's plenty of "single header file" projects that have a single developer who has thrown them onto Github ten years ago and then abandoned. There are ones that require so much boilerplate. There are ones that have more dependencies than the program I'm writing. And there's Unity, which requires Ruby to run.

I tried writing my own simple unit test setup. How hard can it be if legions of programmers have uploaded theirs on Github and abandoned them? The biggest source of boilerplate in writing unit tests in C seems to be having to add each unit test function to your runner. I made the function definition part of creating a test into a macro that adds the function to a list automatically. In order to do this, I had to go off spec and use GCC-specific code, which I find annoying. I guess it works. 🤷

One of the reasons garbage-collected languages are the norm nowadays is because programmers supposedly can't manage memory themselves. Languages like Rust and Zig use ownership models to manage memory more deterministically. How hard can it be, though? For every malloc, you must also free. Or so I thought. This is one of those situations where I'd expect static analysis tools to shine. I'd love it if a linter could yell at me in my editor. I haven't found such a tool. I have tried using Valgrind, but I don't understand what it's telling me. It says I'm leaking memory, but I don't understand how. Each malloc has a corresponding free, and yet.

The biggest things I miss from every language I've ever touched are dynamic arrays and associative arrays. Despite the fact that every non-trivial program ever written uses them, the C standard library doesn't have either of these. Because I want to be a "real programmer" I tried to implement them myself. 🙄 I guess I'm just not hardcore enough. I didn't get a traditional Computer Science education, so I missed out on the joys of accomplishing these feats with the added pressure of a university environment. I understand both of these data structures in principle. Doing it in practice is a different story. Especially trying to do it in a generic way that would work with any kind of data type.

I could grab Glib or something that has these structures built-in, but then I lose portability and I have to accept the opinionated way in which the authors have chosen to implement them. I hate opinionated libraries.

What's the problem? I'm not a "real programmer", I guess. Despite supporting my family for over a decade on a very generous income afforded to me by my profession, despite earning the respect of my coworkers, despite being a tutor and mentor to others, I'm not a "real programmer". I guess I'm just too soft. I grew up with garbage collectors and Visual Studio. I never had it rough. I was born just barely too late for the microcomputers that came with thick manuals and booted into BASIC interpreters. I'm obsessed with dumb concepts like "correctness" and "best practices". I am not a "real programmer". But I guess I want to be...?

Linus Torvalds, an impressive programmer, but not exactly an ideal coworker, said "If it compiles, it is good; if it boots up, it is perfect." I wish I could be that cavalier.

#c #impostor syndrome