This project will be both a style guide for how I like to code C and a linter program to check if a source file conforms to it. Declaring a .C file as containing --C code requires adding the comment //--C as the first line. Without that it won't be parsed by the linter.
Borne partially from the fact that I don't do enough programming and a problem when using C++ was that I'd learn about some new feature but when returning to it weeks later I could never remember how it worked so I had to constantly relearn stuff. So after using C++ I wondered if going in the opposite direction and making a subset of C instead of a superset like C++ would work better. The idea being to limit the feature set to such an extent that there would only be one way of writing the program but not have that way just be straight imperative. So as to minimize the programming part of programming.
Another inspiration are writing techniques like polysyndeton or Louis-Ferdinand Celine's excessive use of ellipsis. Where you have a limited way of structuring the text that uses very few building blocks but still allows great expressiveness. Though admittedly they can both seem repetitive at first. Setting constrictions on how work can be done I think generally improves the final result.
--C then is a subset of C99 with some of what I consider best practices enforced and language features I don't like removed or eschewed.
Sometimes I would think about creating a new programming language but it's a lot of work not just in implementation but more so if you want it to be performant and have access to external libraries in other languages. As what I wanted wasn't new features but less of them and a particular way of working a simple style guide will suffice.
Refactoring is the overriding concern for me when programming as having to sit down and plan how to structure code in advance is boring and things never work out as planned anyway. Instead I prefer to just start writing and continuously refactor.
Functional programming makes refactoring a lot easier as it breaks the code down into discrete chunks that can be manipulated easily. Data oriented programming is something I think fits well with a functional approach and these two combined was something I wanted to focus on. Naturally there are limits to how functional you can get with C.
It's not a huge departure from C, just a hand full of rules as there isn't all that much that can be removed from the language. Nothing is set in stone and I'm continually changing stuff as I write another project using these rules. Some parts I'm pretty happy with like having most things const or avoiding loops but others are more experimental and some might be considered stupid.
It might not work at all and I don't know if you could complete a whole project with it. If you find yourself asking "How can you do X using this?" then the answer is that you probably can't. I made it specifically for my own needs which is hobbyist game programming and it's not intended to be suitable for multi million line code bases.
The name is obviously a jab at C++ and the joke that the name C++ itself is a bug since using post-increment only increments the value after you've used it.
It needed a short phonetic name as you can't have "--" in directory names and 'minusminusc' is a bit long so in the tradition of mispronouncing C++ as "sepples" I thought --C could be pronounced "mince" as in mince pie.
The linter only does some very crude analysis on the text to try and guess if one of the rules are being violated. Meaning it won't be able to handle all the monstrous ways C can be obfuscated so you have to be nice to it or there will probably be inappropriate errors. Reported errors are rudimentary with only a line number and error type given. No linking is done either so there are some things that aren't known to it.
Spending a lot of time writing the linter isn't something I want to do since it'll likely only ever be used by myself but at the same time an actual program that checks for conformity makes the project feel a lot more real and tangible than just a style guide listing the rules.
Here are the different rules. A V means it's implemented in the linter and an X means it isn't yet:
V All variables have to be either const or static or both. This to force most variables to be constant necessitating a more functional approach. It also forces global variables to be declared as static which consequently requires the use of getters and setters.
Ternary operations have to be used if a variable's value should depend on some evaluation.
Struct members are also affected by this and have to be const making static structs useless.
V All function arguments must be const. Corollary of the previous rule it has the additional intention of not allowing values to be returned through pointers as all pointers also have to be const. This avoids mixing in data and return values as having some variables being both input and output can be confusing. Multiple return values have to be handled using structs.
X Pointers returned from functions must be const. So as to not allow globals to be manipulated outside setters.
V Struct members may only be accessed when calling a function. This one is a bit weird and I'm really not sure it's a good idea but here's the rationale: I don't want variables to be passed into a function as a struct blob even though they might have to be returned as such because there are no multiple return values in C. Instead all function input should be clearly enumerated as arguments.
Easy way of doing this would be to simple not allow structs as function arguments however that runs into two problems: First you might have external library functions that requires structs as inputs. For example external drawing functions might take a texture struct as input and if you can't pass texture structs into your function it won't be able to draw anything. Secondly I still wanted to allow linked lists which requires structs and those would be pretty pointless if you couldn't pass them along into other functions.
What I wanted to avoid however was the programmer taking a struct containing multiple returned values and passing it directly into another function, as it's then less obvious what the function does. The best way I can come up with is that any member variables you want to use inside a function has to be passed separately. It'll be a little more convoluted to use linked lists since the function would have to act something like this:
foo( const int a, const double b, const struct linkedList ll ) { ... foo( ll.a, ll.b, ll.next ); }
V No extern keyword. To again encourage the use of getters and setters.
V No typedefs. They're weak in C so I consider them pointless and though you can use them to not have to type "struct" so much I prefer to have structs explicitly labeled.
V All variables have to be initialized. Generally agreed to be a good idea I think.
V No global struct definitions. This to discourage OOP. I thought about banning structs all together but again that would break compatibility with most all external libraries and make linked lists impossible and additionally having multiple return values for functions would require non-const pointers if not allowing for structs.
Structs then are only intended for handling multiple return values and the occasional linked list but not for in any way representing objects.
Though I don't know how to store things like textures and stuff that are structs that come from external libraries.
V Structs may only contain primitive data types. So no structs or function pointers as members in order to again avoid OOPish compositioning and emulated methods.
Exception to this rule will be that they may contain struct members which is of the same type as themselves in order to allow for the creation of linked lists.
X Getters are only allowed to be called as an argument to a function call. This to discourage the use of getters as global variables and make the programmer use them more sparingly, making you pass variables explicitly. Instead of writing something like:
int foo() { const double a = 5 / some_getter(); const double b = 12 / some_getter(); }
You'd have to write something like this:
int foo( const double someValue ) { const double a = 5 / someValue; const double b = 12 / someValue; } void bar() { foo( some_getter() ); }
What then counts as a getter? This is a little tricky but probably it'll be considered a function that takes one or no function arguments. As then it can't really do any meaningful calculations and ergo must be a getter? I'm not sure if this will work but probably the program will check if you try to initialize a variable using such a function.
V No if statements. Switch statements are neater than if-else ladders so I wanted to try and see what happened if I was forced to always use them. You could argue that it would be better to just remove if-else statements and leave single if statements. Because sometimes it's convenient to just have an if statement to check if something is true or false and e.g. return. Having only switch statements makes those occasions unnecessarily verbose and you'd probably be right but this has turned into an experiment in simplification so out they go.
V No loops. So no for, while or do-while. Iteration has to be handled by recursive functions. This almost becomes a requirement by forcing const on variables anyway. Recursion is something I find to be more aesthetically pleasing and it makes you think a bit harder of how iterative code functions than if you can just slap down a for loop anywhere in the code.
Essentially this requires the use of a compiler capable of optimizing away tail calls. With gcc you have to use -O2 otherwise something like having a recursive game loop would overflow the stack. br>
One motivation for this is that with loops you usually don't have only one thing being incremented each iteration, say the "i" variable of a for loop. Instead many things tend to be mutated in one iteration and by having the loop as a separate function those variables have to be clearly inputted as function arguments. This is a stupid example but instead of:
int foo() { int a = 57; int b = 12; for( int i = 0; i < 10; i++ ) { a++; b++; } return a + b; }
You'd have to split it into two functions. One setting up the environment and one doing the iterating:
int foo() { return bar( 57, 12, 10 ); } int bar( const int a, const int b, const int nbrIterations ) { switch( nbrIterations == 0 ) { case true: return a + b; } return bar( a + 1, b + 1, nbrIterations - 1 ); }
X Functions with side effects must be of void type. Another one I'm not certain is a good idea but I wanted some way of differentiating functions that does things like change the program state from those only doing calculations. For example in the ray caster engine there's a function that calculates a couple of positions on screen and then draws a slice of a wall all in the same function. It then returns the calculated screen positions so another function can draw the floor. I thought it would be cleaner to have that function split into three, one that calculates the relevant screen positions and two others that have the side effects of drawing wall and floor to the screen.
You could then not mix functions with side effects and those without in the chain of function calls e.g: a void function calls an int function calls a void function wouldn't work as the intermediate int function couldn't call a void function without itself being void. Instead all void functions with side effects would need to be at the top of the function calls and functions doing calculations below them and their return values passed up the chain to the state functions. Not sure if this would work in practice.
The main function would have to be an exception as it has to return an int and still needs to be able to call void functions.
You could say that some functions with side effects should be able to return a value to indicate whether their operation completed successfully or not and that's a good point. If a function sets a value somewhere it could return true or false for if it succeeded or not. Maybe allow them to return bool?
This was partly inspired by reading about monads in Haskell though I admit I never managed to understand the concept completely.
The way this rule would be implemented would probably be to check if a function was called without storing it's return value, the calling function then would have to be void:
void foo( const int bar ) { setter( bar ); }