AnyWriter/AnyReader vs anytype

/// Writes the given value to the std.io.Writer stream ...
std.json.stringify( 
    value: anytype, 
    options: StringifyOptions, 
    out_stream: anytype, 
)

What’s the purpose of using anytype instead of std.io.Writer?

std.io.Writer is a function, not a type. Infinitely many types can be produced out of that function.

1 Like

There may be some confusion because in the source code: lib/std/io/Writer.zig is a struct but that is turned into AnyWriter in the io.zig source and GenericWriter which is a function is renamed to std.io.Writer.

(On zig 13.0 btw)

// io.zig line: 368
/// Deprecated; consider switching to `AnyWriter` or use `GenericWriter`
/// to use previous API.
pub const Writer = GenericWriter;
1 Like

Currently (14+), Writer is an alias to GenericWriter. AnyWriter is a type erasure that stores the vtable pointer as field, allowing any writer to disguise under the same type. This carries a performance cost.

1 Like

anytype is replaced by the actual type when stringify is called.
You can use any type that implements whatever stringify calls for out_stream.

anytype can appear only in the function argument list as type.
see: Function Parameter Type Inference

1 Like

Ah I wasn’t aware of this. Rephrasing my question, still why not use AnyWriter instead of anytype for out_stream?

In std/json/stringify.zig/WriteStream, these are the functions called on out_stream:

  • write
  • writeByte
  • print
  • writeAll

These are all covered by the AnyWriter interface.

Because AnyWriter is not an interface. Zig does not have interfaces.

Using anytype allows us to have a type that implements the required functions.

For example, if we want to stringify to a socket stream, we can use std.net.Stream.Writer that is a type created by the GenericWriter type function.

2 Likes

FWIW, I am 0.7 sure that it is intended to be AnyWriter, and is currently anytype simply because AnyWriter is newer than static-dispatched-based reader/write abstraction.

If you reader/writer is, in the end underpinned by read & write syscalls, you almost certainly want them to be dynamicaly dispatched: the performance cost of missing inline is negligible in this case (you are doing a syscall anyway), but the binary size benefits are substantial.

Though, there is this corner-case of writing into an in-memory buffer, where potentially you do want to use static dispatch.

5 Likes

I meant using std.io.AnyWriter as a parameter similar to std.mem.Allocator usage.

But this is a satisfactory answer for me.

1 Like

Does this mean we will have 2 APIs, static and dynamic, to work with? The docs confuse me whether it is deprecating the std.io.Writer alias only:

/// Deprecated; consider switching to `AnyWriter` or use `GenericWriter`
/// to use previous API.
pub const Writer = GenericWriter;

What is the difference between static and dynamic in this scenario?

static means the functions being called are fixed and known at compile time, whereas dynamic dispatch means the functions can be swapped at runtime.

Using writer: anytype means the function gets instantiated once for every concrete type you pass in for that writer, so each instance will be calling functions that are fixed at compile file. Using AnyWriter means you are using a single concrete type that supports any writer, which it does by taking a set of function pointers at runtime that can be swapped in/out for each underlying writer type.

So, AnyWriter is one function instance with dynamic dispatch (dynamic dispatch has more runtime overhead than static dispatch). anytype is multiple function instances with static dispatch. Matklad points out that the dynamic dispatch overhead is negligible in cases where the writer implementation boils down to syscalls.

Another thing to note is that dynamic dispatch may not actually have any extra overhead if for example the optimizer is able to determine that you’re only actually passing a single writer type to the function, it could optimize those dynamic function calls into static ones. So should you use AnyWriter or anytype? One thing anytype has going for it is that it’s non-commital. If the code gets too big because of this, applications are free to cut down on types by only passing AnyWriter to the function, so anytype effectively supports both methods which is nice. But generic types in general are just more complex than concrete types, so is this complexity worth it? :man_shrugging:

EDIT: I suppose I do have an answer to this. In general I try to use the “simpler solution” until the more complex solutions justifies itself, so I suppose that means, use AnyWriter until something justifies using anytype

7 Likes

I think this rounds off to not a problem, however, because the use of AnyReader and AnyWriter means that functions taking an anytype reader/writer will only specialize on the dynamically-dispatched version anyway.

And use of anytype handles this use case as well (although, in particular, ArrayList uses the AnyWriter interface, so we’re back to specializing on one struct).

Using anytype leaves some freedom to, one example, implement a custom struct which provides the subset of member functions which are actually called by parts of the program using a reader or writer. There are other uses for it: I have a sort of experimental library where if a writer: anytype has a certain decl, this is used in some parts of the function, but otherwise it falls back on writeAll. If this is only ever used with type-erased dispatch, then only one specialization of the function will be created.

2 Likes

Sorry for being unclear, I was referring to why static dispatch is needed when writing into an in-memory buffer.

This is new to me though so thank you!

1 Like

I think @matklad’s caveat was only meant to acknowledge that there are exceptions to readers/writers being “underpinned by read & write syscalls”—for example, FixedBufferStream.Writer or ArrayList(u8).Writer. I don’t see a reason you’d need static dispatch for these, but it could potentially be beneficial in these cases where the cost of dynamic dispatch isn’t being overshadowed by the cost of a system call.

2 Likes

Though, you can still do this with AnyWriter by implementing only the writeFn

2 Likes

Okay, that makes sense. I thought it was some completely separate thing.

And this would also share the same specialization as any other instance of AnyWriter. Sometimes that’s what you want.

My main point is that using an anytype leaves some flexibility for implementation, and will still only specialize once if functions are only called with the type-erased generic. The downsides are mainly ergonomic.