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.

5 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.

7 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.

6 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
}
7 Likes

The answers here are a little confusing to me as they all are OOP, and I’m totally new to Odin with experience in C/C++, Go, Java, Kotlin and many more. An interface to me is a way of saying what something should contain, not what it actually contains.

Like if I have an Banana_Counter :: interface { number_of_bananas: int }, I would expect a proc(bc: Banana_Counter) to accept all structs containing the field number_of_bananas as int.

There aren’t really any other uses for interfaces. It’s to be able to know what data to expect from a pointer reference, without having to know or have access to the complete underlying type.

How do you do composition in Odin, or is it only OOP?

Edit: Sorry, I didn’t want to create a whole new thread for this.

I’m still learning too, but this is my understanding: In Odin, an “interface” is literally just a struct of function pointers (a la “interfaces” in C). That’s it. There’s no Interface keyword; it’s just a design pattern, and there’s no magic behind it, unlike most other languages. It’s not quite like Java’s Interface where it specifies what methods an object must have, or a trait in Rust, because both of those involve extra compiler magic to make sure objects satisfy the “interface”.

Allocator is an example of an interface in Odin. See also Interfaces: Fine.

I don’t believe this kind of thing is possible in Odin at runtime, and I’m not even sure if it’s possible to specify a specific field name for a struct with parametric polymorphism at compile time (see also Odin Intro (2 / 3) - Polymorphism). But also, this kind of thinking really is OOP-like and is probably not a direction you want to go in with Odin.

OOP is a spectrum really, and Odin is probably the least OOP-centric language you will find out there. It’s unabashedly procedural. For composition, just use structs and functions, and “interfaces” or parametric polymorphism only if needed. “Code like a 15 year old with 30 years of experience.”

If you want to do something similar to interfaces you need to use unions or generics and switch over them. I’ll give an example using go and how to translate it.

Go Code

package main

import "fmt"

type Print interface {
	Print()
}

type Foo struct {
	val int
}

type Bar struct {
	val int
}

func (f Foo) Print() {
	fmt.Println(f.val)
}

func (b Bar) Print() {
	fmt.Println(b.val)
}

func main() {
	foo := Foo{10}
	bar := Foo{10}

	foo.Print()
	bar.Print()
}

Odin code

package main

import "core:fmt"

Foo :: struct {
	val: int,
}

Bar :: struct {
	val: int,
}

FooBar :: union {
	Foo,
	Bar,
}

// using unions
print_foobar :: proc(s: FooBar) {
	switch i in s {
	case Foo:
		fmt.println(i.val)
	case Bar:
		fmt.println(i.val)
	case:
		panic("Invalid Type")
	}
}

// using generics
print_foo_bar :: proc(s: $T) {
	switch typeid_of(type_of(s)) {
	case Foo, Bar:
		fmt.println(s.val)
	case:
		panic("Invalid Type")
	}
}

main :: proc() {
	foo := Foo{10}
	bar := Bar{10}
	foobar: FooBar = bar

	print_foobar(foo)
	print_foobar(bar)
	print_foobar(foobar)
	print_foo_bar(foo)
}
1 Like

I’m not super fond of having to use generics, but I truly appreciate your examples of how this is solved (or maybe rather not solved) in Odin.

Thank you, it’s always difficult to discuss data structures especially in limited languages that are strongly opinionated. I’ll accept that it’s not possible to do composition and that I have to only use multiple inheritance to achieve anything close to type matching.

What do you mean by multiple inheritance? Odin has no inheritance.
The best solution would be what I showed using unions.

2 Likes

Union works, but another file can’t add its structs to the same union, which means where the union lives it has to always know of all structs. And on top of that it doesn’t limit the structs to a shared property, like having a specific field.

I did think a bit more on it though and I’ve found one solution, and that’s just having a common struct type of a field in all structs that support a certain action. This way the program can provide a reference to that field to procedures that can handle it.

This would be the same as single inheritance in OOP, but it’s clearly better than multiple. And the thing dealing with the struct must always know the full struct AND all the possible procedures that can handle it up front. No data driven programming, where the procedures can use the data without the data source itself being dependent on it.

Clearly the best solution would be if the data source wouldn’t be dependent on the parts of the program using it. Separating concerns helps a lot in all sorts of programs.

“I’ll accept that it’s not possible to do composition and that I have to only use multiple inheritance”

You’ve got that exactly backwards. FAQ

Odin’s data structures are really simple and easy to discuss. Advanced Types
What they aren’t is OOPS, which seems to be what you’re looking for.

1 Like