Another Idea that I believe is a great fit for Zig. Sorry, this is just a brain dump and too long for my own taste. I hope it makes some sense.
Contexts
A lot of functionality is driven by context and it’s often difficult for general purpose libraries to accommodate use cases in different contexts.
Examples
Generic user interaction
UI’s are very diverse and platform dependent, e.g. desktop, terminals, phones.
Yet there are interaction patterns that can be applied to all or most of these otherwise very different systems:
info: Pass on some information to the user
notice: Same as info, but make sure the user really sees it (confirms receipt)
confirm: Ask the user for confirmation before a process continues
input: Obtain data from the user (usually text)
Error handling
What is an error in some scope of code may be a normal operation in the enclosing scope. Errors may be ignored in some context, downgraded to warnings, might need to be delegated or might have a recovery option in a specific context but not in others.
Logging
Logging has many purposes, but it’s almost always disconnected from the current processes of a software’s instance.
Yet the software has to decide whether to log an even, what information to include and how to categorize it. The more generic the software, the more difficult and error prone that is.
Transactionality
If code runs in the context of some notion of transactions (isolation, atomicity, reversibility), it often needs to do some extra work, such as to create snapshots, use expensive operations such as semaphores or locks, preserve information required to undo an action.
The more generic the software, the more difficult it is to provide such features. Software generally needs to be designed to support such features.
The Problem
The real world context has to somehow be considered in many different places. A library offering complex functionality has to either ignore context and be designed to support fine granular interfaces, or it needs to know about the context so that it can act on it.
That means that the context needs to be its own thing and it needs to be available where ever it’s needed. You could pass the context explicitly in function parameters, provide it implicitly through environment variables and similar side channels, use global state and identify local context through some mechanism. Some context is already there, for example the call stack. But it can’t be accessed (for many good reasons).
When I said “context is its own thing”, I meant that there needs to be a contract between context providers and consumers (an interface or an api).
So the real problem seems to be that we don’t generally acknowledge the importance of context and don’t have an abstraction in our programming mind set to handle context sensitivity easily.
We have the model of function calls and there parameters, return values and error conditions. This mechanism is powerful enough to support context sensitivity (all you need to do is to pass a “context” parameter), but we don’t generally use it. And there are good reasons for it:
- It’s yet another layer of boilerplate to take care of
- Context is rarely used, often all that needs to be done with it is to pass it on
- Context consumes resources. Both when providing it and when consuming it. If you need to do complex formatting to write a log message that will not be used because it’s not needed in a context, the computation time and memory is wasted
- Context might open side channels providing attack surface to malware. Context often means to hide enclosing context. You can hardly do that by adding context via arguments.
The Ziggy Solution
Zig added Allocators as a convention to provide memory management contexts. That’s not a language feature, it’s just good style.
But that is only successful, because memory management is so important, that everybody understands it needs careful consideration and seeing an “allocator” argument, immediately means something and almost the same this to every programmer. We need to have understood that, to work without a garbage collector or automatic reference counting.
Zig has comptime, which clearly supports the two contexts run- and compiletime. And yet, you can mostly write code ignoring the difference, because it doesn’t affect you - because comptime takes care of it.
The meaning of code mixing run and comptime is clear because its semantics does not depend on when code is evaluated, but by what it actually does.
On the other hand, it’s also easy to understand what comptime does and what the results of it are when you look at code.
Having comptime available makes it possible to create zero cost abstractions for context sensitive code, at least for all aspects that can be decided by separating run time and comptime.
For example, a library can make intensive use of logging, provide flexible error handling, use an abstract user interface into which client code can inject a concrete user interface. And at the same time, the library can compile to code that does not contain any overhead if none of these features are required.
Context is most often nested, mainly because our software is. All code has a stack trace - context.
But there is also flat or more generally any kind of context. A disk is context, at least when its full while we want to write to it. Network is context, especially when it fails. Context is typically what we forget when we write version 0.1 of something.
The only problem that remains is that context is not restricted to the two aspects of memory management and compilation vs. run time. We cannot even adopt context in our most fundamental mind model, the stack, because that would require that each and every developer who participates in a product would agree to dragging a context item around everywhere they go.
React has an interesting solution by offering a means to access context that is managed in their view hierarchy specification. They are (ab-)using a side channel by doing rather impressive JavaScript hackery to facilitate the communication between context producer and consumer.
I was trying to use a similar approach by adding a context object to a type as a comptime parameter. There I defined error handler methods to allow client code to hook into error handling logic (providing a recover method allowing an actor to ignore/retry/replace/abort). That worked nicely until I needed parameters that in turn need the parameterized type and then I hit the wall with cyclic dependencies.
This could be hacked around and made to work, but my goal is not to find a hack but instead a to use a pattern for handling contexts in general.
Zig has two very important features supporting a satisfying solution of the context problem. Comptime mitigates performance issues. Allocators allow flexible solutions for resolving unrelated life time issues.
The only problem is how can an arbitrary piece of code access a particular type of context and then get the right version of that context depending on where that code runs right now. That’s what this proposal is about.