Explain me interfaces and methods in Odin, please

I didn’t find these topics and I asked to Gemini about them. Gemini showed the system of interfaces in Odin, a mixed form of Rust/Go interfaces. But sadly, Gemini’s explanation looks like a lie.

I noticed this ausence and then I heard about data-driven system in Odin. I want to know more, and how to scale it. Methods added to objects are useful to mantain a clear code. Odin says that wants simplicity. I suppose that you separate in files or some form of maintaining each struct with their functions.

In other words, explain me this data-driven philosophy in pragmatic form.

Odin does not have OOP paradigms. It’s all structs and funct… procedures. You could somewhat reconstruct a method as a pointer to a function:

MyVector :: struct {
   x, y: f32
   add: proc(this: ^MyVector, other: MyVector
}

myvector_add_1 :: proc(this: ^MyVector, other: MyVector) {
    this.x += other.x
    this.y += other.y
}

myvector_add_2 :: proc(this: ^MyVector, other: MyVector) {
    this.x += other.y
    this.y += other.x
}

and then when initializing your struct, you can choose any funct… procedure you like:

mv1 := MyVector{13, 37, myvector_add_1}
mv2 := MyVector{42, 69, myvecotr_add_2}

mv1->add(mv2)  // calls myvector_add_1
mv2->add(mv1)  // calls myvector_add_2

So in some sense, you define an “interface” for every “method” separately, which in my experience often is the thing you’d actually want in OOP, but OOP doesn’t give you.

However, this couples your data with behaviour, which leads to the OOP thinking of individual instances of your data. The Data-Oriented way of doing this to always think of arrays of data. E.g. in a game you usually don’t just have a single Entity, but hundreds, maybe even thousands of them. So instead of call a function on every Entity individually (where each method call has a cost), you just have a funct… procedure that iterates over all the Entity and applies the behaviour on it.

You could then apply different behaviour by just having entities in different lists, maybe a roaming: []Entity for NPCs walking around in the world, maybe a fighting: []Entity for all NPCs and Monsters attacking someone, maybe even a dead: []Entity for all Monsters waiting to respawn.

One option that decoupling your data from your behaviour gives you is to split your Entity data in different Components, e.g.:

Transform :: struct {
    position: Vector2,
    scale: Vector2,
    rotation: Vector2
}

Movement :: struct {
    velocity: Vector2,
    acceleration: Vector2
}

Stats :: struct {
    max_health: f32,
    health: f32,
    max_mana: f32,
    mana: f32,

    strength: f32,
    dexterity: f32,
    intelligence: f32,
}

You can then have a []Transform, a []Movement, and a []Stats, create an EntityID :: distinct int for each Entity. Some Entities may only have a Transform Component, other Entites (like the Player) may have all 3. By mixing the components you create Archetypes, where each can be collected in a simple archetype: []EntityID, so you can apply behaviour to your Entites based on their Archetype.

Using this approach, you will end up with a bunch of encapsulated Systems that all work on a subset of the same set of Components, and each Component is associated with an Entity.

For a deeper understanding you can read about the “Entity Component System” (ECS) Architecture. There are even a couple ECS implementations for Odin on github.

4 Likes

I like to think of it as “model the problem based how you need to access the data” and not “organize your data based how you model the problem”.


E.g. you’re asked that for a given family tree figure out for 2 given person if they are cousins.

If you model this as an actual tree, where a person has a list of siblings, pointers to children and pointers to its parents this looks like a family tree but makes queries rather over complicated but printing it as a tree is obvious.

Person :: struct {
    parents: [2]^Person,
    siblings: []^Person,
    children: []^Person,
}

If you go backwards and say “I need queries X,Y,Z to be simple” then a family tree could be turned into just an array. Printing a tree is now complicated but queries are simple.

Person :: struct {
    gen: int, // if X is child of Y then X.gen = Y.gen + 1
    idx: int, // index in array
    siblings_start: int, // range of siblings start..<end
    siblings_end: int,
    parents: [2]int,
}

Family_Tree :: []Person

is_sibling :: proc(p0, p1: Person) -> bool {
    return p0.gen == p1.gen && \
        p0.siblings_start <= p1.idx && p1.idx < p0.siblings_end
}

is_cousin(p0, p1: Person) -> bool {
    return p0.gen == p1.gen && !is_sibling(p0, p1)
}

This is a rather stupid example but hope it shows what I mean.

4 Likes

“interfaces and methods in Odin”
TL;DR: Doesn’t have them, doesn’t need them. Odin is not an OOPS language.

I’m not being facetious or cruel. There are data structures, and there are procedures that act upon those data structures. As Niklaus Wirth famously wrote, “Algorithms + Data Structures = Programs”. It’s not any more complicated than that. The sheer simplicity can be disorienting to those trained in the overly complex thinking required by many OOPS languages.

Breathe in. Let go of the needless complexity. Breathe out. Embrace the inherent simplicity. Figure out what your program needs to do. Figure out the minimal data structures you need to support those actions. Write the code to manipulate and transform the data.

Read the documentation. Read the example code.
Iterate. Test. Explore. Learn. Grow. Understand.

3 Likes

Here is how an allocator interface is implemented in Odin:

Allocator :: struct {
	procedure: Allocator_Proc,
	data: rawptr,
}

Allocator_Proc :: #type proc(
	allocator_data: rawptr,
	mode: Allocator_Mode,
	size: int,
	alignment: int,
	old_memory: rawptr,
	old_size: int,
	location: Source_Code_Location = #caller_location,
) -> ([]byte, Allocator_Error)

If you want to create an allocator, you would create an implementation of Allocator_Proc. Here is how Odin implements an Arena:

Arena :: struct {
	data:       []byte,
	offset:     int,
	peak_used:  int,
	temp_count: int,
}

@(require_results)
arena_allocator :: proc(arena: ^Arena) -> Allocator {
	return Allocator{
		procedure = arena_allocator_proc,
		data = arena,
	}
}

arena_allocator_proc :: proc(
	allocator_data: rawptr,
	mode:           Allocator_Mode,
	size:           int,
	alignment:      int,
	old_memory:     rawptr,
	old_size:       int,
	loc := #caller_location,
) -> ([]byte, Allocator_Error)  {
	arena := cast(^Arena)allocator_data
	switch mode {
	case .Alloc:
		return arena_alloc_bytes(arena, size, alignment, loc)
	case .Alloc_Non_Zeroed:
		return arena_alloc_bytes_non_zeroed(arena, size, alignment, loc)
	case .Free:
		return nil, .Mode_Not_Implemented
	case .Free_All:
		arena_free_all(arena)
	case .Resize:
		return default_resize_bytes_align(byte_slice(old_memory, old_size), size, alignment, arena_allocator(arena), loc)
	case .Resize_Non_Zeroed:
		return default_resize_bytes_align_non_zeroed(byte_slice(old_memory, old_size), size, alignment, arena_allocator(arena), loc)
	case .Query_Features:
		set := (^Allocator_Mode_Set)(old_memory)
		if set != nil {
			set^ = {.Alloc, .Alloc_Non_Zeroed, .Free_All, .Resize, .Resize_Non_Zeroed, .Query_Features}
		}
		return nil, nil
	case .Query_Info:
		return nil, .Mode_Not_Implemented
	}
	return nil, nil
}
6 Likes