Execution context support

I wanted to discuss something I brought up in the server logging thread a bit more generally.

I'll restate the idea here: to create a formal mechanism for managing an "execution context". To give a concrete example, if function foo() calls function bar() which calls function baz() (potentially in an async operation), allow implicit parameters to be set in foo and passed to baz without having to forward those parameters through bar. For instance (please ignore naming and syntax):

let contextArgument: Contextual<T> = …
func foo() {
   let contextValue = T()
   withContextArgument(contextArg, setTo: contextValue) {
       bar()
   }
}
func bar() {
   someAsyncFunction {
      baz()
   }
}
func baz() {
   print("\(valueOfContextArgument(contextArg))")
}

Under the hood, this would likely use thread-local storage. Core Swift would provide simple API to save/restore/modify/access the execution context, and commit that any future async APIs would propagate the execution context in an unsurprising way. Practically, popular concurrency solutions like GCD and NIO would need to adopt these primitives.

The current use case for this would be server logging, where adding contextual information to emitted logs is very useful when trying to debug issues in production, but such information is often omitted to avoid adding a context or logger argument to every function which may eventually log a line.

Some questions:

  1. Has this been brought up before?
  2. Can this be useful beyond the server logging use case?
  3. Can providing this functionality encourage developers to write hard-to-understand code? Is this a problem?
  4. Are there performance considerations that can not be addressed by providing an async(propagateContext: false) API?
2 Likes

I think some discussion was brought up when Chris Lattner's Actor manifesto first came around. I think the context there was around how GCD based APIs and future-Swift's first-class concurrency primitives would interact. I don't remember the details though. Unfortunately that topic died (and hopefully will eventually be picked back up).

That being said, this seems like it could nearly be written as a user level library. You just have to manually pass around a context. I could foresee getting around that by introducing something like Scala's implicit functionality. Where you can leave off certain things if an appropriate value can be found. However given how controversial/complex implicit is in Scala, I doubt Swift would adopt something like that.

FWIW, managing some execution context is almost the practical definition of a monad, isn’t it? The State monad, in this case. This is what “execution context passing” looks like using the do notation in Haskell, using a simple stack as the context. Given these two functions:

import Control.Monad.State

pop :: State Stack Int
pop = State $ \(x:xs) -> (x,xs)

push :: Int -> State Stack ()
push a = State $ \xs -> ((),a:xs)

…you can pass the stack implicitly like this:

import Control.Monad.State
  
stackManip :: State Stack Int
stackManip = do
    push 3
    a <- pop
    pop

(Example stolen from Learn You a Haskell.)

The big advantage of the do notation is that you can “plug any semantics in”, like chaining optionals or running async actions. So it’s not a one-trick pony – the language just supplies the notation, anyone can supply new monads (new meanings).

I’m pretty sure this was mentioned several times as an interesting option on the forums, but these things tend to get a bit controversial, as people think them too magical or complex. (I have no idea about it, it is well above my head.)

In order for this to be a useful user-level library, it would need to be used consistently, across many different async APIs. That way, folks could write unrelated frameworks that use the library without limiting the users of their framework to a particular concurrency primitive. I don't see that happening unless it is part of the standard library.

I'm not sure how implicit works, but for performance reasons I don't think we want to add an implicit parameter to all function calls once a context is set. This is why I brought up thread-local storage, it seems like the only way to have implicit parameters and not incur prohibitive overhead.