what-is-an-interface.md (7626B)
1 +++ 2 draft = false 3 date = 2025-08-01T06:26:18-03:00 4 title = "What is an Interface" 5 description = "Behavioral Polymorphism: The foundation of OOP." 6 slug = "" 7 authors = [] 8 tags = ["OOP", "Go", "Odin", "Interface"] 9 categories = [] 10 externalLink = "" 11 series = [] 12 +++ 13 14 **Behavioral Polymorphism: The foundation of OOP.** 15 16 The **interface** is the foundation of OOP (Object Oriented Programming). 17 18 It provides _behavioral polymorphism_: constraining a type by its behaviours - 19 or more secifically, its method-set. 20 21 > A "method" (as termed in OOP langauges) is nothing more than a procedure who's 22 > first argument is a pointer to the object it is associated with. There is 23 > nothing special about a method. 24 25 **Definitions** 26 27 - **Poly**: many 28 - **Mono**: singular 29 - **Morphism**: shape 30 - **Polymorphism**: having many shapes 31 - **Monomorphism**: having one shape 32 33 Polymorphic refers to the ability to represent many shapes. 34 35 **Anology: Power Sockets** 36 37 You might have ten electrical appliances all completely different. 38 Toaster, microwave, blender. 39 The way they use the electricity are all different, but the power plug is the same. 40 The power socket provides a common interface that allows polymorphism with respect to the appliances. 41 As long as an appliance has a the correct plug, the power socket will accept it. 42 Thus the power socket is an interface that enables behavioral polymorphism. 43 44 Polymorphism in programing works the same way. 45 46 > In dynamically-typed programming every value is polymorphic, and only the _use_ of the value will reveal whether the particular morphism is valid. 47 > In statically-typed programming every value is monomorphic, and special types must be used to allow for polymorphism. 48 49 **What is a shape anyway?** 50 51 In programming a shape is some definable characteristic about a value. 52 - At a low level: how are the fields represented in memory 53 - At a high level: is it iterable, is it indexable, is it a reference, etc 54 55 We abstract the low level up to the high level. 56 57 OOP languages often build in language-level native support for interfaces since 58 this kind of polymorphism is foundational to the OOP paradigm. In low level 59 languages like C you will typically achieve polymorphism through data layout: 60 using flexible structures and memory indirection. 61 62 Interfaces allow for polymorphism in statically-typed languages. 63 That is, they don't require generics in the type-system to implement. 64 65 It is no coincidence that Java and Go started out statically-typed using interfaces. 66 Lacking parametric polymorphism, those languages often awkwardly relied on behavioral polymorphism. 67 68 ## The Fat Pointer 69 70 This section applies to _runtime_ interfaces, otherwise known as _dynamic dispatch_. 71 72 At the low level, an interface is nothing more than a **fat pointer**: a structure 73 containing a pair of pointers - one that points to the data, and the other that 74 points to a function that operates on the data. 75 76 This is all you need. 77 78 **Odin Example: Stream** 79 80 ```odin 81 Stream_Proc :: #type proc(stream_data: rawptr, mode: Stream_Mode, p: []byte, offset: i64, whence: Seek_From) -> (n: i64, err: Error) 82 83 Stream :: struct { 84 procedure: Stream_Proc, 85 data: rawptr, 86 } 87 ``` 88 89 This struct contains a pointer to a procedure and a pointer to the data. 90 It can be used to represent any kind of streaming operation: reading, writing, flushing, closing, etc, on any type. 91 92 Each specialized type (file, buffer, http body) simply needs to map itself to this shape and it can integrate with streaming logic. 93 94 **Converting a Buffer to a Stream** 95 96 ```odin 97 buffer_to_stream :: proc(b: ^Buffer) -> (s: io.Stream) { 98 s.data = b 99 s.procedure = _buffer_proc 100 return 101 } 102 ``` 103 104 It simply builds the stream struct using a pointer to the buffer sets the correct procedure. 105 106 To achieve behavioral polymorphism all you need is a common 107 representation (the fat pointer) that all specialized types can conform to. 108 109 **Usage Example: io.Reader** 110 111 ```odin 112 package main 113 114 import "core:bytes" 115 import "core:io" 116 import os "core:os/os2" 117 118 main :: proc() { 119 // Assign both the file and buffer reader to this to show they are the same shape. 120 reader: io.Reader 121 122 // Scratch is just a buffer to read into. 123 scratch := make([dynamic]byte, 1024 * 1024) 124 125 // Open a file and make a reader for it. 126 file, _ := os.open("file") 127 reader = os.to_reader(file) 128 129 // This call is operating on the file. 130 io.read_full(reader, scratch[:]) 131 132 // Allocate a byte buffer and make a reader for it. 133 buf: bytes.Buffer 134 bytes.buffer_init(&buf, make([dynamic]byte, 1024 * 1024)[:]) 135 reader = bytes.buffer_to_stream(&buf) 136 137 // This call is operating on the buffer. 138 io.read_full(reader, scratch[:]) 139 } 140 ``` 141 142 In this example, the `io.Reader` is the interface. 143 The `bytes.Buffer` and the `os.File` are the specialized types can map to it. 144 `io.read_full` can therefore operate on any type that can map to an `io.Reader`. 145 146 Behavioral polymorphism is achieved by matching shapes. No runtime or type system 147 support needed. 148 149 150 ## The VTable 151 152 **Virtual Table** 153 154 The Odin core prefers to (but doesn't require) to use a VTable-of-one. A single 155 procedure that can handle any possible operation of the interface. 156 157 This keeps the shape a simple fat-pointer, making it easier to construct and 158 more compact in memory. 159 160 The traditional way (e.g., C++) assigns each method its own entry in the VTable. 161 This means the pointer to the procedure becomes a pointer to a table of procedures. 162 163 Each entry into the virtual table represents a single method. 164 165 **Example: Multi-Method VTable** 166 167 ```odin 168 FileInterface :: struct { 169 vtable: FileVTable, 170 data: rawptr, 171 } 172 173 FileVTable :: struct { 174 read: proc(data: rawptr, p: byte[]) -> (n: int, err: Error), 175 write: proc(data: rawptr, p: byte[]) -> (n: int, err: Error), 176 close: proc(data: rawptr) -> Error, 177 } 178 ``` 179 180 As you can see, each additional method increases the memory required. 181 182 There is no **logical** difference between the two representations, only structural. 183 184 ## Windows COM 185 186 To see how far you can take behavioral polymorphism, look no further than Windows COM. 187 188 COM is an API and ABI implemented entirely around VTables — to the exclusion of all else. 189 190 You can call and implement a COM object in _any_ langauge, as long as the VTable 191 representation and behaviour of the methods are correct. 192 193 The point is not to teach COM, but to illustrate that interfaces are just VTables. 194 Behavioral polymorphism is a simple low-level idea that is abstracted into OOP 195 languages as a first class language feature. 196 197 Windows COM also shows how flexible the humble VTable can be — it allows a 198 polyglot object system. Fascinating, but probably not something to aspire to. 199 200 ## The Cost 201 202 Interfaces are simple structures, but they are not free. 203 204 - **Size**: at a minimum, two pointers. 205 - **Growth**: VTable size grows with number of methods. 206 - **Indirection**: Runtime method calls are indirect. 207 208 In large dynamic arrays, interface overhead can add up: 209 - **Memory cost** 210 - **Cache pressure** due to pointer indirection 211 - **Fragmented memory** for the actual data 212 213 With a more sophisticated type system, interfaces can be monomorphised at 214 compile-time (i.e., static dispatch). 215 216 This eliminates runtime cost but increases compile-time complexity. 217 218 ## Conclusion 219 220 Interfaces allow behavioral polymorpshim. 221 222 The classic runtime interface is implemented as a simple fat-pointer shape. 223 224 Concrete types only need to satisfy the shape of the interface and they can be used anywhere 225 that interface is used. 226 227 Interfaces are not scary, and not hard to implement at the low level. They are also not a 228 substitute for parametric polymorphism as I'm sure many Go programmers are well aware.