This is very much an invitation for brainstorming. In the spirit of Cunningham’s Law, I’m hoping my initial half-baked idea can be improved upon because I do think there is some sort of enhancement that can be done to help authors reduce the cognitive overhead associated with finding the right function to call and using it correctly.
What motivated this post in part was the functions in std.array_list. There is a kind of ad-hoc structure to them which is encoded directly in the function names and wasn’t intuitive to me until I had experience using the various functions. Some of this overhead is to be expected, but I think perhaps some functions could be improved by having more structured signatures that can separate contractual properties / side effects from the simple description of what the function is intended to do.
For example, in the following appendSlice states in its comment that it may invalidate pointers (due to allocation) and appendSliceAssumeCapacity encodes in its name that it assumes capacity is present (and therefore presumably won’t allocate).
/// Append the slice of items to the list. Allocates more
/// memory as necessary.
/// Invalidates element pointers if additional memory is needed.
pub fn appendSlice(self: *Self, items: []const T) Allocator.Error!void
/// Append the slice of items to the list.
/// Never invalidates element pointers.
/// Asserts that the list can hold the additional items.
pub fn appendSliceAssumeCapacity(self: *Self, items: []const T) void
It’s true that an assert is present to check the capacity and that we can also see the absence of an Allocator.Error in the signature, but I think something like the following helps make it clear that there are a subset of functions that share this particular property. Furthermore, the function names themselves can still be simple strings via concatenation and static analysis tools could pick up these ‘tags’ and enforce their contract, etc.
pub fn appendSlice(self: *Self, items: []const T) Allocator.Error!void
pub fn appendSlice[NoAlloc](self: *Self, items: []const T) void
Calling the structured function could be either of the following:
self.appendSlice[NoAlloc](items)
self.appendSliceNoAlloc(items)
Another example might be:
self.foo[NoAlloc, ThreadSafe](items)
self.fooNoAllocThreadSafe(items)
Or
self.foo[NA, TS](items)
self.fooNATS(items)
It might be necessary to declare these at the top level, such that they can also be documented.
/// No allocation takes place.
fn_tag NoAlloc;
/// Safe to call from multiple threads.
fn_tag ThreadSafe;
These are just my initial thoughts. I’m interested to know what you think.