{"article":{"slug":"we-have-traits-at-home","title":"We have Traits at Home","subtitle":null,"summary":"Axel Magnuson shares techniques for abstraction in Zig, comparing comptime duck typing and interfaces with vtables against a trait pattern inspired by OCaml modules, then building trait helpers and trait functors on top of it.","content_type":"tutorial","language":"en","canonical_url":"https://www.axelmagn.com/blog/202610-zig-traits/","author":{"name":"Axel Magnuson","url":null,"person_slug":null,"person_url":null},"authored_by":"human","publisher":{"name":"Mesoloop","url":"https://www.axelmagn.com/","listing_slug":null,"listing":null},"topics":[{"name":"Zig","slug":"zig","url":"https://listedarticles.com/topics/zig"},{"name":"Programming Languages","slug":"programming-languages","url":"https://listedarticles.com/topics/programming-languages"},{"name":"Programming","slug":"programming","url":"https://listedarticles.com/topics/programming"}],"about_listings":[],"cover_image_url":null,"license":"all-rights-reserved","word_count":4073,"reading_minutes":18,"published_at":"2026-10-03T00:00:00.000Z","added_at":"2026-10-22T23:12:45.804Z","updated_at":"2026-10-22T23:12:45.804Z","added_via":"api","contributor":{"type":"agent","name":"ListedStartups Using Bot","registered":true},"profile_url":"https://listedarticles.com/articles/we-have-traits-at-home","markdown_url":"https://listedarticles.com/articles/we-have-traits-at-home.md","example":false,"citation":"Axel Magnuson, Mesoloop. \"We have Traits at Home.\" 3 Oct 2026. https://www.axelmagn.com/blog/202610-zig-traits/ (all-rights-reserved)","access":{"human_view":"preview","full_text_available":true,"source_url":"https://www.axelmagn.com/blog/202610-zig-traits/"},"body_markdown":"# We have Traits at Home\n\n2026-10-03\n·\n[zig](https://www.axelmagn.com/tags/zig/),\n[traits](https://www.axelmagn.com/tags/traits/),\n[polymorphism](https://www.axelmagn.com/tags/polymorphism/),\n[abstraction](https://www.axelmagn.com/tags/abstraction/)\n\nI have had more time recently to write code in Zig, and it has been good for my\nsoul. I wanted to share some insights I have arrived at regarding abstractions\nin Zig, including some techniques that I have found useful but not seen employed\nmore broadly.\n\nConsiderable[[1]](https://www.axelmagn.com/blog/202610-zig-traits/#fn-seguin2023) ink[[2]](https://www.axelmagn.com/blog/202610-zig-traits/#fn-figuratively) has[[3]](https://www.axelmagn.com/blog/202610-zig-traits/#fn-wong2025) been[[4]](https://www.axelmagn.com/blog/202610-zig-traits/#fn-mckay2025)\nspilled[[5]](https://www.axelmagn.com/blog/202610-zig-traits/#fn-cypert2026) on Zig Interfaces, and justifiably so. While not a language\nfeature, interfaces are the de-facto way of implementing runtime polymorphism in\nZig. Yet[[6]](https://www.axelmagn.com/blog/202610-zig-traits/#fn-kelley2021) more[[7]](https://www.axelmagn.com/blog/202610-zig-traits/#fn-redig2025) has been said about zig's comptime, and\nhow it may be leveraged to avoid the performance woes of dynamic dispatch and\nwrite slick, data-oriented code.\n\nTo put it briefly, polymorphism in Zig is conventionally obtained in one of two\nways:\n\n2. **Dynamic Interfaces**: Use opaque pointers, virtual tables, and dynamic\n   dispatch to achieve runtime polymorphism.\n3. **Comptime Duck Typing**: Use type reflection, switch statements, and\n   monomorphism at compile time to achieve static polymorhpism.\n\nThis state of affairs has proven to be quite effective so far, but it is not\nwithout its tradeoffs.\n\nInterfaces offer the sort of type contracts that we are used to from other\nlanguages. A function that accepts an interface has a guarantee of its method\nsignatures, but not its implementation. However because interfaces are, by\ndesign, modular at runtime, they also introduce a hard barrier for the compiler\nand thus a performance overhead. There is enough indirection that it often\ncannot effectively apply the optimizations that it would to monomorphic code.\n\nFor cases of performance critical code, Zig code typically relies on comptime\nduck typing. A function argument is either `anytype` or parameterized by a type\nargument `T`, and the function implementation will either perform compile-time\nreflection or make assumptions about the type's methods that can be checked at\ncompile-time. Implementations can then be monomorphised, similar to C++\ntemplating or C macros. This is slightly more safe than runtime duck typing,\nbecause any interface violations will be caught at compile time rather than\nruntime, but the caller and callee must still be careful to document and follow\nthe contract of what types may be passed.\n\nIf you are coming from another strongly typed language, you would be forgiven\nfor finding this arrangement rather simplistic. This is perhaps by design\n-- Zig aims to be simple. However if you have grown accustomed to Rust's traits\nor Haskell typeclasses, there appears to be no ready equivalent in Zig.\n\nI will argue that, with a little adaptation, it is perfectly possible to express\nsimilar abstractions over types in Zig userland. I would propose a new\n**convention** for defining compile-time type contracts as **Traits** in Zig. It\ndoesn't require any libraries or machinery - in fact if fits remarkably well\ninto vanilla Zig. It is inspired by the functional programming world in general,\nand OCaml in particular.\n\n> **Definition:** a Trait is a compile-time struct which contains related\n> functions over a given type. [[8]](https://www.axelmagn.com/blog/202610-zig-traits/#fn-ocaml-inspired)\n\nSo as not to bury the lead, let me begin with a brief example of how we might\ndefine a trait for folding (reducing) on a type.\n\n```\n/// T forms a Reducer under an empty value and a combine operation\nfn Reducer(T: type) type {\n    return struct {\n        empty: T,\n        combine: fn (T, T) T,\n\n        fn reduce(self: @This(), xs: []const T) T {\n            var accum = self.empty;\n            for (xs) |x| accum = self.combine(accum, x);\n            return accum;\n        }\n    };\n}\n\n/// i32 forms a sum reducer under 0 and addition\nconst sum_trait = impl(Reducer(i32), struct {\n    pub const empty = 0;\n    pub fn combine(x: T, y: T) T { return x + y; }\n});\n\n/// i32 forms a product reducer under 1 and multiplication\nconst product_trait = impl(Reducer(i32), struct {\n    pub const empty = 1;\n    pub fn combine(x: T, y: T) T { return x * y; }\n});\n\nexport fn run(buf: [*c]i32, n: usize) i32 {\n    const sum = sum_trait.reduce(buf[0..n]);\n    const product = product_trait.reduce(buf[0..n]);\n    return sum + product;\n}\n```\n\nUgh. Isn't that just buttery smooth?[[9]](https://www.axelmagn.com/blog/202610-zig-traits/#fn-so-good) The contract is clear, LSP\nautocompletion picks it up, the compiler even knows to inline it and unroll the\nloop.\n\nTraits are just bags of functions and types that tell the receiving function how\nto execute subtasks that it's not responsible for defining. They behave a lot\nlike vtables, except that they are evaluated at comptime rather than runtime.\n\n## Conventional Polymorphism vs Traits\n\nTo justify traits, let's take a tour through Zig polymorphism with a common\nmotivating example: arithmetic operations. I've seen a few complaints over the\nyears about how awkward is it to do math in Zig over user-defined types. In its\npursuit of explicit control flow, Zig has eschewed operator overloading, and\nthis can be jarring if you're coming from other typed languages.\n\nLet's imagine we have some user-defined types like `Rational` and `Complex`, and\nwe want to define their basic arithmentic operations. We would probably end up\nwith something like the following.\n\n```\nconst Rational = struct {\n    numerator: i32,\n    denominator: i32,\n\n    fn add(lhs: Rational, rhs: Rational) Rational {...}\n    fn sub(lhs: Rational, rhs: Rational) Rational {...}\n    fn mul(lhs: Rational, rhs: Rational) Rational {...}\n    fn div(lhs: Rational, rhs: Rational) Rational {...}\n};\n\nconst Complex = struct {\n    real: f32,\n    imaginary: f32,\n\n    fn add(lhs: Complex, rhs: Complex) Complex {...}\n    fn sub(lhs: Complex, rhs: Complex) Complex {...}\n    fn mul(lhs: Complex, rhs: Complex) Complex {...}\n    fn div(lhs: Complex, rhs: Complex) Complex {...}\n};\n```\n\nFor the sake of having something to do with them, let's say that we want to\ncompute the\n[Möbius transformation](https://en.wikipedia.org/wiki/M%C3%B6bius_transformation):\n`(a * x + b) / (c * x + d)`, which is useful for complex numbers as well as\nrationals. To keep things simple, we'll assume that all arguments to the\nfunction are of the same type.\n\n### Comptime Duck Typing\n\nIf we were to implement the Mobius transformation naively, we could just use\nbuilt-in operators.\n\n```\nfn mobius(T: type, x: T, a: T, b: T, c: T, d: T) T {\n    return (a * x + b) / (c * x + d);\n}\n```\n\nThis would work fine, as long as the inputs were all floats of the same type.\nDivision would break for integers, and of course it wouldn't work with our\nuser-defined types, but it's a starting point.\n\nWe could of course notice that both of our user-defined types have arithmetic\nfunctions with matching names and similar signatures, which would lend\nthemselves to duck typing as well.\n\n```\nfn mobius(T: type, x: T, a: T, b: T, c: T, d: T) T {\n    const numerator = a.mul(x).add(b);\n    const denominator = c.mul(x).add(d);\n    return numerator.div(denominator);\n}\n```\n\nThis will work for our user-defined types, but obviously not with Zig's\nprimitive types. We would have to wrap those in our own user-defined types with\ntheir own functions. More troublingly, this sort of ties us to a sort of\nobject-oriented pattern: the de-facto way to obey this abstraction contract is\nto define methods on the struct.\n\nThe good news is that it's all monomorphised at compile time, allowing the\ncompiler to simplify and inline these function calls.\n\n### Interfaces & VTables\n\nIf we wanted this to work dynamically at runtime, we could perhaps declare a\n`Num` interface that uses a VTable and opaque pointer to dynamically dispatch\nthese math operations.\n\n```\nconst Num = struct {\n    value: [32]u8,\n    vtable: *const VTable,\n\n    const VTable = struct {\n        add: *const fn([*]u8, [*]u8) Num,\n        sub: *const fn([*]u8, [*]u8) Num,\n        mul: *const fn([*]u8, [*]u8) Num,\n        div: *const fn([*]u8, [*]u8) Num,\n    };\n\n    fn add(lhs: Num, rhs: Num) Num { ... }\n    fn sub(lhs: Num, rhs: Num) Num { ... }\n    fn mul(lhs: Num, rhs: Num) Num { ... }\n    fn div(lhs: Num, rhs: Num) Num { ... }\n\n    fn mobius(x: Num, a: Num, b: Num, c: Num, d: Num) {\n        const numerator = a.mul(x).add(b);\n        const denominator = c.mul(x).add(d);\n        return numerator.div(denominator);\n    }\n}\n```\n\nWe would then need to wire up `Complex` and `Rational` to expose\n`fn num(self: *Self) Num` (or some such) that wraps our arithmentic functions,\nwhich is a little too tedious to reproduce here, but it would work fine on the\nhappy path. You would of course need to be a bit careful: there's nothing\nstopping you from mixing complex and rational numbers in your function calls,\nand this would lead to undefined behavior. You could probably add some saftey\nchecks with a bit of overengineering, but we won't get into that here.\n\nNot only is this clunkier than comptime duck typing, it will also be slower. We\nare effectively demanding dynamic dispatch whenever we want to do a math\noperation, which will trip up the compiler and have a significant performance\nimpact if we're doing a lot of math.\n\n### Traits\n\nWhen we define the mobius function, we really want to say \"I don't know what\ntype I'm dealing with, but it needs to have arithmetic operations.\" In this sort\nof case, I think it makes the most sense to just pass the functions it needs to\nuse at compile-time. This would remove any ambiguity, at the cost of being\nrather explicit.\n\n```\nfn mobius(\n    comptime T: type,\n    comptime add: fn (T, T) T,\n    comptime mul: fn (T, T) T,\n    comptime div: fn (T, T) T,\n    x: T,\n    a: T,\n    b: T,\n    c: T,\n    d: T,\n) T {\n    const numerator = add(mul(a, x), b);\n    const denominator = add(mul(c, x), d);\n    return div(numerator, denominator);\n}\n```\n\nThis is effectively compile-time dependency injection at a really fine-grained\nlevel. The signature is rather verbose, but there's actually a lot less\nboilerplate to write for `Complex` or `Rational` compared to runtime interfaces.\n\n```\nmobius(Complex, Complex.add, Complex.mul, Complex.div, x, a, b, c, d);\nmobius(Real, Real.add, Real.mul, Real.div, x, a, b, c, d);\n```\n\nFrom here, it's not a huge leap to traits. We're effectively just cleaning up\nthe function signature by gathering together the arithmetic functions into one\nstruct.[[10]](https://www.axelmagn.com/blog/202610-zig-traits/#fn-num-is-simplified)\n\n```\nfn Num(T: type) type {\n    return struct {\n        add: fn (T, T) T,\n        sub: fn (T, T) T,\n        mul: fn (T, T) T,\n        div: fn (T, T) T,\n    };\n}\n\nfn mobius(T: type, num: Num(T), x: T, a: T, b: T, c: T, d: T) T {\n    const numerator = num.add(num.mul(a, x), b);\n    const denominator = num.add(num.mul(c, x), d);\n    return num.div(numerator, denominator);\n}\n```\n\nFrom here, I usually just declare the trait implementation as part of my type.\n\n```\nconst Complex = struct {\n    // fields...\n    const num = Num(Complex) { .add = add, .sub = sub, .mul = mul, .div = div };\n    // functions (add, sub, mul, div...)\n};\n\nmobius(Complex, Complex.num, x, a, b, c, d);\n```\n\nWhat I really like about this approach is how flexible it is, while remaining\nvery clear about the contract between the function and type. For example,\nimagine that we wanted to extend our definition of `Num` to the primitive types\nlike `i32` and `f32`. There are a bunch of ways we could do this, but I would\nprobably just define a function that produces something appropriate for integer\nand float types.\n\n```\nfn builtinNum(T: type) Num(T) {\n    const inner = struct {\n        fn add(lhs: T, rhs: T) T { return lhs + rhs; }\n        fn sub(lhs: T, rhs: T) T { return lhs - rhs; }\n        fn mul(lhs: T, rhs: T) T { return lhs * rhs; }\n    };\n    const div = switch (@typeInfo(T)) {\n        .int, .comptime_int => struct {\n            fn div(lhs: T, rhs: T) T { return @divTrunc(lhs, rhs); }\n        }.div,\n        .float, .comptime_float => struct {\n            fn div(lhs: T, rhs: T) T { return lhs / rhs; }\n        }.div,\n        else => @compileError(\"unsupported type: \" ++ @typeName(T)),\n    };\n    return .{ .add = inner.add, .sub = inner.sub, .mul = inner.mul, .div = div };\n}\n\nmobius(f32, builtinNum(f32), x, a, b, c, d);\n```\n\nTraits are also transparent to the compiler, which will happily monomorphize and\ninline these functions. I prefer this over runtime polymorphism most of the\ntime, especially in hot loops or for library functions that are unopinionated on\ndata layout.\n\n### Limitations\n\nYou'll notice that traits have to be explicitly conditioned on a known type.\nUnlike interfaces, there's no way to say \"give me a number, I don't care what\nkind.\" For some applications, it would be nice if we could have an associated\ntype.\n\n```\nfn Iter(Elem: type) type {\n    return struct {\n        T: type,\n        next: fn (T) ?Elem,\n    };\n}\n```\n\nUnfortunately, the above isn't valid Zig, and it's unclear how that would even\nwork at a language level. Instead, traits insist that the types they are\noperating on are known in advance.\n\n```\nfn Iter(T: type, Elem: type) type {\n    return struct {\n        next: fn (T) ?Elem,\n    };\n}\n```\n\nI have not yet found an ergonomic way around this. Traits do not play very\nwell with type erasure.\n\n## Trait Helpers\n\nBefore proceeding, I want to introduce some small helper functions that have\nstreamlined how I work with traits.\n\n### impl\n\nIf you adopt traits as a programming pattern and convention, you'll quickly end\nup defining functions and passing them around. You will have already noticed\nabove that I define a lot of throwaway inner struct types, just so that I can\npull functions out of them. This has led to the following convenience function\nfor declaring trait implementations, which removes some of the boilerplate.\n\n```\npub fn impl(comptime Trait: type, comptime Impl: type) Trait {\n    comptime {\n        var result: Trait = undefined;\n        for (@typeInfo(Trait).@\"struct\".field_names) |field_name| {\n            @field(result, field_name) = @field(Impl, field_name);\n        }\n        return result;\n    }\n}\n```\n\nI use this all over the place to derive trait implementations from anonymous\nstructs. For example, we could use it to rewrite `builtinNum` to be a little\nmore concise and functional. Note that the declarations need to be public for\nthe type reflection to work.\n\n```\nfn builtinNum(T: type) Num(T) {\n    return impl(Num(T), struct {\n        pub fn add(lhs: T, rhs: T) T { return lhs + rhs; }\n        pub fn sub(lhs: T, rhs: T) T { return lhs - rhs; }\n        pub fn mul(lhs: T, rhs: T) T { return lhs * rhs; }\n\n        pub const div = switch (@typeInfo(T)) {\n            .int, .comptime_int => struct {\n                fn div(lhs: T, rhs: T) T { return @divTrunc(lhs, rhs); }\n            }.div,\n            .float, .comptime_float => struct {\n                fn div(lhs: T, rhs: T) T { return lhs / rhs; }\n            }.div,\n            else => @compileError(\"unsupported type: \" ++ @typeName(T)),\n        };\n    });\n}\n```\n\n### getTrait\n\nMost of the time, I don't mind passing trait implementations around explicitly.\nI've gotten into the habit, and just don't find it to be that big of a deal.\nTrait functors make it pretty easy to reduce the boilerplate, and I like having\nexplicit control over how things are implemented.\n\nHowever when I'm dealing with my own structs, 99% of the time the trait\nimplementation I'm looking for is going to be declared as a const on the struct\nitself. I use the function below to to easily retrieve or construct the most\ncommon cases.\n\n```\nfn getTrait(Trait: type, Impl: type) Trait {\n    comptime {\n        // try to find the trait declared explicitly as a member\n        for (@typeInfo(Impl).@\"struct\".decl_names) |decl_name| {\n            if (@TypeOf(@field(Impl, decl_name)) == Trait) {\n                return @field(Impl, decl_name);\n            }\n        }\n        // otherwise throw an error\n        @compileError(\"trait \" ++ @typeName(Trait) ++ \" not found on \" ++ @typeName(Impl));\n    }\n}\n```\n\nThis is basically equivalent to saying \"eh, it's duck typed. figure it out\".\nJust because we *are* explicit, doesn't mean that we *can't* be lazy. If we want\nto take this full circle back to duck typing, we can even define trait functors\nthat rely on `getTrait` to extract traits from the types they pass.\n\n```\nfn duckMobius(T: type) fn(T, T, T, T) T {\n    comptime {\n        const num = getTrait(Num(T), T);\n        return makeMobius(T, num);\n    }\n}\n```\n\n## Trait Functors\n\nWith some helpers defined, let's return to the `Num` example.\n\nTraits in Zig resemble traits in Rust, typeclasses in Haskell, or modules in\nOcaml. In Rust or Haskell, they would be resolved from a global registry managed\nby the compiler. This can feel very convenient, but it violates Zig's principle\nof explicit control flow. Rust and Haskell can require a fair bit of hunting for\nwhere a trait is defined, especially when something's wrong and the LSP isn't\nresolving it properly. It can also be a contributor to long compile times.\nResolving a generic trait in these languages is often equivalent to evaluating a\nproof, and that can take time.\n\nOcaml modules, however, work quite a bit like Zig traits: modules are\nfirst-class objects, and they are passed as arguments to the functions that\nconsume them. There is no inference of what modules apply to a type - you locate\nthem and pass them explicitly. If we were to define `Num` as a module, it might\nlook something like the following.\n\n```\n(* signature *)\nmodule type NUM = sig\n  type t\n  val add : t -> t -> t\n  val sub : t -> t -> t\n  val mul : t -> t -> t\n  val div : t -> t -> t\nend\n\n(* Implementation on float *)\nmodule Float : NUM with type t = float = struct\n  type t = float\n  let add = ( +. )\n  let sub = ( -. )\n  let mul = ( *. )\n  let div = ( /. )\nend\n\n(* function *)\nlet mobius (type a) (module N : NUM with type t = a) x a b c d =\n  let numerator = N.add (N.mul a x) b in\n  let denominator = N.add (N.mul c x) d in\n  N.div numerator denominator\n\n(* Usage *)\nlet res = mobius (module Float) x a b c d)\n```\n\nBecause modules in Ocaml are first-class values, they can actually take it a\nstep further. Rather than defining `mobius` as written above, it is common in\nOcaml to make use of \"module functors\", which are simply functions that take\nmodules as arguments and return new modules. If we wanted to define `mobius` in\nterms of a given numeric type, we might instead write it:\n\n```\n(* MakeMobius receives a NUM module and returns a new module with mobius function *)\nmodule MakeMobius (N : NUM) = struct\n  let mobius x a b c d =\n    let numerator = N.add (N.mul a x) b in\n    let denominator = N.add (N.mul c x) d in\n    N.div numerator denominator\nend\n\n(* usage *)\nmodule FloatMobius = MakeMobius (Float)\nlet res = FloatMobius.mobius x a b c d\n```\n\nThis is effectively currying: we are applying the module arguments now, and the\nvalue arguments later. However it derives a mobius function that doesn't need to\nbe taking a bunch of type arguments all the time, and that module can in turn be\nused in later constructions. This translates straightforwardly to Zig. If we\nwanted to adhere strictly to the traits pattern, it might look like the\nfollowing.\n\n```\nfn Mobius(T: type) type {\n    return struct {\n        mobius: fn(T, T, T, T, T) T,\n    };\n}\nfn makeMobius(T: type, num: Num(T)) Mobius(T) {\n    return impl(Mobius(T), struct {\n        fn mobius(x: T, a: T, b: T, c: T, d: T) T {\n            const numerator = num.add(num.mul(a, x), b);\n            const denominator = num.add(num.mul(c, x), d);\n            return num.div(numerator, denominator);\n        }\n    });\n}\nconst complex_mobius = MakeMobius(Complex, Complex.num);\ncomplex_mobius.mobius(x, a, b, c, d);\n```\n\nThis works fine, but I think it's even simpler to move the functor into\n`Mobius(T)`:\n\n```\nfn Mobius(T: type) type {\n    return struct {\n        mobius: fn(T, T, T, T, T) T,\n\n        fn make(num: Num(T)) Mobius(T) {\n            return impl(Mobius(T), struct {\n                fn mobius(x: T, a: T, b: T, c: T, d: T) T {\n                    const numerator = num.add(num.mul(a, x), b);\n                    const denominator = num.add(num.mul(c, x), d);\n                    return num.div(numerator, denominator);\n                }\n            });\n        }\n    };\n}\nconst complex_mobius = Mobius(Complex).make(Complex.num);\ncomplex_mobius.mobius(x, a, b, c, d);\n```\n\nHowever for single functions like this, unless I knew it would need to be\nconsumed by another function as a trait, I would probably be tempted to just\nimplement it as a type-curried function rather than a full-blown trait:\n\n```\nfn makeMobius(T: type, num: Num(T)) fn(T, T, T, T) T {\n    return struct {\n        fn mobius(x: T, a: T, b: T, c: T, d: T) T {\n            const numerator = num.add(num.mul(a, x), b);\n            const denominator = num.add(num.mul(c, x), d);\n            return num.div(numerator, denominator);\n        }\n    }.mobius;\n}\n\nconst complexMobius = makeMobius(Complex, Complex.num);\ncomplexMobius(x, a, b, c, d);\n```\n\nThe versatility of Zig's comptime makes all of these more or less equivalent to\nthe compiler. It's just a stylistic choice, depending on what the situation\ncalls for.\n\nAlso, notice that technically we have already used trait functors when we\ndefined `builtinNum`. It's not a deep, technical concept. It's just a function\nthat constructs a trait implementation for you.\n\n## Useful Traits\n\nHaving thoroughly introduced traits, let's talk about what we can do with them.\n\nIn this section are a few traits that I find myself reaching for with enough\nregularity that I keep a file around containing them. All of these are fairly\nhumdrum (but wildly applicable) abstractions that I lifted wholecloth from\nother languages.\n\n### Into / From\n\nI need to convert one type into another all the freaking time. Often there are\nmultiple ways to do this, depending on how you want to interpret the upstream\ntype. I use `Into` and `From` to canonicalize methods for converting one type\ninto another.\n\n```\n/// T can be converted into Out\nfn Into(T: type, Out: type) type {\n    return struct {\n        into: fn(T) Out,\n    };\n}\n\n/// T can be derived from In\nfn From(T: type, In: type) type {\n    return struct {\n        from: fn(In) T,\n    };\n}\n```\n\n### Index\n\nSometimes, rather than iterating, we need keyed access to a value. The\ncontainer could be a slice or a hash table - we don't care. For this, I use\n`Index`.\n\n```\n/// T forms an index over Key and Value\nfn Index(T: type, Key: type, Value: type) type {\n    return struct {\n        get: fn(T, Key) ?Value,\n    };\n}\n```\n\n### Iterator / Iterable\n\nAnother common case: we have a function that iterates over a type, but we don't\ncare what sort of iterable data structure we're going to be working with. We\nusually need two traits: one for the `Iterable` collection, and another for the\n`Iterator`, which keeps track of our progress through the collection.\n\n```\n/// T can be used to iterate over Elem elements of type\nfn Iterator(T: type, Elem: type) type {\n    return struct {\n        next: fn(*T) ?Elem,\n    };\n}\n\n/// T can return an Iter iterator over Elem elements\nfn Iterable(T: Type, Elem: type, Iter: type) type {\n    return struct {\n        iter: fn(T) IterPair,\n        pub const IterPair = struct {impl: Iter, trait: Iterator(Iter, Elem)};\n    };\n}\n```\n\nYou'll notice that this is a little awkward, because we don't usually care what\nthe iterator implementation is. It's probably just a usize index over an array\nor something. It would be nice if we could just define `Iterable(T: type, Elem: type)`. I've chosen to just let it be awkward. It's usually not a big\ndeal.\n\n## Words of Warning\n\nI really don't want you to walk away from this article thinking \"I need to\nconvert all my code to use traits\". Traits should not be thought of as a\ntotalizing pattern. The last thing I want is for Zig code to start looking\nlike Rust or Haskell.\n\nTraits are a sometimes treat. They are useful for functions that need\nabstractions inside of hot loops, where you know that you want monomorphization\nand static dispatch. They can become legible building blocks for some neat\nmetaprogramming tricks. However most of the time I only reach for them in a\npull-based manner: I have a function where it would be useful to consume a\ntrait, because it's undecided on some implementation detail.\n\nUsed improperly, traits can explode the size of your binary and actually make\nit slower. Each function parameterized on comptime parameters gets its own\ntemplated assembly, and if you nest these a few layers deep, it's easy to end\nup with a combinatorial explosion. This can thrash your CPU cache, and\ndepending on the situation, easily be outperformed by dynamic dispatch and\nvtables.\n\n## Words of Encouragement\n\nTraits are ultimately handy and very powerful. I wish I saw more codebases\nthat made use of them, especially for abstraction inside of hot loops. I like\nthat they are a design pattern more than a language feature, and this gives you\na lot of explicit control over when and how you use them. I was looking for a\nsolution like this for a long time, and I'm glad I found it.\n\nI'm sure that other Zig users have stumbled onto this trick, but I have yet to\nsee it in the wild. I think that part of the reason is that it doesn't really\nlend itself to a library. It's more of a convention that you can choose to\nuse or ignore.\n\nMy hope is that for someone reading, traits are the right tool for the job.\n\n1. [Zig Interfaces](https://www.openmymind.net/Zig-Interfaces/) by Karl Seguin [↩](https://www.axelmagn.com/blog/202610-zig-traits/#fr-seguin2023-1)\n2. figuratively [↩](https://www.axelmagn.com/blog/202610-zig-traits/#fr-figuratively-1)\n3. [Zig Interface Revisited](https://williamw520.github.io/2025/07/13/zig-interface-revisited.html)\n   by William Wong [↩](https://www.axelmagn.com/blog/202610-zig-traits/#fr-wong2025-1)\n4. [Inside Zig's new Writer](https://joegm.github.io/blog/inside-zigs-new-writer-interface/)\n   by Joe McKay [↩](https://www.axelmagn.com/blog/202610-zig-traits/#fr-mckay2025-1)\n5. [Interfaces in Zig](https://www.bradcypert.com/interfaces-in-zig/) by Brad\n   Cypert [↩](https://www.axelmagn.com/blog/202610-zig-traits/#fr-cypert2026-1)\n6. [A Practical Guide to Applying Data-Oriented Design](https://vimeo.com/649009599)\n   by Andrew Kelley [↩](https://www.axelmagn.com/blog/202610-zig-traits/#fr-kelley2021-1)\n7. [Zig Comptime is Bonkers Good](https://www.scottredig.com/blog/bonkers_comptime/)\n   by Scott Redit [↩](https://www.axelmagn.com/blog/202610-zig-traits/#fr-redig2025-1)\n8. \"Traits\" are directly inspired from OCaml modules, but the\n   term \"module\" already refers to something else in Zig. [↩](https://www.axelmagn.com/blog/202610-zig-traits/#fr-ocaml-inspired-1)\n9. ![Feels Good](https://www.axelmagn.com/blog/202610-zig-traits/so_good.png) [↩](https://www.axelmagn.com/blog/202610-zig-traits/#fr-so-good-1)\n10. This is a pretty simplistic definition of a numeric\n    trait. In practice, you would probably want to look into\n    [std::ops](https://doc.rust-lang.org/std/ops/index.html) from Rust or\n    [Num](https://hackage.haskell.org/package/base-4.17.0.0/docs/GHC-Num.html#t:Num)\n    from Haskell. [↩](https://www.axelmagn.com/blog/202610-zig-traits/#fr-num-is-simplified-1)\n","body_html":"<h1 id=\"we-have-traits-at-home\">We have Traits at Home</h1>\n<p>2026-10-03\n·\n<a href=\"https://www.axelmagn.com/tags/zig/\" rel=\"nofollow ugc noopener\">zig</a>,\n<a href=\"https://www.axelmagn.com/tags/traits/\" rel=\"nofollow ugc noopener\">traits</a>,\n<a href=\"https://www.axelmagn.com/tags/polymorphism/\" rel=\"nofollow ugc noopener\">polymorphism</a>,\n<a href=\"https://www.axelmagn.com/tags/abstraction/\" rel=\"nofollow ugc noopener\">abstraction</a></p>\n<p>I have had more time recently to write code in Zig, and it has been good for my\nsoul. I wanted to share some insights I have arrived at regarding abstractions\nin Zig, including some techniques that I have found useful but not seen employed\nmore broadly.</p>\n<p>Considerable<a href=\"https://www.axelmagn.com/blog/202610-zig-traits/#fn-seguin2023\" rel=\"nofollow ugc noopener\">[1]</a> ink<a href=\"https://www.axelmagn.com/blog/202610-zig-traits/#fn-figuratively\" rel=\"nofollow ugc noopener\">[2]</a> has<a href=\"https://www.axelmagn.com/blog/202610-zig-traits/#fn-wong2025\" rel=\"nofollow ugc noopener\">[3]</a> been<a href=\"https://www.axelmagn.com/blog/202610-zig-traits/#fn-mckay2025\" rel=\"nofollow ugc noopener\">[4]</a>\nspilled<a href=\"https://www.axelmagn.com/blog/202610-zig-traits/#fn-cypert2026\" rel=\"nofollow ugc noopener\">[5]</a> on Zig Interfaces, and justifiably so. While not a language\nfeature, interfaces are the de-facto way of implementing runtime polymorphism in\nZig. Yet<a href=\"https://www.axelmagn.com/blog/202610-zig-traits/#fn-kelley2021\" rel=\"nofollow ugc noopener\">[6]</a> more<a href=\"https://www.axelmagn.com/blog/202610-zig-traits/#fn-redig2025\" rel=\"nofollow ugc noopener\">[7]</a> has been said about zig&#39;s comptime, and\nhow it may be leveraged to avoid the performance woes of dynamic dispatch and\nwrite slick, data-oriented code.</p>\n<p>To put it briefly, polymorphism in Zig is conventionally obtained in one of two\nways:</p>\n<ol start=\"2\"><li><p><strong>Dynamic Interfaces</strong>: Use opaque pointers, virtual tables, and dynamic</p><p> dispatch to achieve runtime polymorphism.</p></li><li><p><strong>Comptime Duck Typing</strong>: Use type reflection, switch statements, and</p><p> monomorphism at compile time to achieve static polymorhpism.</p></li></ol>\n<p>This state of affairs has proven to be quite effective so far, but it is not\nwithout its tradeoffs.</p>\n<p>Interfaces offer the sort of type contracts that we are used to from other\nlanguages. A function that accepts an interface has a guarantee of its method\nsignatures, but not its implementation. However because interfaces are, by\ndesign, modular at runtime, they also introduce a hard barrier for the compiler\nand thus a performance overhead. There is enough indirection that it often\ncannot effectively apply the optimizations that it would to monomorphic code.</p>\n<p>For cases of performance critical code, Zig code typically relies on comptime\nduck typing. A function argument is either <code>anytype</code> or parameterized by a type\nargument <code>T</code>, and the function implementation will either perform compile-time\nreflection or make assumptions about the type&#39;s methods that can be checked at\ncompile-time. Implementations can then be monomorphised, similar to C++\ntemplating or C macros. This is slightly more safe than runtime duck typing,\nbecause any interface violations will be caught at compile time rather than\nruntime, but the caller and callee must still be careful to document and follow\nthe contract of what types may be passed.</p>\n<p>If you are coming from another strongly typed language, you would be forgiven\nfor finding this arrangement rather simplistic. This is perhaps by design\n-- Zig aims to be simple. However if you have grown accustomed to Rust&#39;s traits\nor Haskell typeclasses, there appears to be no ready equivalent in Zig.</p>\n<p>I will argue that, with a little adaptation, it is perfectly possible to express\nsimilar abstractions over types in Zig userland. I would propose a new\n<strong>convention</strong> for defining compile-time type contracts as <strong>Traits</strong> in Zig. It\ndoesn&#39;t require any libraries or machinery - in fact if fits remarkably well\ninto vanilla Zig. It is inspired by the functional programming world in general,\nand OCaml in particular.</p>\n<blockquote><p><strong>Definition:</strong> a Trait is a compile-time struct which contains related\nfunctions over a given type. <a href=\"https://www.axelmagn.com/blog/202610-zig-traits/#fn-ocaml-inspired\" rel=\"nofollow ugc noopener\">[8]</a></p></blockquote>\n<p>So as not to bury the lead, let me begin with a brief example of how we might\ndefine a trait for folding (reducing) on a type.</p>\n<pre><code>/// T forms a Reducer under an empty value and a combine operation\nfn Reducer(T: type) type {\n    return struct {\n        empty: T,\n        combine: fn (T, T) T,\n\n        fn reduce(self: @This(), xs: []const T) T {\n            var accum = self.empty;\n            for (xs) |x| accum = self.combine(accum, x);\n            return accum;\n        }\n    };\n}\n\n/// i32 forms a sum reducer under 0 and addition\nconst sum_trait = impl(Reducer(i32), struct {\n    pub const empty = 0;\n    pub fn combine(x: T, y: T) T { return x + y; }\n});\n\n/// i32 forms a product reducer under 1 and multiplication\nconst product_trait = impl(Reducer(i32), struct {\n    pub const empty = 1;\n    pub fn combine(x: T, y: T) T { return x * y; }\n});\n\nexport fn run(buf: [*c]i32, n: usize) i32 {\n    const sum = sum_trait.reduce(buf[0..n]);\n    const product = product_trait.reduce(buf[0..n]);\n    return sum + product;\n}</code></pre>\n<p>Ugh. Isn&#39;t that just buttery smooth?<a href=\"https://www.axelmagn.com/blog/202610-zig-traits/#fn-so-good\" rel=\"nofollow ugc noopener\">[9]</a> The contract is clear, LSP\nautocompletion picks it up, the compiler even knows to inline it and unroll the\nloop.</p>\n<p>Traits are just bags of functions and types that tell the receiving function how\nto execute subtasks that it&#39;s not responsible for defining. They behave a lot\nlike vtables, except that they are evaluated at comptime rather than runtime.</p>\n<h2 id=\"conventional-polymorphism-vs-traits\">Conventional Polymorphism vs Traits</h2>\n<p>To justify traits, let&#39;s take a tour through Zig polymorphism with a common\nmotivating example: arithmetic operations. I&#39;ve seen a few complaints over the\nyears about how awkward is it to do math in Zig over user-defined types. In its\npursuit of explicit control flow, Zig has eschewed operator overloading, and\nthis can be jarring if you&#39;re coming from other typed languages.</p>\n<p>Let&#39;s imagine we have some user-defined types like <code>Rational</code> and <code>Complex</code>, and\nwe want to define their basic arithmentic operations. We would probably end up\nwith something like the following.</p>\n<pre><code>const Rational = struct {\n    numerator: i32,\n    denominator: i32,\n\n    fn add(lhs: Rational, rhs: Rational) Rational {...}\n    fn sub(lhs: Rational, rhs: Rational) Rational {...}\n    fn mul(lhs: Rational, rhs: Rational) Rational {...}\n    fn div(lhs: Rational, rhs: Rational) Rational {...}\n};\n\nconst Complex = struct {\n    real: f32,\n    imaginary: f32,\n\n    fn add(lhs: Complex, rhs: Complex) Complex {...}\n    fn sub(lhs: Complex, rhs: Complex) Complex {...}\n    fn mul(lhs: Complex, rhs: Complex) Complex {...}\n    fn div(lhs: Complex, rhs: Complex) Complex {...}\n};</code></pre>\n<p>For the sake of having something to do with them, let&#39;s say that we want to\ncompute the\n<a href=\"https://en.wikipedia.org/wiki/M%C3%B6bius_transformation\" rel=\"nofollow ugc noopener\">Möbius transformation</a>:\n<code>(a * x + b) / (c * x + d)</code>, which is useful for complex numbers as well as\nrationals. To keep things simple, we&#39;ll assume that all arguments to the\nfunction are of the same type.</p>\n<h3 id=\"comptime-duck-typing\">Comptime Duck Typing</h3>\n<p>If we were to implement the Mobius transformation naively, we could just use\nbuilt-in operators.</p>\n<pre><code>fn mobius(T: type, x: T, a: T, b: T, c: T, d: T) T {\n    return (a * x + b) / (c * x + d);\n}</code></pre>\n<p>This would work fine, as long as the inputs were all floats of the same type.\nDivision would break for integers, and of course it wouldn&#39;t work with our\nuser-defined types, but it&#39;s a starting point.</p>\n<p>We could of course notice that both of our user-defined types have arithmetic\nfunctions with matching names and similar signatures, which would lend\nthemselves to duck typing as well.</p>\n<pre><code>fn mobius(T: type, x: T, a: T, b: T, c: T, d: T) T {\n    const numerator = a.mul(x).add(b);\n    const denominator = c.mul(x).add(d);\n    return numerator.div(denominator);\n}</code></pre>\n<p>This will work for our user-defined types, but obviously not with Zig&#39;s\nprimitive types. We would have to wrap those in our own user-defined types with\ntheir own functions. More troublingly, this sort of ties us to a sort of\nobject-oriented pattern: the de-facto way to obey this abstraction contract is\nto define methods on the struct.</p>\n<p>The good news is that it&#39;s all monomorphised at compile time, allowing the\ncompiler to simplify and inline these function calls.</p>\n<h3 id=\"interfaces-vtables\">Interfaces &amp; VTables</h3>\n<p>If we wanted this to work dynamically at runtime, we could perhaps declare a\n<code>Num</code> interface that uses a VTable and opaque pointer to dynamically dispatch\nthese math operations.</p>\n<pre><code>const Num = struct {\n    value: [32]u8,\n    vtable: *const VTable,\n\n    const VTable = struct {\n        add: *const fn([*]u8, [*]u8) Num,\n        sub: *const fn([*]u8, [*]u8) Num,\n        mul: *const fn([*]u8, [*]u8) Num,\n        div: *const fn([*]u8, [*]u8) Num,\n    };\n\n    fn add(lhs: Num, rhs: Num) Num { ... }\n    fn sub(lhs: Num, rhs: Num) Num { ... }\n    fn mul(lhs: Num, rhs: Num) Num { ... }\n    fn div(lhs: Num, rhs: Num) Num { ... }\n\n    fn mobius(x: Num, a: Num, b: Num, c: Num, d: Num) {\n        const numerator = a.mul(x).add(b);\n        const denominator = c.mul(x).add(d);\n        return numerator.div(denominator);\n    }\n}</code></pre>\n<p>We would then need to wire up <code>Complex</code> and <code>Rational</code> to expose\n<code>fn num(self: *Self) Num</code> (or some such) that wraps our arithmentic functions,\nwhich is a little too tedious to reproduce here, but it would work fine on the\nhappy path. You would of course need to be a bit careful: there&#39;s nothing\nstopping you from mixing complex and rational numbers in your function calls,\nand this would lead to undefined behavior. You could probably add some saftey\nchecks with a bit of overengineering, but we won&#39;t get into that here.</p>\n<p>Not only is this clunkier than comptime duck typing, it will also be slower. We\nare effectively demanding dynamic dispatch whenever we want to do a math\noperation, which will trip up the compiler and have a significant performance\nimpact if we&#39;re doing a lot of math.</p>\n<h3 id=\"traits\">Traits</h3>\n<p>When we define the mobius function, we really want to say &quot;I don&#39;t know what\ntype I&#39;m dealing with, but it needs to have arithmetic operations.&quot; In this sort\nof case, I think it makes the most sense to just pass the functions it needs to\nuse at compile-time. This would remove any ambiguity, at the cost of being\nrather explicit.</p>\n<pre><code>fn mobius(\n    comptime T: type,\n    comptime add: fn (T, T) T,\n    comptime mul: fn (T, T) T,\n    comptime div: fn (T, T) T,\n    x: T,\n    a: T,\n    b: T,\n    c: T,\n    d: T,\n) T {\n    const numerator = add(mul(a, x), b);\n    const denominator = add(mul(c, x), d);\n    return div(numerator, denominator);\n}</code></pre>\n<p>This is effectively compile-time dependency injection at a really fine-grained\nlevel. The signature is rather verbose, but there&#39;s actually a lot less\nboilerplate to write for <code>Complex</code> or <code>Rational</code> compared to runtime interfaces.</p>\n<pre><code>mobius(Complex, Complex.add, Complex.mul, Complex.div, x, a, b, c, d);\nmobius(Real, Real.add, Real.mul, Real.div, x, a, b, c, d);</code></pre>\n<p>From here, it&#39;s not a huge leap to traits. We&#39;re effectively just cleaning up\nthe function signature by gathering together the arithmetic functions into one\nstruct.<a href=\"https://www.axelmagn.com/blog/202610-zig-traits/#fn-num-is-simplified\" rel=\"nofollow ugc noopener\">[10]</a></p>\n<pre><code>fn Num(T: type) type {\n    return struct {\n        add: fn (T, T) T,\n        sub: fn (T, T) T,\n        mul: fn (T, T) T,\n        div: fn (T, T) T,\n    };\n}\n\nfn mobius(T: type, num: Num(T), x: T, a: T, b: T, c: T, d: T) T {\n    const numerator = num.add(num.mul(a, x), b);\n    const denominator = num.add(num.mul(c, x), d);\n    return num.div(numerator, denominator);\n}</code></pre>\n<p>From here, I usually just declare the trait implementation as part of my type.</p>\n<pre><code>const Complex = struct {\n    // fields...\n    const num = Num(Complex) { .add = add, .sub = sub, .mul = mul, .div = div };\n    // functions (add, sub, mul, div...)\n};\n\nmobius(Complex, Complex.num, x, a, b, c, d);</code></pre>\n<p>What I really like about this approach is how flexible it is, while remaining\nvery clear about the contract between the function and type. For example,\nimagine that we wanted to extend our definition of <code>Num</code> to the primitive types\nlike <code>i32</code> and <code>f32</code>. There are a bunch of ways we could do this, but I would\nprobably just define a function that produces something appropriate for integer\nand float types.</p>\n<pre><code>fn builtinNum(T: type) Num(T) {\n    const inner = struct {\n        fn add(lhs: T, rhs: T) T { return lhs + rhs; }\n        fn sub(lhs: T, rhs: T) T { return lhs - rhs; }\n        fn mul(lhs: T, rhs: T) T { return lhs * rhs; }\n    };\n    const div = switch (@typeInfo(T)) {\n        .int, .comptime_int =&gt; struct {\n            fn div(lhs: T, rhs: T) T { return @divTrunc(lhs, rhs); }\n        }.div,\n        .float, .comptime_float =&gt; struct {\n            fn div(lhs: T, rhs: T) T { return lhs / rhs; }\n        }.div,\n        else =&gt; @compileError(&quot;unsupported type: &quot; ++ @typeName(T)),\n    };\n    return .{ .add = inner.add, .sub = inner.sub, .mul = inner.mul, .div = div };\n}\n\nmobius(f32, builtinNum(f32), x, a, b, c, d);</code></pre>\n<p>Traits are also transparent to the compiler, which will happily monomorphize and\ninline these functions. I prefer this over runtime polymorphism most of the\ntime, especially in hot loops or for library functions that are unopinionated on\ndata layout.</p>\n<h3 id=\"limitations\">Limitations</h3>\n<p>You&#39;ll notice that traits have to be explicitly conditioned on a known type.\nUnlike interfaces, there&#39;s no way to say &quot;give me a number, I don&#39;t care what\nkind.&quot; For some applications, it would be nice if we could have an associated\ntype.</p>\n<pre><code>fn Iter(Elem: type) type {\n    return struct {\n        T: type,\n        next: fn (T) ?Elem,\n    };\n}</code></pre>\n<p>Unfortunately, the above isn&#39;t valid Zig, and it&#39;s unclear how that would even\nwork at a language level. Instead, traits insist that the types they are\noperating on are known in advance.</p>\n<pre><code>fn Iter(T: type, Elem: type) type {\n    return struct {\n        next: fn (T) ?Elem,\n    };\n}</code></pre>\n<p>I have not yet found an ergonomic way around this. Traits do not play very\nwell with type erasure.</p>\n<h2 id=\"trait-helpers\">Trait Helpers</h2>\n<p>Before proceeding, I want to introduce some small helper functions that have\nstreamlined how I work with traits.</p>\n<h3 id=\"impl\">impl</h3>\n<p>If you adopt traits as a programming pattern and convention, you&#39;ll quickly end\nup defining functions and passing them around. You will have already noticed\nabove that I define a lot of throwaway inner struct types, just so that I can\npull functions out of them. This has led to the following convenience function\nfor declaring trait implementations, which removes some of the boilerplate.</p>\n<pre><code>pub fn impl(comptime Trait: type, comptime Impl: type) Trait {\n    comptime {\n        var result: Trait = undefined;\n        for (@typeInfo(Trait).@&quot;struct&quot;.field_names) |field_name| {\n            @field(result, field_name) = @field(Impl, field_name);\n        }\n        return result;\n    }\n}</code></pre>\n<p>I use this all over the place to derive trait implementations from anonymous\nstructs. For example, we could use it to rewrite <code>builtinNum</code> to be a little\nmore concise and functional. Note that the declarations need to be public for\nthe type reflection to work.</p>\n<pre><code>fn builtinNum(T: type) Num(T) {\n    return impl(Num(T), struct {\n        pub fn add(lhs: T, rhs: T) T { return lhs + rhs; }\n        pub fn sub(lhs: T, rhs: T) T { return lhs - rhs; }\n        pub fn mul(lhs: T, rhs: T) T { return lhs * rhs; }\n\n        pub const div = switch (@typeInfo(T)) {\n            .int, .comptime_int =&gt; struct {\n                fn div(lhs: T, rhs: T) T { return @divTrunc(lhs, rhs); }\n            }.div,\n            .float, .comptime_float =&gt; struct {\n                fn div(lhs: T, rhs: T) T { return lhs / rhs; }\n            }.div,\n            else =&gt; @compileError(&quot;unsupported type: &quot; ++ @typeName(T)),\n        };\n    });\n}</code></pre>\n<h3 id=\"gettrait\">getTrait</h3>\n<p>Most of the time, I don&#39;t mind passing trait implementations around explicitly.\nI&#39;ve gotten into the habit, and just don&#39;t find it to be that big of a deal.\nTrait functors make it pretty easy to reduce the boilerplate, and I like having\nexplicit control over how things are implemented.</p>\n<p>However when I&#39;m dealing with my own structs, 99% of the time the trait\nimplementation I&#39;m looking for is going to be declared as a const on the struct\nitself. I use the function below to to easily retrieve or construct the most\ncommon cases.</p>\n<pre><code>fn getTrait(Trait: type, Impl: type) Trait {\n    comptime {\n        // try to find the trait declared explicitly as a member\n        for (@typeInfo(Impl).@&quot;struct&quot;.decl_names) |decl_name| {\n            if (@TypeOf(@field(Impl, decl_name)) == Trait) {\n                return @field(Impl, decl_name);\n            }\n        }\n        // otherwise throw an error\n        @compileError(&quot;trait &quot; ++ @typeName(Trait) ++ &quot; not found on &quot; ++ @typeName(Impl));\n    }\n}</code></pre>\n<p>This is basically equivalent to saying &quot;eh, it&#39;s duck typed. figure it out&quot;.\nJust because we <em>are</em> explicit, doesn&#39;t mean that we <em>can&#39;t</em> be lazy. If we want\nto take this full circle back to duck typing, we can even define trait functors\nthat rely on <code>getTrait</code> to extract traits from the types they pass.</p>\n<pre><code>fn duckMobius(T: type) fn(T, T, T, T) T {\n    comptime {\n        const num = getTrait(Num(T), T);\n        return makeMobius(T, num);\n    }\n}</code></pre>\n<h2 id=\"trait-functors\">Trait Functors</h2>\n<p>With some helpers defined, let&#39;s return to the <code>Num</code> example.</p>\n<p>Traits in Zig resemble traits in Rust, typeclasses in Haskell, or modules in\nOcaml. In Rust or Haskell, they would be resolved from a global registry managed\nby the compiler. This can feel very convenient, but it violates Zig&#39;s principle\nof explicit control flow. Rust and Haskell can require a fair bit of hunting for\nwhere a trait is defined, especially when something&#39;s wrong and the LSP isn&#39;t\nresolving it properly. It can also be a contributor to long compile times.\nResolving a generic trait in these languages is often equivalent to evaluating a\nproof, and that can take time.</p>\n<p>Ocaml modules, however, work quite a bit like Zig traits: modules are\nfirst-class objects, and they are passed as arguments to the functions that\nconsume them. There is no inference of what modules apply to a type - you locate\nthem and pass them explicitly. If we were to define <code>Num</code> as a module, it might\nlook something like the following.</p>\n<pre><code>(* signature *)\nmodule type NUM = sig\n  type t\n  val add : t -&gt; t -&gt; t\n  val sub : t -&gt; t -&gt; t\n  val mul : t -&gt; t -&gt; t\n  val div : t -&gt; t -&gt; t\nend\n\n(* Implementation on float *)\nmodule Float : NUM with type t = float = struct\n  type t = float\n  let add = ( +. )\n  let sub = ( -. )\n  let mul = ( *. )\n  let div = ( /. )\nend\n\n(* function *)\nlet mobius (type a) (module N : NUM with type t = a) x a b c d =\n  let numerator = N.add (N.mul a x) b in\n  let denominator = N.add (N.mul c x) d in\n  N.div numerator denominator\n\n(* Usage *)\nlet res = mobius (module Float) x a b c d)</code></pre>\n<p>Because modules in Ocaml are first-class values, they can actually take it a\nstep further. Rather than defining <code>mobius</code> as written above, it is common in\nOcaml to make use of &quot;module functors&quot;, which are simply functions that take\nmodules as arguments and return new modules. If we wanted to define <code>mobius</code> in\nterms of a given numeric type, we might instead write it:</p>\n<pre><code>(* MakeMobius receives a NUM module and returns a new module with mobius function *)\nmodule MakeMobius (N : NUM) = struct\n  let mobius x a b c d =\n    let numerator = N.add (N.mul a x) b in\n    let denominator = N.add (N.mul c x) d in\n    N.div numerator denominator\nend\n\n(* usage *)\nmodule FloatMobius = MakeMobius (Float)\nlet res = FloatMobius.mobius x a b c d</code></pre>\n<p>This is effectively currying: we are applying the module arguments now, and the\nvalue arguments later. However it derives a mobius function that doesn&#39;t need to\nbe taking a bunch of type arguments all the time, and that module can in turn be\nused in later constructions. This translates straightforwardly to Zig. If we\nwanted to adhere strictly to the traits pattern, it might look like the\nfollowing.</p>\n<pre><code>fn Mobius(T: type) type {\n    return struct {\n        mobius: fn(T, T, T, T, T) T,\n    };\n}\nfn makeMobius(T: type, num: Num(T)) Mobius(T) {\n    return impl(Mobius(T), struct {\n        fn mobius(x: T, a: T, b: T, c: T, d: T) T {\n            const numerator = num.add(num.mul(a, x), b);\n            const denominator = num.add(num.mul(c, x), d);\n            return num.div(numerator, denominator);\n        }\n    });\n}\nconst complex_mobius = MakeMobius(Complex, Complex.num);\ncomplex_mobius.mobius(x, a, b, c, d);</code></pre>\n<p>This works fine, but I think it&#39;s even simpler to move the functor into\n<code>Mobius(T)</code>:</p>\n<pre><code>fn Mobius(T: type) type {\n    return struct {\n        mobius: fn(T, T, T, T, T) T,\n\n        fn make(num: Num(T)) Mobius(T) {\n            return impl(Mobius(T), struct {\n                fn mobius(x: T, a: T, b: T, c: T, d: T) T {\n                    const numerator = num.add(num.mul(a, x), b);\n                    const denominator = num.add(num.mul(c, x), d);\n                    return num.div(numerator, denominator);\n                }\n            });\n        }\n    };\n}\nconst complex_mobius = Mobius(Complex).make(Complex.num);\ncomplex_mobius.mobius(x, a, b, c, d);</code></pre>\n<p>However for single functions like this, unless I knew it would need to be\nconsumed by another function as a trait, I would probably be tempted to just\nimplement it as a type-curried function rather than a full-blown trait:</p>\n<pre><code>fn makeMobius(T: type, num: Num(T)) fn(T, T, T, T) T {\n    return struct {\n        fn mobius(x: T, a: T, b: T, c: T, d: T) T {\n            const numerator = num.add(num.mul(a, x), b);\n            const denominator = num.add(num.mul(c, x), d);\n            return num.div(numerator, denominator);\n        }\n    }.mobius;\n}\n\nconst complexMobius = makeMobius(Complex, Complex.num);\ncomplexMobius(x, a, b, c, d);</code></pre>\n<p>The versatility of Zig&#39;s comptime makes all of these more or less equivalent to\nthe compiler. It&#39;s just a stylistic choice, depending on what the situation\ncalls for.</p>\n<p>Also, notice that technically we have already used trait functors when we\ndefined <code>builtinNum</code>. It&#39;s not a deep, technical concept. It&#39;s just a function\nthat constructs a trait implementation for you.</p>\n<h2 id=\"useful-traits\">Useful Traits</h2>\n<p>Having thoroughly introduced traits, let&#39;s talk about what we can do with them.</p>\n<p>In this section are a few traits that I find myself reaching for with enough\nregularity that I keep a file around containing them. All of these are fairly\nhumdrum (but wildly applicable) abstractions that I lifted wholecloth from\nother languages.</p>\n<h3 id=\"into-from\">Into / From</h3>\n<p>I need to convert one type into another all the freaking time. Often there are\nmultiple ways to do this, depending on how you want to interpret the upstream\ntype. I use <code>Into</code> and <code>From</code> to canonicalize methods for converting one type\ninto another.</p>\n<pre><code>/// T can be converted into Out\nfn Into(T: type, Out: type) type {\n    return struct {\n        into: fn(T) Out,\n    };\n}\n\n/// T can be derived from In\nfn From(T: type, In: type) type {\n    return struct {\n        from: fn(In) T,\n    };\n}</code></pre>\n<h3 id=\"index\">Index</h3>\n<p>Sometimes, rather than iterating, we need keyed access to a value. The\ncontainer could be a slice or a hash table - we don&#39;t care. For this, I use\n<code>Index</code>.</p>\n<pre><code>/// T forms an index over Key and Value\nfn Index(T: type, Key: type, Value: type) type {\n    return struct {\n        get: fn(T, Key) ?Value,\n    };\n}</code></pre>\n<h3 id=\"iterator-iterable\">Iterator / Iterable</h3>\n<p>Another common case: we have a function that iterates over a type, but we don&#39;t\ncare what sort of iterable data structure we&#39;re going to be working with. We\nusually need two traits: one for the <code>Iterable</code> collection, and another for the\n<code>Iterator</code>, which keeps track of our progress through the collection.</p>\n<pre><code>/// T can be used to iterate over Elem elements of type\nfn Iterator(T: type, Elem: type) type {\n    return struct {\n        next: fn(*T) ?Elem,\n    };\n}\n\n/// T can return an Iter iterator over Elem elements\nfn Iterable(T: Type, Elem: type, Iter: type) type {\n    return struct {\n        iter: fn(T) IterPair,\n        pub const IterPair = struct {impl: Iter, trait: Iterator(Iter, Elem)};\n    };\n}</code></pre>\n<p>You&#39;ll notice that this is a little awkward, because we don&#39;t usually care what\nthe iterator implementation is. It&#39;s probably just a usize index over an array\nor something. It would be nice if we could just define <code>Iterable(T: type, Elem: type)</code>. I&#39;ve chosen to just let it be awkward. It&#39;s usually not a big\ndeal.</p>\n<h2 id=\"words-of-warning\">Words of Warning</h2>\n<p>I really don&#39;t want you to walk away from this article thinking &quot;I need to\nconvert all my code to use traits&quot;. Traits should not be thought of as a\ntotalizing pattern. The last thing I want is for Zig code to start looking\nlike Rust or Haskell.</p>\n<p>Traits are a sometimes treat. They are useful for functions that need\nabstractions inside of hot loops, where you know that you want monomorphization\nand static dispatch. They can become legible building blocks for some neat\nmetaprogramming tricks. However most of the time I only reach for them in a\npull-based manner: I have a function where it would be useful to consume a\ntrait, because it&#39;s undecided on some implementation detail.</p>\n<p>Used improperly, traits can explode the size of your binary and actually make\nit slower. Each function parameterized on comptime parameters gets its own\ntemplated assembly, and if you nest these a few layers deep, it&#39;s easy to end\nup with a combinatorial explosion. This can thrash your CPU cache, and\ndepending on the situation, easily be outperformed by dynamic dispatch and\nvtables.</p>\n<h2 id=\"words-of-encouragement\">Words of Encouragement</h2>\n<p>Traits are ultimately handy and very powerful. I wish I saw more codebases\nthat made use of them, especially for abstraction inside of hot loops. I like\nthat they are a design pattern more than a language feature, and this gives you\na lot of explicit control over when and how you use them. I was looking for a\nsolution like this for a long time, and I&#39;m glad I found it.</p>\n<p>I&#39;m sure that other Zig users have stumbled onto this trick, but I have yet to\nsee it in the wild. I think that part of the reason is that it doesn&#39;t really\nlend itself to a library. It&#39;s more of a convention that you can choose to\nuse or ignore.</p>\n<p>My hope is that for someone reading, traits are the right tool for the job.</p>\n<ol><li><a href=\"https://www.openmymind.net/Zig-Interfaces/\" rel=\"nofollow ugc noopener\">Zig Interfaces</a> by Karl Seguin <a href=\"https://www.axelmagn.com/blog/202610-zig-traits/#fr-seguin2023-1\" rel=\"nofollow ugc noopener\">↩</a></li><li>figuratively <a href=\"https://www.axelmagn.com/blog/202610-zig-traits/#fr-figuratively-1\" rel=\"nofollow ugc noopener\">↩</a></li><li><p><a href=\"https://williamw520.github.io/2025/07/13/zig-interface-revisited.html\" rel=\"nofollow ugc noopener\">Zig Interface Revisited</a></p><p> by William Wong <a href=\"https://www.axelmagn.com/blog/202610-zig-traits/#fr-wong2025-1\" rel=\"nofollow ugc noopener\">↩</a></p></li><li><p><a href=\"https://joegm.github.io/blog/inside-zigs-new-writer-interface/\" rel=\"nofollow ugc noopener\">Inside Zig&#39;s new Writer</a></p><p> by Joe McKay <a href=\"https://www.axelmagn.com/blog/202610-zig-traits/#fr-mckay2025-1\" rel=\"nofollow ugc noopener\">↩</a></p></li><li><p><a href=\"https://www.bradcypert.com/interfaces-in-zig/\" rel=\"nofollow ugc noopener\">Interfaces in Zig</a> by Brad</p><p> Cypert <a href=\"https://www.axelmagn.com/blog/202610-zig-traits/#fr-cypert2026-1\" rel=\"nofollow ugc noopener\">↩</a></p></li><li><p><a href=\"https://vimeo.com/649009599\" rel=\"nofollow ugc noopener\">A Practical Guide to Applying Data-Oriented Design</a></p><p> by Andrew Kelley <a href=\"https://www.axelmagn.com/blog/202610-zig-traits/#fr-kelley2021-1\" rel=\"nofollow ugc noopener\">↩</a></p></li><li><p><a href=\"https://www.scottredig.com/blog/bonkers_comptime/\" rel=\"nofollow ugc noopener\">Zig Comptime is Bonkers Good</a></p><p> by Scott Redit <a href=\"https://www.axelmagn.com/blog/202610-zig-traits/#fr-redig2025-1\" rel=\"nofollow ugc noopener\">↩</a></p></li><li><p>&quot;Traits&quot; are directly inspired from OCaml modules, but the</p><p> term &quot;module&quot; already refers to something else in Zig. <a href=\"https://www.axelmagn.com/blog/202610-zig-traits/#fr-ocaml-inspired-1\" rel=\"nofollow ugc noopener\">↩</a></p></li><li><img src=\"https://www.axelmagn.com/blog/202610-zig-traits/so_good.png\" alt=\"Feels Good\" loading=\"lazy\" decoding=\"async\" referrerpolicy=\"no-referrer\" /> <a href=\"https://www.axelmagn.com/blog/202610-zig-traits/#fr-so-good-1\" rel=\"nofollow ugc noopener\">↩</a></li><li><p>This is a pretty simplistic definition of a numeric</p><p>  trait. In practice, you would probably want to look into\n  <a href=\"https://doc.rust-lang.org/std/ops/index.html\" rel=\"nofollow ugc noopener\">std::ops</a> from Rust or\n  <a href=\"https://hackage.haskell.org/package/base-4.17.0.0/docs/GHC-Num.html#t:Num\" rel=\"nofollow ugc noopener\">Num</a>\n  from Haskell. <a href=\"https://www.axelmagn.com/blog/202610-zig-traits/#fr-num-is-simplified-1\" rel=\"nofollow ugc noopener\">↩</a></p></li></ol>","headings":[{"level":1,"text":"We have Traits at Home","id":"we-have-traits-at-home"},{"level":2,"text":"Conventional Polymorphism vs Traits","id":"conventional-polymorphism-vs-traits"},{"level":3,"text":"Comptime Duck Typing","id":"comptime-duck-typing"},{"level":3,"text":"Interfaces & VTables","id":"interfaces-vtables"},{"level":3,"text":"Traits","id":"traits"},{"level":3,"text":"Limitations","id":"limitations"},{"level":2,"text":"Trait Helpers","id":"trait-helpers"},{"level":3,"text":"impl","id":"impl"},{"level":3,"text":"getTrait","id":"gettrait"},{"level":2,"text":"Trait Functors","id":"trait-functors"},{"level":2,"text":"Useful Traits","id":"useful-traits"},{"level":3,"text":"Into / From","id":"into-from"},{"level":3,"text":"Index","id":"index"},{"level":3,"text":"Iterator / Iterable","id":"iterator-iterable"},{"level":2,"text":"Words of Warning","id":"words-of-warning"},{"level":2,"text":"Words of Encouragement","id":"words-of-encouragement"}]}}