The Polymorphic Path: C & Odin"
Polymorphism is very important term in Computer Science which means according to wekipedia , it refers to the ability of different objects to respond to the same message or method invocation in different ways. It allows objects of different types to be treated as instances of a common superclass or interface, enabling code to be written in a more general and flexible manner. It means in an object oriented manner that it refers to the ability of objects to take on different forms or behaviors depending on the context in which they are used. It allows objects of different types to be treated as instances of a common superclass or interface, enabling code to be written in a more general and flexible manner.
JAVA CODE
class Animal {
void makeSound() {
System.out.println("Animal makes a sound");
}
}
class Dog extends Animal {
@Override
void makeSound() {
System.out.println("Dog barks");
}
}
public class Main {
public static void main(String[] args) {
Animal animal = new Dog(); // polymorphism
animal.makeSound(); // Calls Dog's makeSound method
}
}
In above example we can see that animal has a reference to a Dog instance and we can see that while calling animal dog's make_sound function Dog class method has been called. How does this dynamic behavior occur and if you are coming from an non oops language then you'll have an confusion why compiler is not warning about the type mismatch of animal pointing to a Dog's reference.
Diving low-level into the realm of polymorphism
In mostly languages like C++,java and C# this idea of polymorphism is abstracted and there are keywords and patterns to implement it such as virtual methods in C++, and interfaces , inheritance in java lets uncover this secret of dynamic dispatch and try to implement interfaces or runtime polymorphism in C and then in modern system language known as Odin lets begin. We will implement above example of Animal and Dog in C.
Now lets make a prototype or a base class Animal in C.
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
typedef struct Animal{
bool is_intelligent;
void (*make_sound)(struct Animal* ani); // function pointer
}Animal;
Here we have a struct Animal which has an is_intelligent property and a function pointer which points to a function which takes Animal pointer as a parameter and return nothing. In this structure function pointer is very important this will point to different implementation of a make_sound function.
typedef struct Dog{
// defining animal struct in here to simulate inheritance or
// simply inheriting the Animal struct into Dog now dog will also
// share its property with Dog.
Animal trait;
}Dog;
Now we can see we made a Dog struct having an Animal struct embedded into it which has is_intelligent and a function pointer which will point to the implementation of the make_sound function for the dog.
void make_sound_animal(Animal* ani){
printf("Animal makes sound\n");
}
void make_sound_Dog(Animal* ani){
printf("Dog makes sound");
}
void whoose_sound(Animal* ani){
ani->make_sound(ani); // calling the function pointer
}
Defining the function's for animal and dogs that we are going dispatch it with the function pointer to achieve runtime polymorphism.
Now here it is a fun part in the main function
int main(){
Animal* animal;
Dog* d = malloc(sizeof(Dog)); // just like new Dog in java
// setting up the inherited property and embedding the make_sound
// function pointer the function that we wrote above make_sound_dog
d->trait.is_intelligent = true;
d->trait.make_sound = make_sound_dog;
animal = d; // similar to java code Animal animal = new Dog;
animal->make_sound(animal); // Dogs make_sound has been called
return 0;
}
Now animal's make_sound will invoke the make_sound of dog because dog's make_sound has already been pointing to make_sound_dog function above with the help of embedded struct of Animal instance trait so the function pointer inside the Dog's is already pointing to the correct function so Dogs instance d is now implicitly casted to animal some compiler will give warning although it is fine Animal and Dogs struct share common properties. if there are more function's that are common in the class or structs of the Animal and Dog so there would me so much of function pointer's in the struct's for that we make a Table of function pointer's known as Vtable or Virtual table.
Odin's take on runtime polymorphism
Odin is a systems programming language which is made to be a better C and it contains very good and important such as generics , package system and explicit procedure overloading and also comes with the all of the packages used for game or graphics programming such as opengl ,vulkan ,sdl bindings etc. which makes it very usable for games and graphics.
package runtime_poly
import "core:fmt"
Animal :: struct{
is_intelligent:bool,
make_sound:proc(inter:^Animal)
}
Dog :: struct{
using inter :Animal,
}
whoose_sound :: proc(inter:^Animal){
inter->make_sound()
}
main :: proc(){
animal :^Animal;
d:^Dog = new(Dog);
d.is_intelligent = true;
d.make_sound = proc(ani:^Animal){
fmt.println("Dog makes Sound");
}
animal = d;
animal->make_sound();
// equivalent to animal.make_sound(animal)
}
Here in this Odin code you can see the similar method of defining struct. Animal struct contains an is_intelligent and an function pointer pointing to a function which takes Animal pointer and return nothing. In Odin pointer is denoted by the carrot symbol (^) this means pointer to a single entity. In dog struct there is a difference in C and Odin code in Odin there is a using keyword but in C we are making an instance of an Super class or struct Animal which is named trait and then we accessing the function pointer and the is_intelligent field using trait instance which is quite verbose.
We can also do that thing in Odin we can easily define an Animal struct instance inside a Dog but that will not be very much much ergonomic So here using keyword provides a sub-typing behavior you can understand it like it expands all of the field inside of Dog's struct so that dog struct will have all the properties of Animal independently without the need of making the instance of an Animal struct inside of the Dog's isn't it cool.
Now main function follows the similar behavior as of C's main function but there is a difference in 5'th line we are assigning function pointer to relevant function in dog struct inline we don't have to define the function above like we did in c with the name make_sound_dog and make_sound_animal but in Odin the syntax allows defining function inline with the help of proc keyword for example.
x:proc(a:int) -> int;
x = proc(a:int) -> int{
return a * 3;
}
This code in Odin means that we are defining the function pointer x which has a type that it takes an integer and returns an integer and in the next line we are assigning x to its relevant function inline we didn't have to define it earlier this is a very good feature that eliminates a lot of bloat from out C code.
Now everything is similar to c code we are making a animal pointer and allocating Dog struct then assigning it to animal pointer then calling relevant make_sound function so the output will be definitely dogs make_sound gets called because of dispatching we have done in line 5 of main function in Odin's code. which is very ergonomic and bloat-less if we compare it to C code.
Conclusion
In this Article we have demystified the working and internal working of runtime polymorphism and implemented the basic version of polymorphism in C as well as Odin for having a good gist in runtime polymorphism
Subscribe to my newsletter
Read articles from Aditya Nath goswami directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Aditya Nath goswami
Aditya Nath goswami
I am a passionate about mathematics and computer science and I like to share my random and technical and computer science thoughts and knowledge with people