site

personal website, served at mordaunt.dev/site
Log | Files | Refs

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.