n-channel

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs

commit 3ee1cca27858a1a6267b06643bef26b5b567ae86
parent 4f8c91dd5798229d604c16f6f38123d9848430ce
Author: Samdal <samdal@protonmail.com>
Date:   Sat, 22 Feb 2025 15:20:44 +0100

add implications of OOP

Diffstat:
M_posts/2025-02-21-Software-Rants.md | 4++--
A_posts/2025-02-22-implications-of-OOP.md | 303+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
M_posts/2025-02-22-making-generic-data-structures-in-C.md | 6+++---
M_sass/_main.scss | 14++++++++++++--
4 files changed, 320 insertions(+), 7 deletions(-)

diff --git a/_posts/2025-02-21-Software-Rants.md b/_posts/2025-02-21-Software-Rants.md @@ -1,12 +1,12 @@ --- layout: post -title: Software rant compilation +title: Software rants you should watch description: A Compilation of rants about software, programming and OOP comments: true tags: [misc, programming] --- -This is where I have my compilation of good programming rants worth watching. +A compilation of good programming rants worth watching. ## Short Videos diff --git a/_posts/2025-02-22-implications-of-OOP.md b/_posts/2025-02-22-implications-of-OOP.md @@ -0,0 +1,303 @@ +--- +layout: post +title: "Implications of inheritence: OOP is Bad" +description: The one feature that is properly unique to OOP is terrible, this is why. +comments: true +tags: [writing, programming, C] +--- + +Although the main premise of Object Orianted Programming may be simple, it forces certain design decisions which you otherwise wouldn't do. +Structuring your program with an OOP mindset already restricts your thinking, but these extra implications make thinking outside the ~~object~~ box a lot harder. + +Modern programming languages which "support" OOP **forces** you to use these restrictions all throughout your program. + +## What is OOP? + +OOP, in the modern sense, is a software strategy where *functions* and *data* are tied together. + +I don't have a problem with that, but if that was all that OOP did, then it would basically be an alternative syntax for doing this: +``` +// not OOP (?) +struct my_struct { + int a, b, c; +} + +int calculate_sum(my_struct& self) { + return self.a + self.b + self.c; +} + +//~ usage /////////////////// + +my_struct object = {1, 2, 3}; +int res = calculate_sum(object); +``` +``` +// OOP +class my_class { + int a, b, c; +} + +int my_class::calculate_sum(/* implicit self */) { + return self.a + self.b + self.c; +} + +//~ usage /////////////////// + +my_class object = {1, 2, 3}; +int res = object.calculate_sum(); +``` + +So this isn't the whole story, something being "OOP" imples other things, mainly **inheritence**. + +### What is inheritence? + +Inheritence is the ability for a class to extend one classes. +This allows you to have some "generic base class", and then add new methods and members on top of it. + +It looks something like this +``` +// OOP +class my_base_class { + int a, b; +} +class my_class : my_base_class { + // int a, b; are automatically included from the base class + int c; +} +``` +But you can still do this without OOP. +``` +// not OOP (?) +struct my_struct { + int a, b; +} +struct my_struct { + my_base_struct base; + int c; +} +``` +The only difference here is that you have to explicitly type `object.base.a + object.base.b`, but you could add a language feature to make it identical. + +The "non-OOP" way is so far superior: +- Control over memory layout +- More explicit without being cumbersome +- Allows composability (although some languages allow for "multiple-inheritence") + +## So what's the catch + +OOP inheritence allows you to *override* functions. +``` +// OOP +class my_base_class { + virtual string to_string() { + return "base_class"; + } +} +class my_class : my_base_class { + string to_string() { // overrided function + return "my_class" + } +} +``` +The "magic" part about OOP is that when you *override* a method with an inherited class, when you cast the object to parent class, it retains the overrided functionality. + +``` +void print_string(my_base_class obj) { + print(obj.to_string()); +} + +my_class object = new my_class(); +// object will implicitly cast to 'my_base_class' +print_string(object); // prints "my_class" +``` + +Did you notice something?\ +In order for this functionality to work, we have to create `object` with a constructor, which is called through `new my_class();`. + +So RAII, *Resource acquisition is initialization*, that's the catch that makes OOP unique. + +## Other ways which don't use RAII + +### 1. Function pointers +This will essensially manually do what the RAII and virtual methods does "automatically" for us. +``` +typedef string to_string_function_pointer_t(void* self); + +////////////////////////////////////////////////// +// replace virtual function with function pointer defined above + +struct my_base_struct { + to_string_function_pointer_t* to_string; +} +struct my_struct { + my_base_struct base; +} + +////////////////////////////////////////////////// +// make the to_string functions + +string to_string_base_struct(void* self_void) { + my_base_struct* self = self_void; + return "base_class"; +} + +string to_string_my_struct(void* self_void) { + my_struct* self = self_void; + return "my_class"; +} + +////////////////////////////////////////////////// +// Make "constructors" + +my_base_struct* make_my_base_struct() { + my_base_struct* res = malloc(sizeof(*res)); + *res = (my_base_struct){ .to_string = to_string_base_struct}; + return res; +} + +my_struct* make_my_struct() { + my_struct* res = malloc(sizeof(*res)); + *res = (my_base_struct){ .to_string = to_string_my_struct}; + return res; +} +``` +Usage then remains the same +``` +void print_string(my_base_struct base) { + print(base.to_string()); +} + +my_struct object = make_my_struct(); +print_string(object.base); // prints "my_class" +``` +So this goes to show how much work the OOP language is doing for us. +C shows us how complicated of a solution this is. +### 2. Enums +``` +enum struct_type { + TYPE_MY_BASE_STRUCT, + TYPE_MY_STRUCT, +}; + +struct my_base_struct { + struct_type type; +} + +string to_string(my_base_struct* obj) { + switch (obj.type) { + case TYPE_MY_BASE_STRUCT: return "my_base_struct"; + case TYPE_MY_STRUCT: return "my_struct"; + default: return "invalid type"; + } +} + +``` +``` +void print_string(my_base_struct base) { + print(to_string(base)); +} + +my_base_struct object = {.type = TYPE_MY_STRUCT}; +print_string(object.base); // prints "my_class" +``` + +## Why I think the non-RAII ways are better + +### 1. Function pointers +As you saw, the function pointer strategy was pretty painful. However it's very explicit, in the cases where we geniuenly need a fully obfuscated API, this is perfcet. + +Because it's just functions, you can easily re-use them. The `to_string_my_struct()` function could easily compose with similar functions +``` +string to_string_my_struct(my_struct* obj) { + return to_string_my_base_struct(obj.base) + int_to_string(obj.c) +} +``` +which many OOP languages will not let you do.\ +Many child-structs can re-use and compose multiple different function pointers as they want. Something which is impossible with typical OOP syntax. + +I personally only find function pointers useful with dynamically loaded code. Something an OOP mindset fails to make obvious. + +Through ease composability and lack of required language features—I would argue that for dynamic cases, even without having gotten to the thick part of why RAII is bad, function pointers are superior. + +### 2. Enums +The enum example was roughly the same amount of code as the OOP example, maybe slightly more. +Because the enum solution doesn't use any language features, this should drive home that the enum solution is a lot simpler. + +If you looked closely, you might also have noticed that I never created a `my_struct` in the enum example. This is because we simply don't need multiple types anymore. + +The enums let us compose all our behavior in a single type! +``` +struct entity { + entity_type type; + struct { + int hp; + hit_box hitbox; + vec2 velocity; + vec2 position; + } common; + + struct { + entity_ref current_target; + } common_enemy; +} +``` +If you have never done this before, it is **increadibly** useful. Now everything is one function, the different data members simply toggle the branches which are related. + +This also allows us to exploit all the benefits of the *non-OOP* strategies above, namely composing as opposed to "multiple-inheritence". + +You have no idea how much more obvious the code becomes with this. +All the struct members are clearly laid out. All the branches and composed behavior get placed next to each-other. + +Additionally, if you're concerned about memory usage of having one large struct, you can use a "tagged union". + +### 3. You can combine the two above +``` +struct entity { + entity_type type; + struct { + int hp; + hit_box hitbox; + vec2 velocity; + vec2 position; + } common; + + struct { + entity_ref current_target; + } common_enemy; + + void* extra_entity_data; + entity_function_poitner_t* update; + entity_function_poitner_t* on_spawn; + entity_function_poitner_t* on_death; +} +``` + +If you wish to dynamically register entities in an "enum based" type, you can combine the function pointer without removing anything. + +## Finally: Problems with RAII + +OOP and RAII in general suffers from a problematic thinking pattern where each and every object needs to be constructed, destroyed and handled individually. +This is almost *never* the case in the real world. +Often when I talk to people who are tainted by the stains of OOP, they end up with a bunch of boiler plate and scattered types and functions. When in reality it could be much simpler. + +Not yet having learned how to chunk lifetimes together is the largest reasons why manual memory management seems so daunting to people. +I almost never think about memory management, and when I do, it's a core part of the logic behind my program. + +<iframe style="width: 80%; aspect-ratio:16/9; height: auto; margin: 10px auto;" +src="https://www.youtube.com/embed/xt1KNDmOYqA" title="HMH N+2"> +</iframe> +Want more videos like the one above? [My list of software rants worth watching]({% post_url 2025-02-21-Software-Rants %}). + +Just because OOP languages want to support one single feature, namely poor automation of function pointers, they force your entire program to be written in a suboptimal way. +Some languages like C++ allow a hybrid approach, due to it's history as an extesnion of C, but this has some problems of it's own. +Most OOP languages are not like this. + +When I use a language with native OOP features, I find myself searching "How to do struct literal in X". Only to realize I have to write a constructor, despite not even using virtual functions. + +### What about encapsulation +You might have noticed that I didn't mention encapsulation. +That's because encapsulation is stupid, and it should never be used. + +If you truly need encapsulation, you should use an opaque type. + +If you need to make sure only some individual data member is used correctly, then prefix the member name with `_` or `_INTERNAL_`. diff --git a/_posts/2025-02-22-making-generic-data-structures-in-C.md b/_posts/2025-02-22-making-generic-data-structures-in-C.md @@ -54,7 +54,7 @@ Instead of using `void*`, just create macros that assume member names. #define llist_stack_pop(f) ((f)==0) ? 0 : ((f)=(f->next)) ``` -This requires you to define the structures yourself. +This requires defining the structures yourself. ``` typedef struct llist_int llist_int; struct llist_int { @@ -74,7 +74,7 @@ llist_stack_push(list_first, n); It's a lot simpler, it's type safe and it gives the user control of memory management. -Meanwhile, with the `void*` method you have to do `*(int*)list_first->data` just to read the memory. +Meanwhile, with the `void*` method, you have to do `*(int*)list_first->data` just to read the memory. Having more control over datatypes also makes it easier to compose larger structures. ``` @@ -182,7 +182,7 @@ Usage code: ``` htable_def(int) int_htable; int_htable id_table = { - .table_sz = 100, + .table_sz = 1001, .table = malloc(1001 * sizeof(*id_table.table)), }; diff --git a/_sass/_main.scss b/_sass/_main.scss @@ -193,13 +193,23 @@ strong, b { margin-bottom: 0.5rem; } -.posts ul,header ul { - list-style:none; +ul { + padding-left: 1.5rem; + margin-top: 0.5rem; + margin-bottom: 0.5rem; } li { list-style-position: inside; } + + +/*********** posts ************/ + +.posts ul,header ul { + list-style:none; + padding-left: 1rem; +} .posts li { align-items:center; display:flex;