I tried to illustrate the problem I have with scenario 2 (deserialize unbounded list of integers.
The problem I have is that it is difficult to express a reserve first API. Its difficult for me to provide an API (to myself) that is not programmer-error prone that allows me to express my assumptions about the size of the input.
For example, what if I know that there will be at most 5 integers to deserialize?
Lets take a look at how I can encode this assumption into my program:
const std = @import("std");
// scenario 2: unbounded list of integers
/// caller must free returned memory
fn deserializeIntsAlloc(allocator: std.mem.Allocator, reader: *std.Io.Reader) ![]u16 {
var list = std.ArrayList(u16).empty;
errdefer list.deinit(allocator);
while (true) {
const bytes_containing_int: []const u8 = reader.takeDelimiterExclusive(' ') catch |err| switch (err) {
error.EndOfStream => break,
error.StreamTooLong => return error.StreamTooLong,
error.ReadFailed => return error.ReadFailed,
};
const num = try std.fmt.parseInt(u16, bytes_containing_int, 10);
try list.append(allocator, num);
}
return try list.toOwnedSlice(allocator);
}
test "alloc four ints" {
const serialized: []const u8 = "1 51 765 65535";
var reader = std.Io.Reader.fixed(serialized);
const deserialized = try deserializeIntsAlloc(std.testing.allocator, &reader);
defer std.testing.allocator.free(deserialized);
const expected: []const u16 = &.{ 1, 51, 765, 65535 };
try std.testing.expectEqualSlices(u16, expected, deserialized);
}
// scenario 2: how can we provide errorless / reserve first API?
// For example, the user knows there are only up to 5 ints ahead of time
// this sucks!
test {
const serialized: []const u8 = "1 51 765 65535";
var reader = std.Io.Reader.fixed(serialized);
// Guessing sizes here! Depends on the efficiency of the allocator!
// Look at how much work it is to do "reserve first", something
// we should encourage!
var stack_memory: [4096]u8 = undefined;
var fba = std.heap.FixedBufferAllocator.init(&stack_memory);
const deserialized = deserializeIntsAlloc(fba.allocator(), &reader) catch |err| switch (err) {
error.OutOfMemory => unreachable,
error.ReadFailed => return error.ReadFailed,
error.StreamTooLong => return error.StreamTooLong,
error.Overflow => return error.Overflow,
error.InvalidCharacter => return error.InvalidCharacter,
};
const expected: []const u16 = &.{ 1, 51, 765, 65535 };
try std.testing.expectEqualSlices(u16, expected, deserialized);
}
And here is my bonus solution to scenario 1 (returning the de serialized values on the stack)
// scenario 1: 4 fixed-width integers
/// return 4 ints on the stack because its small and we prefer not to allocate
/// if we do not have to.
fn deserializeFourInts(reader: *std.Io.Reader) ![4]u16 {
var result: [4]u16 = undefined;
for (&result) |*num| {
const bytes_containing_int: []const u8 = try reader.takeDelimiterExclusive(' ');
num.* = try std.fmt.parseInt(u16, bytes_containing_int, 10);
}
return result;
}
test "four ints" {
const serialized: []const u8 = "1 51 765 65535";
var reader = std.Io.Reader.fixed(serialized);
const deserialized = try deserializeFourInts(&reader);
const expected: []const u16 = &.{ 1, 51, 765, 65535 };
try std.testing.expectEqualSlices(u16, expected, &deserialized);
}