Skip to content

Interfaces

Harneet now ships with full interface support in both AST and bytecode/JIT modes. Interfaces enable structural typing, zero-boilerplate polymorphism, and powerful runtime type inspection. This guide walks through every major capability.

Overview

Interfaces describe behavior rather than data. Any struct that exposes the required methods automatically satisfies an interface—no explicit implements clause is needed.

Key characteristics:

  • Implicit implementation: a struct satisfies an interface if it provides all required methods with matching signatures.
  • Structural typing: method sets determine compatibility (no inheritance hierarchy).
  • First-class values: interface instances carry both the interface type and the concrete value.
  • Works everywhere: assignment, function parameters, collections, and serialization all support interface values.

Declaring Interfaces

package main

// Interfaces list required methods
 type Reader interface {
     Read() string
 }

 type Writer interface {
     Write(string)
 }

 // Interfaces may be embedded inside other interfaces
 type ReadWriter interface {
     Reader
     Writer
 }

Interfaces can reference other interfaces (embedding) or define their own method set. Methods look exactly like struct methods.

Implementing Interfaces

Implementation is implicit—no keywords required. If a struct declares every method in the interface, it automatically satisfies it.

type File struct {
    name    string
    content string
}

function (f File) Read() string {
    return f.content
}

function (f File) Write(value string) {
    f.content = value
}

// File now satisfies Reader, Writer, and ReadWriter with zero extra code.

Interface Assignment

Assign a struct to an interface variable to wrap it automatically:

1
2
3
var file = File{name: "notes.txt", content: "Hello"}
var reader Reader = file     // implicit wrapping
var rw ReadWriter = file     // satisfies larger interface too

Behind the scenes Harneet stores:

  1. The interface type information.
  2. The concrete struct value.

This metadata is used for dispatch and runtime checks.

Dynamic Dispatch

Method calls on interface values automatically invoke the concrete implementation:

1
2
3
4
5
6
7
8
9
function printContent(r Reader) {
    fmt.Printf("Content: %s\n", r.Read())
}

function main() {
    var f File = File{content: "Hi"}
    var r Reader = f
    printContent(r)         // prints "Content: Hi"
}

Dispatch works identically in AST and bytecode modes. Bytecode uses a lightweight map wrapper internally and unwraps values on demand, so no special syntax is needed.

Type Assertions

Use value.(Type) to recover concrete values or validate types at runtime.

1
2
3
4
5
6
var reader Reader = File{content: "doc"}
var file = reader.(File)           // succeeds
fmt.Println(file.content)

// runtime error: interface holds File, not Buffer
var buffer = reader.(Buffer)

Type assertion failures produce descriptive runtime errors that include the actual concrete type.

Suggested Pattern

Wrap assertions with helper functions to emit friendly errors:

1
2
3
function expectFile(r Reader) File {
    return r.(File)
}

Type Switches (.type() Syntax)

Harneet uses a method-like syntax for type switches:

