Embedding a parametarised struct in another struct

I have a “generic” struct which can be parameterised by passing a writer to the constructor. I am trying to embed an instance of it in another struct, but I can’t figure out the type of the item in the second struct. It’s tricky to explain exactly, but here’s a minimal example. This is not my real application code, which is rather more involved - just a simplistic example to show what I’m trying to do. It doesn’t work at the moment - I get the error:

wrap2.zig:32:26: error: expected type ‘type’, found ‘fn(comptime type) type’
pub const ReportWriter = struct {
^~~~~~

I’ve put a comment by the declaration I’m not sure about. Can someone either help me find the correct declaration, or suggest another way of doing this?

First, here’s the inner struct - this works OK. A writer can be passed to the countWriter() function and it creates a suitable CountWriter “object”.:

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

/// Write text to a file as lines with a leading line number count.
pub fn CountWriter(comptime T: type) type {
    return struct {
        writer: T,
        count: usize,

        const Self = @This();

        pub fn writeLine(self: *Self, text: []const u8) !void {
            self.count += 1;
            try std.fmt.formatIntValue(self.count, "", .{ .width = 4 }, self.writer);
            _ = try self.writer.write(": ");
            _ = try self.writer.write(text);
            _ = try self.writer.write("\n");
        }
    };
}

/// Create and initialise the count writer
pub fn countWriter(wr: anytype) CountWriter(@TypeOf(wr)) {
    return .{
        .writer = wr,
        .count = 0,
    };
}

Now the part that doesn’t work. I want to create a CountWriter in the init() function of this struct and store it in the struct, but as the type depends on the type of the writer I can’t figure out how to specify it…

/// Try to embed the count writer in another struct - DOESN'T WORK
pub const ReportWriter = struct {
    count_writer: CountWriter, // <--- What should this be?

    pub fn init(alloc: Allocator) ReportWriter {
        var dest = std.ArrayList(u8).init(alloc);
        defer dest.deinit();
        var dest_writer = dest.writer();
        var count_writer = countWriter(dest_writer);
        return ReportWriter{ .count_writer = count_writer };
    }
};

Suggestions? Thanks.

Hey @andrewb , good to see you again at the forum!

I don’t know if this is the best or most efficient way to do this, but it works:

// For readability and convenience.
const ByteListWriter = CountWriter(std.ArrayList(u8).Writer);

pub const ReportWriter = struct {
    allocator: Allocator,
    byte_list: std.ArrayList(u8),
    count_writer: ByteListWriter = undefined,

    pub fn init(allocator: Allocator) !*ReportWriter {
        const rw_ptr = try allocator.create(ReportWriter);
        rw_ptr.* = ReportWriter{
            .allocator = allocator,
            .byte_list = std.ArrayList(u8).init(allocator),
        };
        rw_ptr.count_writer = countWriter(rw_ptr.byte_list.writer());

        return rw_ptr;
    }

    pub fn deinit(self: *ReportWriter) void {
        self.byte_list.deinit();
        self.allocator.destroy(self);
    }
};

test "Embedded CountWriter" {
    const allocator = std.testing.allocator;

    var rw_ptr = try ReportWriter.init(allocator);
    defer rw_ptr.deinit();

    try rw_ptr.count_writer.writeLine("foo");
    try std.testing.expectEqual(@as(usize, 1), rw_ptr.count_writer.count);
    try rw_ptr.count_writer.writeLine("bar");
    try std.testing.expectEqual(@as(usize, 2), rw_ptr.count_writer.count);
}

I had to use the allocation because otherwise the ArrayList created in init would be invalid once the function returns, being stack allocated.

Thanks, @dude_the_builder - good to hear from you again also. I’ve been away from Zig for a while (work and other commitments) and am just getting back into it. This is helpful, and also points out some (now obvious) errors in my example code.

It doesn’t quite achieve what I was looking for - I realise now that my minimal example was too minimal. What I really want to do is to pass a filename into ReportWriter.init() and have it open a file and pass the file writer into countWriter. I used an ArrayList writer because it was simple to test but it doesn’t represent my real use case well enough. The problem is that because the file is opened inside ReportWriter.init() its type is not available during the declaration. I suppose I could open the file outside of ReportWriter and then pass the opened file writer in, but it does make the code rather more messy. I’ll give it a go, but any other suggestions are welcome.

I would probably make the containing structure parameterized on that type, too, so that you actually name the contained writer type, and creating+initializing it with a helper newThing() method.

CountWriter is generic, so ReportWriter needs to be generic aswell in order to be able to wrap it.

fn ReportWriter(comptime T: type) type {
   return struct {
      count_writer: CountWriter(T),
      // ...
   };
}

Thanks, all. Making the container structure parameterised doesn’t really achieve what I was aiming for - I wanted to pass a file name to the container structure initialiser and have it responsible for managing the file. However, I think your suggestions have pointed me towards the solution. I think I should be storing dest in CountWriter, not the writer returned by dest.writer() and calling dest.writer() inside CountWriter. I’m not quite there with the working solution yet but I’m close - I’ll post it here when I’m done.

I’ve figured out a solution that works for me, though it’s not perfect. I’ve posted a demo program below. Note that this one writes to an ArrayList(u8) for convenience, but I’ve also implemented a slightly more realistic version which writes to a file - it’s just that the former is slightly easier to write tests for.

My original problem was the type signature of the CountWriter when embedding it into the ReportWriter. It needed to be a CountWriter(something) but I couldn’t figure out what something was - it needed to be the type of a Writer for an ArrayList(u8) which I couldn’t figure out. In the end I fixed it by passing a pointer to the writable object (in this case the ArrayList) and creating the Writer inside CountWriter.

The other problem I encountered was that ReportWriter needs to both own the destination and the CountWriter which uses it. This was a problem because I couldn’t see a way of making one field in a struct, as returned by ReportWriter.init(), point to another field in the same struct. Creating the struct and then returning it in two stages doesn’t work because what’s returned is a copy, and the pointer in the CountWriter ends up pointing to the wrong place. The solution I found was to put the destination on the heap instead of the stack.

Here’s the complete demo program. Suggestions for improvements welcome!

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

/// Write text to a writable as lines with a leading line number
/// count.
pub fn CountWriter(comptime T: type) type {
    return struct {
        writable: T,
        count: usize,

        const Self = @This();

        pub fn writeLine(self: *Self, text: []const u8) !void {
            self.count += 1;
            const writer = self.writable.writer();
            try std.fmt.formatIntValue(self.count, "", .{ .width = 4 }, writer);
            _ = try writer.write(": ");
            _ = try writer.write(text);
            _ = try writer.write("\n");
        }
    };
}

/// Create and initialise the count writer
pub fn countWriter(wr: anytype) CountWriter(@TypeOf(wr)) {
    return .{
        .writable = wr,
        .count = 0,
    };
}

/// Report writer creates and owns the report destination but uses
/// CountWriter to write to it.
pub const ReportWriter = struct {
    alloc: Allocator,
    destp: *std.ArrayList(u8),
    count_writer: CountWriter(*std.ArrayList(u8)),

    const Self = @This();

    pub fn init(alloc: Allocator) !ReportWriter {
        const destp = try alloc.create(std.ArrayList(u8));
        destp.* = std.ArrayList(u8).init(alloc);
        return ReportWriter{
            .alloc = alloc,
            .destp = destp,
            .count_writer = countWriter(destp),
        };
    }

    pub fn writeReport(self: *Self, text: []const u8) !void {
        try self.count_writer.writeLine("- Start -");
        try self.count_writer.writeLine(text);
        try self.count_writer.writeLine("- End -");
    }

    pub fn deinit(self: *Self) void {
        self.destp.deinit();
        self.alloc.destroy(self.destp);
    }
};

// Test code

test "count_writer" {
    const expect = std.testing.expect;
    const eql = std.mem.eql;
    const test_alloc = std.testing.allocator;

    var dest = std.ArrayList(u8).init(test_alloc);
    defer dest.deinit();

    var count_writer = countWriter(&dest);
    try count_writer.writeLine("one");
    try count_writer.writeLine("two");
    try expect(eql(u8, dest.items, "   1: one\n   2: two\n"));
    try dest.resize(0);
}

test "report_writer" {
    const expect = std.testing.expect;
    const eql = std.mem.eql;
    const test_alloc = std.testing.allocator;

    var report_writer = try ReportWriter.init(test_alloc);
    defer report_writer.deinit();
    try report_writer.writeReport("test 1");
    try report_writer.writeReport("test 2");

    try expect(eql(
        u8,
        report_writer.destp.items,
        \\   1: - Start -
        \\   2: test 1
        \\   3: - End -
        \\   4: - Start -
        \\   5: test 2
        \\   6: - End -
        \\
        ,
    ));
}

/// Demo program
pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer {
        const leaked = gpa.deinit();
        if (leaked) std.testing.expect(false) catch @panic("TEST FAIL");
    }
    const alloc = gpa.allocator();

    var rw = try ReportWriter.init(alloc);
    defer rw.deinit();
    try rw.writeReport("test 1");
    try rw.writeReport("test 2");
    std.debug.print("{s}\n", .{rw.destp.items});
}

In CountWriter.writeLine you could do the printing in just one line:

try self.writer.print("{d:4}: {s}\n", .{ self.count, text });

In your tests you can use std.testing.expectEqualStrings .

Thanks, yes, you are right on both counts. Those parts of the code were just thrown together as examples to illustrate the point, but I should have noticed that - especially the print.