# What's the idiomatic design in zig when there is no interfaces/traits?

**URL:** <https://ziggit.dev/t/whats-the-idiomatic-design-in-zig-when-there-is-no-interfaces-traits/7817>\
**Category:** Explain\
**Tags:** language\
**Created:** [January 12, 2025, 7:42pm UTC](https://ziggit.dev/t/whats-the-idiomatic-design-in-zig-when-there-is-no-interfaces-traits/7817 "2025-01-12T19:42:09Z")\
**Posts on this page:** 5\
**Page:** 1

<div class="post-metadata">

**Author:** ![Integralist](https://ziggit.dev/user_avatar/ziggit.dev/integralist/32/4196_2.png) [@Integralist](https://ziggit.dev/u/Integralist)\
**Post date:** [January 12, 2025, 7:42pm UTC](https://ziggit.dev/t/whats-the-idiomatic-design-in-zig-when-there-is-no-interfaces-traits/7817/1 "2025-01-12T19:42:09Z")

</div>

👋 I’m only just starting my journey to learning zig today, and I was watching an interview from Mitchell Hashimoto about how coming from Go to Zig required a different programming mindset as there is not concept of interfaces/traits (like found in Go/Rust).

I would imagine then, that y’all define explicit types in your function signatures but he mentioned something about using ‘comptime’ instead to handle the interface use case.

I don’t really understand comptime right now so I was hoping y’all could help me with two things…

1. Understand comptime
2. Understand how it’s relevant to replacing interfaces/traits

Many thanks for any guidance you can give me.

---

<div class="post-metadata">

**Author:** ![n0s4](https://ziggit.dev/user_avatar/ziggit.dev/n0s4/32/2227_2.png) [@n0s4](https://ziggit.dev/u/n0s4)\
**Post date:** [January 12, 2025, 8:16pm UTC](https://ziggit.dev/t/whats-the-idiomatic-design-in-zig-when-there-is-no-interfaces-traits/7817/2 "2025-01-12T20:16:34Z")

</div>

The docs for comptime contains pretty much everything you need to know, read this first: [Documentation - The Zig Programming Language](https://ziglang.org/documentation/master/#comptime).

comptime is relevant to traits / interfaces because it is the mechanism behind static dispatch. If you want to write generic code right now in Zig, you just write a function that accepts a type as one of its parameters, then refer to that type either in the type of other parameter(s) or as the return type:

```zig
// types are also just values that are compile-time known
fn add(comptime T: type, a: T, b: T) T {
    return a + b;
}

```

One important concept that makes comptime make more sense is that Zig’s compiler is _lazy_. It does not analyze functions, declarations, or anything that your program doesn’t use, so it doesn’t care, for example, that `add(bool, true, false)` would cause an error. Only if you actually invoked such a function call, would it then analyze the function given `T=bool`, and report an error.

This means that zig’s generics is basically compile-time [duck typing](https://en.wikipedia.org/wiki/Duck_typing).

We can enforce type constraints similar to traits by ourselves to give the user a more informative error message if their type is wrong. This is achieved by `comptime` assertions like so:

```zig
fn add(comptime T: type, a: T, b: T) T {
    comptime {
        // all code in this scope is interpreted at compile-time.

        // @typeInfo is the builtin function which provides type reflection.
        // This is a very simple example, we can do much deeper inspection if we wanted.
        switch (@typeInfo(T)) {
            .int, .float => {},
            // If T is a numerical type, the compiler won't even look at this branch,
            // so it won't cause a compile error. Again, the compiler is *lazy*.
            else => @compileError("Can only add ints or floats!"),
        }
    }

    return a + b;
}

```

I’ll also showcase `anytype`, since its sometimes used instead of passing a separate `type` parameter, but it can’t do anything the previous method can’t also do.

```zig
// anytype allows generics without an explicitly named T
fn add(a: anytype, b: @TypeOf(a)) @TypeOf(a) {
    return a + b;
}

```

There is also the separate problem of dynamic/runtime dispatch, which is usually what “interfaces” refers to, this is unrelated to comptime and is achieved via function pointers/vtables.

---

<div class="post-metadata">

**Author:** ![ieh3oh](https://ziggit.dev/letter_avatar_proxy/v4/letter/i/c5a1d2/32.png) [@ieh3oh](https://ziggit.dev/u/ieh3oh)\
**Post date:** [January 13, 2025, 8:31pm UTC](https://ziggit.dev/t/whats-the-idiomatic-design-in-zig-when-there-is-no-interfaces-traits/7817/3 "2025-01-13T20:31:44Z")

</div>

Total Zig noob here, but this blog post talks about interfaces. I don’t fully understand it yet, but seems like runtime interfaces are possible in Zig.

> **[Zig Interfaces](https://www.openmymind.net/Zig-Interfaces/)**
>
> Understanding and implementing interfaces in Zig

Are “interfaces” an officially supported design pattern in Zig? For example, it seems like `std.mem.Allocator` is an “interface”, right? Coming from Go, `io.Reader` and `io.Writer` are well understood and encouraged interfaces. Not sure if Zig has those as well.

---

<div class="post-metadata">

**Author:** ![n0s4](https://ziggit.dev/user_avatar/ziggit.dev/n0s4/32/2227_2.png) [@n0s4](https://ziggit.dev/u/n0s4)\
**Post date:** [January 13, 2025, 8:53pm UTC](https://ziggit.dev/t/whats-the-idiomatic-design-in-zig-when-there-is-no-interfaces-traits/7817/4 "2025-01-13T20:53:28Z")

</div>

Creating interfaces like the one in that article (dynamic dispatch) is possible as a by-product of having function pointers and type erasure (`*anyopaque`) which is nothing that, say, C doesn’t have. That is to say, it is not a distinct language feature, but the way of creating interfaces like `std.mem.Allocator` is a common convention.

In languages where interfaces are a language feature, like Go, they are really just using function pointers/vtables under the hood, Zig just makes you do it yourself. This also forces the programmer to be aware of the cost of dynamic interfaces on runtime performance.

The question of reader/writer interfaces is particularly tricky in Zig, because I can think of three main ways to pass a generic reader/writer:

1. `std.io.Any(Reader|Writer)` is a function-pointer-based interface similar to `std.mem.Allocator`, and can be used when you don’t know what kind of reader/writer you want until runtime.

2. `std.io.Generic(Reader|Writer)` creates a static reader/writer type for a specific purpose at compile-time. This is used by `std.fs.File.(Reader|Writer)` and `std.ArrayList(u8).(Reader|Writer)`) as examples.

3. using `anytype` (explained above) as a writer type is a sort of inbetween which doesn’t have the runtime overhead of `std.io.Any(Reader|Writer)`, and is also more flexible then `std.io.GenericReader`. This is good for library code, for example `std.fmt.Format` and `std.json.stringify` from the standard library.

---

<div class="post-metadata">

**Author:** ![dee0xeed](https://ziggit.dev/letter_avatar_proxy/v4/letter/d/3ab097/32.png) [@dee0xeed](https://ziggit.dev/u/dee0xeed)\
**Post date:** [January 14, 2025, 6:33am UTC](https://ziggit.dev/t/whats-the-idiomatic-design-in-zig-when-there-is-no-interfaces-traits/7817/5 "2025-01-14T06:33:03Z")

</div>

Related [topic](https://ziggit.dev/t/interfaces-in-zig-gives-me-the-vibe-of-object-orientation-in-javascript/5650).
