/// 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
?
/// 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.
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;
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.
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
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
:
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.
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.
I meant using std.io.AnyWriter
as a parameter similar to std.mem.Allocator
usage.
But this is a satisfactory answer for me.
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?
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
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.
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!
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.
Though, you can still do this with AnyWriter by implementing only the writeFn
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.