Context Application/Library design

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.

2 Likes

Hi @mutech,

I moved this to its own topic, because I think it is too long and not focused / clear enough what this idea is about to keep it in the topic where you posted it originally.

I think most of the posts in the Advanced project ideas, were quite clear and mostly focused on naming the idea with short descriptions, instead of going into lengthy descriptions.

I think it is fine to create posts for things that require more description, but to keep the original topic from becoming difficult to read, topics that are likely to require clarification and discussion about the concepts are better done in their own topic and then linked in the Advanced project ideas topic.

Feel free to change the title if you have one that better fits what you had in mind.

4 Likes

Thanks for making the effort! I almost regretted writing it because it’s too long and fuzzy. I posted it anyway because the issue is bugging me right now and I wanted to get it out of my head and see if somebody else also shared my issue with this problem.

Here is some of my perspective:

It seems to me that you are thinking about designing different reusable code pieces and how those could neatly click together like lego pieces.

I think one problem with that is that there could be many different kinds of building systems and different applications may call for different ones.

I think specific problems can really be improved by some particular set of constructs, I just don’t know whether we can easily find something that is satisfying to use in very many or all circumstances. So I think there likely will always be different ways and those might need to be bridged too in some places.

Personally I tend to go the bottom up route of creating a single piece that I like, then some other piece and then I put both of them together and try to find out whether they work well together. And try to see whether redesigning any one of them might improve how they can work together.

So instead of starting with a big generic and abstract collection of imagined possibilities (which are vague because I haven’t created specific implementations yet), I try to build small concrete parts and then only make them more generic slowly as I discover ways to make things more general and inter-operative.

I think this approach is slower, but I don’t think that that is necessarily a bad thing, I actually think it allows those things to get more concretely designed (and re-designed) to be good matches to specific use cases and ways in which they interact/combine with other pieces.

I also find it helpful because that way I slowly build up a collection of pieces and at any step I can focus on any number of them and say these are the only pieces I care about right now and that comes with a set of possibilities and constraints that tells me how things could be implemented. So I think this approach naturally allows to approach problems in a way where they are more focused on specific scenarios without having to deal with any use-case there could be.

I found that when I tried to create things that would do everything and work for everything, it always turned into a thing that eventually melts the brain and turns into some kind of gordian knot.

When creating things that are designed from this more limited interoperability, I find it becomes more like islands that are connected via bridges, where you can see more clearly what is possible and what isn’t and where you would have to create a new bridge to make something new possible.

I think in a way I am advocating for using more local small/concrete/specific ways to create interoperability, instead of creating global generic object soups where everything can do everything and should support everything.

I am curious what you think about these approaches.

1 Like

I pretty much follow the same approach as you, at least when I’m programming for fun.

In my work context, it’s often different, because there are too many cross concerns.

Currently I’m working on an idea replacing Ansible and for an early prototype I’m wrapping an SSH library in Zig (libssh).

The ultimate goal would be to have a plugin interface that can work with different SSH implementations. All of them would have to integrate with logging, debug modes. There would need to be rudimentary UI support (type a password, accept an SSH host key and what not).

Another module that runs on localhost (shell/fs) would need the same interfaces.

The problem with this endeavour is that I cannot possibly foresee what I will need. But I know right now, that I need a parameter “do not require SSH host key” that I may use when connecting to a newly deployed VPS. This can be set in a playbook as option, but it might well need to be put to the user in an alert.

Conceptionally, all I need is “confirm(!require_host_key)”. Confirm is a generic UI primitive and require_host_key is the ID of a question. It does not matter how that is eventually implemented, what text a user is asked or where the parameter is coming from.

So I would like to have context that can anchor such a transaction in a simple way to the consumer (ssh-connection) and later I can connect the relevant functionality to that.

A default implementation in the library might ask the user on stdout.

Why not use a natural fine grained API such as what libssh, openssh, libssh2 and dropbear do? That’s what you described and what we both do in “normal” programming mode. Well there is the answer, they all do the same thing but they all do it differently.

A good example what such contexts can do is in React.

You have a context provider and a context consumer. They have a protocol, the type of context (an interface or just any data).

They are related via the view hierarchy (JSX). The consumer gets the closest matching context upstream.

This is really cool to link up views with models and similar stuff.

But in a programming language there is no such thing as a “view hierarchy”, other than the call stack. if we could descent the call stack to find the closet matching context (that has a UI.confirm or a UI.log method, f.e.) this would be trivial to implement.

A context consumer (an arbitrary library) could then simply do:

// Context gets its instances from the stack in this example, not from some `this`
Context(UI).log?(logMessage)
Context(UI).confirm?(questionId, questionText)