function describe(r Reader) {
    switch v := r.type() {
    case File {
        fmt.Printf("File with %s\n", v.content)
    }
    case Buffer {
        fmt.Printf("Buffer with %d bytes\n", len(v.data))
    }
    default {
        fmt.Println("Unknown reader")
    }
}

Why .type() instead of Go’s .(type)? It matches Harneet’s method call ergonomics, avoids parser ambiguities, and is easier to remember.

Type switches automatically unwrap interface values when a case matches, binding the concrete value to the switch variable (here v).

Bytecode/JIT Guarantees

All interface features work in bytecode mode (the default execution model):

  • Interface registration and metadata
  • Satisfaction checks on assignment
  • Dynamic dispatch via interface.method()
  • Type assertions
  • Type switches

The implementation uses the same opcodes described in BYTECODE_INTERFACE_IMPLEMENTATION_PLAN.md, so behavior is consistent across execution modes.

Comprehensive Example

package main

import fmt

// Interfaces
type Shape interface {
    Area() float64
    Perimeter() float64
}

type Colorful interface {
    Color() string
}

// Structs
type Circle struct {
    radius float64
    color  string
}

function (c Circle) Area() float64 {
    return 3.14159 * c.radius * c.radius
}

function (c Circle) Perimeter() float64 {
    return 2.0 * 3.14159 * c.radius
}

function (c Circle) Color() string {
    return c.color
}

function describe(shape Shape) {
    fmt.Printf("Area: %f\n", shape.Area())
    fmt.Printf("Perimeter: %f\n", shape.Perimeter())

    switch v := shape.type() {
    case Circle {
        fmt.Printf("Circle color: %s\n", v.Color())
    }
    default {
        fmt.Println("Non-colorful shape")
    }
}

function main() {
    var circle Shape = Circle{radius: 10.0, color: "teal"}
    describe(circle)

    var c = circle.(Circle)
    fmt.Printf("Unwrapped radius: %f\n", c.radius)
}

Testing & Examples

Extensive interface-focused samples live in examples/interfaces/:

  • bytecode_smoke_test.ha
  • interface_edge_cases_test.ha
  • interface_assignment_test.ha
  • type_assertion_test.ha
  • type_switch_comprehensive.ha

Run them together via:

just test_interfaces

How to Fix Interface Errors

The typechecker performs structural checks to ensure that concrete types actually implement the interfaces you use in:

  • Variable declarations: var g Greeter = person
  • Assignments: g = person
  • Function and method calls: useGreeter(person) when useGreeter(g Greeter)

Here are the most common error patterns and how to fix them.

1. Missing All Interface Methods

Example error

type Person does not implement interface Greeter when assigning to g: missing all interface methods

Why it happens

  • The struct type (Person) has no methods registered that match the interface Greeter.
  • The typechecker cannot find any implementation of the interface’s method set on that type.

How to fix it

  • Add all required methods from Greeter to Person:
  • Same method names.
  • Compatible parameter and return types.
  • Or, change the variable or parameter type to a different interface or concrete type that your struct already implements.

2. Missing a Specific Method

Example error

type MyStruct does not implement interface MyInterface when assigning to i: missing method DoSomething

Why it happens

  • MyStruct defines some methods, but one or more required methods are missing entirely.
  • In this example, MyInterface declares DoSomething, but MyStruct does not.

How to fix it

  • Add the missing method(s) to MyStruct:
  • Use exactly the same method name as in the interface.
  • Ensure the parameter and return types match the interface declaration.
  • Or, adjust the type of the variable/parameter (i) or change the interface definition if its contract is no longer what you want.

3. Method Signature Mismatch

Example error

method DoSomething on type MyStruct does not match interface MyInterface when assigning to i

Why it happens

  • MyStruct defines a method with the correct name, but its signature does not match the interface.
  • Common mismatches:
  • Different number of parameters.
  • Different parameter types (e.g., string vs int).
  • Different return types or counts.

How to fix it

  • Update the method on the struct to match the interface exactly:
  • Same parameter count and types.
  • Same return type list.
  • Or, adjust the interface if the intended contract has changed.

4. Function Arguments That Don’t Implement the Interface

When passing values into functions that expect an interface parameter, the typechecker applies the same structural rules.

Example

type Greeter interface {
    Greet() string
}

function useGreeter(g Greeter) {
    fmt.Println(g.Greet())
}

type Bad struct {}

// Bad has no Greet method
var b = Bad{}
useGreeter(b)  // error

Typical error

type Bad does not implement interface Greeter when assigning to argument 1: missing method Greet

How to fix it

  • Implement Greet() on Bad, or
  • Change the parameter type of useGreeter to a different interface or a concrete type that doesn’t require Greet, or
  • Pass a different type that already implements Greeter.

5. Empty Interfaces

Empty interfaces (no methods) are always satisfied by any type:

1
2
3
4
5
type Any interface {
}

var x Any = 42
var y Any = Circle{radius: 10}

You will not see structural interface errors for empty interfaces, because there is no method contract to enforce.

6. When Static Checking Is Skipped

In a few cases Harneet’s typechecker intentionally defers to runtime:

  • The interface type is not known in the current compilation unit (for example, imported from another module that the checker does not see as a type ... interface declaration).
  • The static type of the value is itself an interface.

In these scenarios, assignments and calls are allowed, and any incompatibilities are surfaced by runtime checks instead of static errors.


Interfaces unlock ergonomic abstractions without sacrificing performance or clarity. Pair them with Harneet’s structural typing, rich standard library, and async features to build expressive systems with minimal boilerplate.