Tess receiver blocks: reduce visual noise in API headers
08 May 2026
I wrote about some practical enhancements to the C language that I introduced while developing the Tess language in a recent post. I left out a feature I call 'receiver blocks', which are syntax sugar to factor out common initial parameters from a set of function declarations (or definitions).
These blocks are inspired in part by typical C++ class declarations, where the first argument is of course
implied when you're reading a class declaration. In C, we often create structs and functions in a similar
style, but our declarations have to include the self parameter and its type each time. This is no large
burden, but it can create unnecessary clutter, especially when we have to namespace the functions too, due
to C's global namespace. (In Tess, functions are namespaced automatically to their module, just like a C++
namespace.)
One way Tess goes a bit further than other approaches to factoring out implicit receiver arguments is that
Tess allows more than one argument to be factored out. And, it's not limited to self arguments, of course:
arbitrary names and types are allowed, because this is purely syntax sugar.
Let's look at a typical example: declaring the API for a dynamic array/stack:
typedef struct { ... } Array; void array_push(Array*, int); int array_pop (Array*);
In Tess, we might as well use a generic type argument, T
Array[T]: { ... }
(self: Ptr[Array[T]]): {
push(x: T) -> Void
pop() -> T
}
With one or two functions, it doesn't look like much, but imagine a huge block functions with the first
argument annotated with Ptr[Array[T]], and you can understand why I implemented this feature.
As an interesting consequence of receiver blocks, I found myself organising API declarations by receiver type. In particular, constructors, mutating operations and non-mutating operations are in three separate blocks. If we add the fact that Tess supports explicit polymorphic allocators, those APIs reside in yet another block, taking advantage of the multi-argument receiver block syntax:
(self: Ptr[Array[T]], alloc: Ptr[Allocator]): {
push (x: T) -> Void
reserve (count: CSize) -> Void
free () -> Void
insert (index: CSize, val: T) -> Void
resize (count: CSize, val: T) -> Void
shrink_to_fit() -> Void
append (other: Array[T]) -> Void
}
Each of the functions above have two arguments not listed in each line: self, and alloc.
I really like this syntax, because it dramatically reduces visual clutter. Imagine the alternative, which is what the blocks de-sugar into:
push (self: Ptr[Array[T]], alloc: Ptr[Allocator], x: T) -> Void reserve (self: Ptr[Array[T]], alloc: Ptr[Allocator], count: CSize) -> Void free (self: Ptr[Array[T]], alloc: Ptr[Allocator]) -> Void insert (self: Ptr[Array[T]], alloc: Ptr[Allocator], index: CSize, val: T) -> Void resize (self: Ptr[Array[T]], alloc: Ptr[Allocator], count: CSize, val: T) -> Void shrink_to_fit(self: Ptr[Array[T]], alloc: Ptr[Allocator]) -> Void append (self: Ptr[Array[T]], alloc: Ptr[Allocator], other: Array[T]) -> Void
It's a blinding wall of text, with the important information overwhelmed by the repeating parameters. The C
equivalent is a bit better, due to the postfix * syntax, but it's still repetitive:
void push (Array* self, Allocator* alloc, T x) void reserve (Array* self, Allocator* alloc, size_t count) void free (Array* self, Allocator* alloc) void insert (Array* self, Allocator* alloc, size_t index, T val) void resize (Array* self, Allocator* alloc, size_t count, T val) void shrink_to_fit(Array* self, Allocator* alloc) void append (Array* self, Allocator* alloc, Array other)
While I do like the receiver block syntax, it has an obvious downside: long receiver blocks mean the reader
has to scroll back up to figure out what the actual callsite needs to look like. Since Tess supports
dot-call UFCS syntax, like arr.push(val), or arr.push(alloc, val), it's a natural pairing when the blocks are
used in unsurprising ways. But it's surely possible for readers to be confused if they're not careful.
You can also have a look at the full Array.tl source from the Tess standard library.
Contact: @mocom@mastodon.social. Published using C-c C-e P p. If you use this content commercially, kindly make an appropriate donation to Zig Software Foundation in my name.