A provider could use an alert, a terminal prompt or a policy file to ask the user the question text or get the configured answer by looking up the policy default for the questionId (f.e. libssh.ignore-missing-host-key).

This is a very loose coupling and can be extremely powerful.

This is also very similar to all these framework concepts like IoC, aspect oriented programming. But these all require frameworks and it tends to be annoying to work with that stuff.

In the example, I followed your approach of doing the stuff I need right now and the flexibility is not in my code, it’s in the fact that what I need is “confirm”.

Another example would be “i need to cache some data”. Instead of dumping it in “/tmp” or looking for the best caching library or any one of the million ways to store temporary data, I would just lookup context(cache).set(key, value).

In the real world I would need to specify some criteria: life time, max size, security constraints and what not. But I can ignore all that now.

The interesting thing happens on the context producer side. There you also have a context and can either not touch it (passing it through) or replace, extend or modify it.

This is both incredibly powerful and very simple.

The only problem is that you needed support to store context on the stack or so that you can find it as if it was stored on the stack.

This would also solve any problems with memory management of context data (contexts on stack would just have auto-defer, like variables on the stack).

I don’t know if that can be done without language support. In react they found a way to hack that into JavaScript, which is quite impressive.

Interesting topic as far as I can understand it.

I often have the feeling or question “why is there no generic solution for this”? When working on another insane database conversion sometimes i wish there was a magic operator overload A = B.
Mostly I follow the same thinking pattern as Sze describes.

Ranting (maybe off topic) a bit about “generic solutions”: I think the best software is very specific, detailed and handmade for a certain task (context). I like dedicated detailed software.
My statement is: the more generic a solution for certain context or problem is, the worse it gets.
Slow, incomprehensible, user-unfriendly, abstract software is the result.

I’m finding the ideas here somewhat hard to follow, because ‘context’ is one of those overloaded words in programming languages.

Nearest I can tell, you’re describing something like Odin’s implicit contexts. Go also has contexts, but those are an explicit parameter which is passed down the stack like any other.

An implicit context is hidden control flow, and Zig doesn’t do that. Go’s context objects aren’t a fit for different reasons, they’re used for handling goroutine-specific tasks like cancellation.

In Zig contexts (heh) I’ve mainly seen the word “context” used to describe closure-like structs, which carry a callback pointer and a user pointer *anyopaque, so that the callback function has some state to work with.

I guess where I’m going with all this is to ask the question: say the word “context” was forbidden, how would you describe what your goal is with this train of thought?

2 Likes

In deed! But it’s also one of the very few words that is still meaningful in many contexts :wink:

I have to read up on Odin and Go’s contexts, I don’t know them.

I’m not sure if it really is. It’s certainly hidden data flow, but so is the stack and Zig does this. Imagine we had an additional hidden parameter on the stack, next to parameters. It can be used exactly as a parameter, but it can’t be declared.

The only reason why this would be hidden is so that code that does not care for contexts does not need to manage it.

But that’s of course all semantics, because the hidden control flow is back as soon as hidden data is used to control the flow.

I don’t know if this qualifies as the kind of hidden control flow that is an anti-paradigm in zig.

I don’t know if I can get away with a description that won’t use context anywhere. Let me try.

Eric described a problem that haunts me since forever:

and

There are millions of examples for this. You use a library that was written for a specific use case and it’s great as well as often also the only available solution for a slightly different problem.

So you use it, but then it starts doing the strangest stuff that in the original use case of the library makes perfect sense but for you it just hurts. Or, your use case requires you to do the strange stuff and the library is not prepared to support that, because the strange stuff has to happen in the middle of a function.

The problem here is that both generic and specific coding has its up- and down sides. There is no right or wrong way to do things (not always).

What I am not supposed to call contexts aims at bridging the gaps and make it easier for specific code to be about as flexible as generic code without having to get generic coding right, which is the hard thing. Generic code is bad because it’s hard to get it right, not because it’s innately bad.

So my goal is, without using the word, to make generic code easier to get right or specific code more flexible without the burden of bloating the interfaces.

Does this description make it clearer?

EDIT: Odin: Yes, this is pretty much exactly what I’m looking for. Wow to Odin! :slight_smile:

EDIT: Odin#2: Or maybe not. What Odin seems to be missing (from glancing at it) is a way for a library to get its own namespace in context (not just “userdata”), because otherwise this would be like a global. The scope that is there (logger, allocator, assert-handler) covers common cases, but it lacks ui (info/notice/confirm), error handler (abort/retry/ignore) at the very least, to cover the most common breaking points.