Context: The Missing Feature of Programming Languages
medium.com
medium.com
For example, if every function is allowed to allocate memory whether it does or not, then changing the implementation of a function so that now it allocates memory is a simple, backward compatible change. If you have to declare that you allocate memory (for example, by adding a new argument, as in Zig), making the same change breaks compatibility, so now you have to change all the callers.
Depending on the circumstances, that might not be considered worth the trouble. It's a reason that Java's checked exceptions aren't popular. Often, reporting a new kind of error shouldn't be a compatibility break; they're all going to get caught and logged the same way anyway.
(An extreme case would be making performance guarantees part of the API, which, outside maybe real-time environments, nobody does. This means that a new version of a library could be much slower than before, maybe causing your servers to become overloaded, without any compiler warning. But performance depend so many factors that making guarantees is usually unfeasible.)
The proposed idea of having a compiler do certain context checks without declaring anything in the API means that there are invisible API constraints that are generated from the implementation. This is sort of like having type inference without the option of declaring a function's type explicitly.
Also for practical reasons an escape to dynamic typing should be provided (the equivalent of unchecked exceptions).
The real difficulty is finding the tradeoff point between too much annotation and too little, bit that's more of an art (or engineering) than CS.
Or maybe even allow the client to use contexts or not. Like pick your own level of abstraction.
(Some googling later)
Yes, it does. Looks like it supports a stack of contexts and is used for stuff like allocators and loggers.
Plain text is the problem. We write code so it can be easily read as plain text. Not as living, breathing, running code.
Globals are considered bad because they are hard to trace and reason about. But this is only so with plain text. You cannot click on a variable and see all it's dependencies. We must painstakingly trace these.
If we visualized the dependencies and the flow of our code, we could see the complexity as we go. And the "context" becomes automatic. There is no difference between explicit and implicit context.
The key for globals, is to have one large graph data structure where everything is related. All your data is modeled the same. No mini-databases spread out over the code. No ad-hoc indexes strewn throughout. No modularity. One single giant data model.
I like global variables as well. One awesome thing they enable is custom debug UIs:
I note that in Scala (and apparently swift) the implicit parameters are type based rather than name based. This works great for “singletons” but is more restricting than the coeffect work referenced. It does solve the problem that having just a single namespace for implicit parameters is bad - nominal types live in their parent namespace.
I’d combine the two and allow to define “context parameters” in module scope. Your base logger library could have a “slot” for the logger, for example. The standard library an allocator. Etc. You’d replace them with `with` or similar, and `with Logging.@logger = Logging.nullLogger()` might let you disable logging within a dynamic context (rather than using `without`). I’d allow `with` to be written in series in a block much like `let`, rather than requiring a bunch of indentation (they would fall out of scope at the end if the block anyway).
Anyway I’d love to have time to contribute towards something like this!
From the context (hehe) I think you meant you prefer the explicit version? Personally I'd prefer to make things explicit, even if it's more verbose.
> Bluefin is a Haskell effect system with a new style of API.
> It is distinct from prior effect systems because effects are accessed explicitly through value-level handles which occur as arguments to effectful operations.
https://github.com/tomjaguarpaw/bluefin/blob/master/bluefin/...
This was the suggestion of "A Second Look at Overloading" [1], where they eliminate class declarations and simply declare which individual functions/operators are overloaded.
I impkemented a slow version of contexts in Smalltalk, just to see how bad it was. Where it shone was in places where you can't pass a context as an arg (eg. binary math operators) either for compatibility reasons or because passing the context would be too noisy.
I've found dynamic scoping useful so few times I can count them on one hand, and while it's an intriguing feature, I think it makes a lot of sense to use a more explicit abstraction. For example, you shouldn't need dynamic scoping for overloading binary operators - you should have a more clear method that takes a context as an argument instead.
Context is external state, and it's extremely surprising when a fundamental operator like "+" modifies it.
Calling useContext(ctx) looks up nearest parent call frame defining setContext(ctx, value)
Of course that makes for implicit global dependency, unless you pass a context manager ref around almost like Go does
But at least it'd help scope resources instead of using globals while avoiding a coloring problem (see Go context where if an interface doesn't take context one has to find an alternative means to passing context through)
You can even implement “without” patterns, albeit only as runtime checks - though if you’re following the React practice that hooks must be called unconditionally, and you have test coverage, you essentially have compile time checks.
And, speaking of coloring problems, if you’re running on gevent, both become greenlet-local, and you can benefit from concurrency limited only by working memory, without needing explicit async code.
Perhaps think of javascript-like objects, but much less impoverished. With multiple inheritance prototyping. And fields that not just shadow, but can compose array/object values. And aliases, renames, deletions, set ops, etc, etc. Extremely succinct specification of namespace derivations and variants. Sort of a configuration system. "Object A is composed of interwoven parts of specifications B and C, and when creating variations on A, the contributions of those parts can be altered by name".
And the namespace then becomes the parse, compile, runtime, and dynamic scope contexts. For Maude-like localized control - "in this lexical context, I want integers absent, numbers to parse as Decimal, use this type system, be implemented using this library, with this allocator, ...". So a lexical scope can begin with `use C99;` or `use Python 3.11;`, and those are "simply" two unremarkable computation contexts. From past experience, it's nice to have a forcing factor of "ok, if we were merely implementing one language then mumble would be satisficing, but when doing n languages, we need to up our game by ...".
My next step was going to be putting together an illustrative demo using the great diversity of computations one might describe as say "2+2". Integers and float types, with diverse and not-always-contiguous bit layouts, cpu flags, finite rings, etc, etc. It ended up backburnered, but summer is coming, and this thread reminded me of it.
Programming language theory also has other abstractions for context as well, like delimited continuations.
The real issue is figuring out a way to statically propagate implicits up without mandatory explicit annotations or whole program inference.
(I think propagating context up should be fine, but requires functions specialisation by call site).
As does Lean: https://lean-lang.org/lean4/doc/implicit.html
Would yours look different from those, or is it the same idea?
To be clear, I'm not inventing anything here, I found out about implicits from scala.
The section on exceptions got me thinking of CL condition system. Idk if it somehow meshes in here, I suspect it might provide some mechanism to inject context somehow... but I never have programmed a single line of CL so idk.
I created ctx-core/be[1] & ctx-core/rmemo[2] to provide Contexts, be_ functions, & reactive memos. It has been a powerful set of abstractions for application code, build logic, devops, animation orchestration, etc.
I would love to use a systems programming language that supports Contexts & compiles fast. Jai looks interesting but it's still in closed beta. With Zig, there was a proposal to dependency inject a context which contains the allocator[3]. It didn't go anywhere though. Golang has Context[4]. It compiles fast but I would like a fast language without a Garbage Collector to compliment js/ts for expensive computations. Perhaps Rust could work but the compile time is slower than ideal.
At this point, Golang seems to be the best compliment as it addresses expensive computations. Hopefully a productive system programming language without the GC will be available to have good support for contexts.
[1]: https://github.com/ctx-core/be
[2]: https://github.com/ctx-core/rmemo
> […]
> Unfortunately deadlocks are not always so easy to spot, especially as more layers of abstraction are added
> […]
> Compilers don’t even try to help here.
https://clang.llvm.org/docs/ThreadSafetyAnalysis.html:
“Clang Thread Safety Analysis is a C++ language extension which warns about potential race conditions in code. The analysis is completely static (i.e. compile-time); there is no run-time overhead. The analysis is still under active development, but it is mature enough to be deployed in an industrial setting. It is being developed by Google, in collaboration with CERT/SEI, and is used extensively in Google’s internal code base.
Thread safety analysis works very much like a type system for multi-threaded programs. In addition to declaring the type of data (e.g. int, float, etc.), the programmer can (optionally) declare how access to that data is controlled in a multi-threaded environment. For example, if foo is guarded by the mutex mu, then the analysis will issue a warning whenever a piece of code reads or writes to foo without first locking mu. Similarly, if there are particular routines that should only be called by the GUI thread, then the analysis will warn if other threads call those routines.
[…]
EXCLUDES is an attribute on functions or methods, which declares that the caller must not hold the given capabilities. This annotation is used to prevent deadlock.”
Googling “static deadlock detection” will uncover similar tools (often not as well integrated with the compiler)
(Jai is the closed beta language by Jonathan Blow.)
However, that alone is not enough for what the article author wants because context stacks are a runtime concept.
The problem with contexts as the author wants is that it only works where things can be statically determined.
This means that the restrictions a function has should be part of its type.
This can be done, but then you have a problem: your functions are essentially colored and/or you are dealing with the Java checked exceptions problem.
The language that solves this, and avoids the function coloring and checked exceptions problem, would be adopted literally overnight.
Needless to say, I am trying to do this in my language, but I am starting to think it may be impossible.
Kubernetes is somewhat distinct in how it normalizes what a broad range of infrastructure objects or intents. Context is a collection of state, and those common ways of dealing with state are the missing practice, be that at a programming language level or other systems level.
Idea for implicit parameters:
Implicit parameters are values from somewhere up the call stack. So make an object with a get function:
Scope.get('Logger')
But then you can get anything. So add a mechanism to put things in there up the call stack: logger = Logger()
Scope.put('Logger', logger)
That's a key-value store with scope levels.(and remember there are good reasons we try to do as much as possible with lexical instead)
The idea is also to tie it together with a static compiler that checks everything fits together.
That might be something an AI could do, but I can't imagine any complex project maintaining a secondary mechanism like that
Spring boot is sort of like that.
Microsoft would call them wizards.
> Much as we like our platonic ideals, our pure functions, we ultimately have to acknowledge that every function exists in a context. The most basic context is the physical hardware on which your code is running
(Note: pure functions set such a high bar that it's a stretch to criticise them. Might as well blame the hardware instead. Which is fine, just be equally dismissive of other 'solutions'.)
> when one fails, it’s up to the engineer to figure out why. In the case of race conditions or other rare occurrences it might not even be possible to reliably reproduce the issue in a debugger,
Pure functions will return the same input for the same output. It will reproduce.
> In particular, they should be better at detecting cases where two separate units conflict with each-other.
Pure functions do not conflict with each other - no detection needed. ("Conflicts" can only arise by "passing the wrong thing" from function to function, which I don't think is the idea here, e.g. in `getPetsName = (getName . getPet)`, getName and getPet cannot conflict with each other, but if getName accidentally returns an address, then of course getPetsName will return the pet's address.
Deadlocks:
> One of the simplest and most pernicious programming problems is the humble deadlock.
> Unfortunately deadlocks are not always so easy to spot, especially as more layers of abstraction are added.
> Deadlocks are common, hard to debug, and can crash an entire app — yet our best defenses against them, integration tests, are porous and blunt.
Pure functions don't deadlock.
> The symptoms are less severe than a deadlock, but the root cause is often quite similar: a function calls a function that calls a function that calls a function that does something inappropriate.
> Compilers don't even try to help here.
It is a compile-time error to call an impure function from a pure function.
> When I call getTimestamp(user.registeredAt, TimeZone.PST) I can assume that it’s just doing some simple math and returning a result. It probably doesn’t make network calls or hold locks or mine bitcoin
No need to assume, check the type signature. Pure function. The compiler will check it for you if you forget:
utcTimeTimestamp :: UTCTime -> Timestamp
https://hackage.haskell.org/package/timestamp-0.2/docs/Timestamp.html
> Context is only dangerous because it is absent from a function's arguments and so is opaque to the caller. The caller, for example, has no way to know if a function could cause a deadlock because it cannot know if the implementation of that function relies on locking.> Knowing when a function modifies state is half the battle, the other half is knowing when a function reads state. This is not an Effect, but rather a Coeffect — although sometimes it all gets lumped together under the banner of ‘Effects’ or ‘Effect Systems’.
Of course it gets lumped together. That kind of reading is an effect. It can cause deadlocks, it can block your UI, it requires thinking about context, it may not be trivially reproducible.
That's enough about pure functions. The author then pivots to proposed features to support context. These don't solve the above problems, but there's plenty of prior art. So this last bit is more about Haskell and less about pure functions in general.
We have a complicated git merge process at work, with around 70 repos. I wrote a Haskell CLI tool to help me out with the process. I believe I've done exactly what the author is suggesting (explicit, compiler-assisted context markers) by using the 'tagless final' approach. Here are a couple of example type signatures:
ensureCheckedOut :: (Git m, Logger m, Monad m, StatusApi m) => Repo -> m ()
ensureRepoDeleted :: (Logger m, Monad m, Shell m, StatusApi m) => Repo -> m ()
This keeps my context in check by only allowing me to call impure functions if they are provided by one of the constraints on m. e.g. I can call logging from both, but ensureCheckedOut cannot run shell commands, and ensureRepoDeleted cannot invoke my git api (for the pedantic, it could technically use the shell to invoke git via cli.) Pure functions are of course allowed from anywhere.In short:
* Pure functions do solve the above problems and it's a mistake not to use them wherever possible.
* Context systems do not solve deadlocks, integration issues, reproducibility, etc. But if you think they're useful, Haskell's got you covered using the 'tagless final' approach.
> The paradigm separates the domain model (data) from use cases (context) and Roles that objects play (interaction). DCI is complementary to model–view–controller (MVC). MVC as a pattern language is still used to separate the data and its processing from presentation. [1]
> DCI was invented by Trygve Reenskaug, also the inventor of MVC. The current formulation of DCI is mostly the work of Reenskaug and James O. Coplien. [1]
[1] https://en.wikipedia.org/wiki/Data,_context_and_interaction
- No mention of dynamicly scoped vars
- No mention of Implicits in Scala
Who is the target audience of this article?