Create a struct which holds a reference to any writer

I’m trying to create a struct which has some methods can write to anything which implements std.io.Writer. I’d like to pass the writer to the struct at initialisation. However, whilst I can pass a generic Writer to a function, I can’t figure out how to store it in a struct. Is it possible?
To illustrate: This works:

const Allocator = std.mem.Allocator;

pub fn main() !void {
    var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
    defer arena.deinit();
    const alloc = &arena.allocator;

    var l = std.ArrayList(u8).init(alloc);
    try takes_writer(l.writer());
    std.debug.print("{s}\n", .{l.items});
}

fn takes_writer(wr: anytype) !void {
    try wr.print("Hello, World\n", .{});
}

But this does not - it fails with “unable to evaluate constant expression”:

const Allocator = std.mem.Allocator;

pub fn main() !void {
    var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
    defer arena.deinit();
    const alloc = &arena.allocator;

    var l = std.ArrayList(u8).init(alloc);
    const ur = UsesWriter{ .writer = l.writer() };
    try ur.say_hello();

    std.debug.print("{s}\n", .{l.items});
}

const UsesWriter = struct {
    writer: anytype,

    fn say_hello(self: *UsesWriter) !void {
        try self.writer.print("Hello, World\n", .{});
    }
};

Any suggestions?

anytype fields in a struct make the struct only usable at comptime. If you want to make the struct generic over the specific Writer type you need to do so the usual way, with a function that builds the generic type and (optionally) a function that creates instances of it by accepting an anytype instance of a writer.

fn UsesWriter(T: type) type {
   return struct {
      writer: T,
      fn say_hello(self: *UsesWriter) !void {
          try self.writer.print("Hello, World\n", .{});
      }
   }
}

fn usesWriter(wr: anytype) UsesWriter(@TypeOf(wr)) {
   return .{ .writer = wr };
}

Sorry I haven’t checked if the code above is completely correct, but it should give you an idea.
You can also see the same pattern being used in the standard library.

2 Likes

Thanks, @kristoff. That makes sense now you explain it - I’m still learning to think in zig. I had to tweak your example slightly to make it work - here’s the complete program:

const std = @import("std");
const Allocator = std.mem.Allocator;

pub fn main() !void {
    var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
    defer arena.deinit();
    const alloc = &arena.allocator;

    var l = std.ArrayList(u8).init(alloc);
    const ur = usesWriter(l.writer());
    try ur.say_hello();
    std.debug.print("{s}\n", .{l.items});
}

fn UsesWriter(comptime T: type) type {
    return struct {
        writer: T,
        const Self = @This();
        fn say_hello(self: Self) !void {
            try self.writer.print("Hello, World\n", .{});
        }
    };
}

fn usesWriter(wr: anytype) UsesWriter(@TypeOf(wr)) {
    return .{ .writer = wr };
}

The main change was that the self parameter in say_hello needed to be changed to the type of the created struct, using @This().