Does Odin has Interfaces?
No.
How can we have interfaces?
By abusing Unions, Structs, #type proc, parapoly and explicit overloading. We can have a something similar to interfaces.
What is an interface/trait/protocol/behaviour?
In Object Oriented Programming, an Interface (trait, protocol or behaviour in other languages and paradigms) is a description of all methods that an object must have in order to be an “X”. Programmers use the term to refer to what methods a subclass must implement to adhere to an interface. This is called “programming to an interface.” We define “programming to an interface” as a way for us to write code that implements the signatures of methods defined in said interface.
In procedural, typed, compiled languages such as Odin, we usually don’t need interfaces because our data and procedures are well separated. Sometimes you want packages to share a public API, so it must be a way to:
- Defining a set of procedures that must be implemented.
- Checking whether that set was actually implemented.
Shapes
This code will show how we can have multiple “interfaces” working together.
package main
import "core:fmt"
IAreaLengthBreadth :: struct {
impl: #type proc(length: int, breadth: int) -> int,
}
IAreaSide :: struct {
impl: #type proc(side: int) -> int,
}
IGetArea :: union {
IAreaSide,
IAreaLengthBreadth,
}
IPolygon :: struct {
side: int,
length: int,
breadth: int,
get_area: IGetArea,
}
Rectangle :: proc(length: int, breadth: int) -> IPolygon {
rect := IPolygon {
length = length,
breadth = breadth,
get_area = IAreaLengthBreadth{impl = proc(length: int, breadth: int) -> int {
return length * breadth
}},
}
return rect
}
Square :: proc(side: int) -> IPolygon {
square := IPolygon {
side = side,
length = side,
breadth = side,
get_area = IAreaSide{impl = proc(side: int) -> int {
return side * side
}},
}
return square
}
Shape :: proc {
Square,
Rectangle,
}
main :: proc() {
rect := Shape(5, 6)
square := Shape(4)
// pointer to our interface implementation
fmt.println(
((^IAreaLengthBreadth)(&rect.get_area)^).impl(rect.length, rect.breadth), // 30
)
fmt.println(
((^IAreaSide)(&square.get_area)^).impl(square.side), // 16
)
}
Simple Cache
This code will show how we can implement a simple cache “interface” in Odin.
package main
import "core:fmt"
ICache :: struct {
set: #type proc(key: string, value: string),
get: #type proc(key: string) -> string,
has: #type proc(key: string) -> bool,
}
SimpleCache :: proc() -> ICache {
return ICache{set = proc(key: string, value: string) {
fmt.println(key, " was stored with value: ", value)
}, get = proc(key: string) -> string {
fmt.println("got value for key: ", key)
return "value"
}, has = proc(key: string) -> bool {
fmt.println("key: ", key, " exists? ", true)
return true
}}
}
TransientCache :: proc() -> ICache {
return ICache{set = proc(key: string, value: string) {
fmt.println(key, " was transient stored with value: ", value)
}, get = proc(key: string) -> string {
fmt.println("got value for key: ", key)
return "value"
}, has = proc(key: string) -> bool {
fmt.println("key: ", key, " exists? ", true)
return true
}}
}
main :: proc() {
// hellope was stored with value: world
// got value for key: hellope
// key: hellope exists? true
cache := SimpleCache()
cache.set("hellope", "world")
cache.get("hellope")
cache.has("hellope")
// ginger was transient stored with value: beer
// got value for key: ginger
// key: ginger exists? true
transient := TransientCache()
transient.set("ginger", "beer")
transient.get("ginger")
transient.has("ginger")
}
Animals
This is a simple Animals example
package main
import "core:fmt"
Animal :: struct {
speak: #type proc() -> string,
}
Dog :: Animal {
speak = proc() -> string {
return "Woof!"
},
}
Cat :: Animal {
speak = proc() -> string {
return "Meow!"
},
}
Llama :: Animal {
speak = proc() -> string {
return "Hum"
},
}
// Woof!
// Meow!
// Hum
main :: proc() {
animals := []Animal{Dog, Cat, Llama}
for animal in animals {
fmt.println(animal.speak())
}
}
Circles and Rects
Demostrating the use of the ->
operator and subtype polymorphism.
package main
import "core:fmt"
import "core:math"
IGeometry :: struct {
area: #type proc(self: Geometry) -> f64,
perimeter: #type proc(self: Geometry) -> f64,
}
IRect :: struct {
width, height: f64,
using geometry: IGeometry,
}
ICircle :: struct {
radius: f64,
using geometry: IGeometry,
}
Geometry :: union {
IRect,
ICircle,
}
Rect :: proc(width, height: f64) -> IRect {
return IRect{width = width, height = height, perimeter = proc(self: Geometry) -> f64 {
rect := self.(IRect)
return 2 * rect.width + 2 * rect.height
}, area = proc(self: Geometry) -> f64 {
rect := self.(IRect)
return rect.width * rect.height
}}
}
Circle :: proc(radius: f64) -> ICircle {
return ICircle{radius = radius, perimeter = proc(self: Geometry) -> f64 {
circle := self.(ICircle)
return math.PI * circle.radius * circle.radius
}, area = proc(self: Geometry) -> f64 {
circle := self.(ICircle)
return 2 * math.PI * circle.radius
}}
}
measure_rect :: proc(geo: IRect) {
fmt.println(geo)
fmt.println(geo->area())
fmt.println(geo->perimeter())
}
measure_circle :: proc(geo: ICircle) {
fmt.println(geo)
fmt.println(geo->area())
fmt.println(geo->perimeter())
}
measure :: proc {
measure_rect,
measure_circle,
}
main :: proc() {
rect := Rect(3, 4)
measure(rect)
circle := Circle(5)
measure(circle)
}
Output
IRect{width = 3, height = 4, geometry = IGeometry{area = proc(Geometry) -> f64 @ 0x103242C40, perimeter = proc(Geometry) -> f64 @ 0x103242B30}}
12
14
ICircle{radius = 5, geometry = IGeometry{area = proc(Geometry) -> f64 @ 0x103242E30, perimeter = proc(Geometry) -> f64 @ 0x103242D40}}
31.41592653589793
78.53981633974483
This is Inline Inheritance
Odin does not have interfaces. Procs that are tied to structs and enforced by the compiler. Go, Zig, Rust all have this. Odin does not. If you want something similar you can check how runtime.Allocator is implemented. It wouldn’t be enforced by the compiler. It’s more of a, here’s a list of actions you may (or may not) be able to do.
Doing something like this oftentimes, making something that generic generally causes more issues than making it solves. Is recommended to go with simplest possible thing a person could implement without forcing them to write a bunch of interface boilerplate. The other approach to making something broadly compatible is to yield data in a format that can easily be transformed into a variety of other formats
Thanks
- @gingerBill
- @jasonKercher
- Thag
- Scoobery Doobery