Pitch #3: ConcurrentValue and @concurrent closures

Thanks, I clarified this a bit by saying "Furthermore, they implicitly capture local values by-value (which are immutable like a let value) and require that the captured values conform to ConcurrentValue."

No, it does a normal Swift copy.

Yes, I agree that someone could try to do that, and it would be a bad idea (given that mutable strings subclass immutable strings in ObjC). I'm trying to subset ObjC interop and related issues out of this base proposal though, so I think that further extensions like this can be considered as follow-ons.

Thanks Matthew. Yes, that's pretty much what it implies. The value can be shared across concurrency domains and safely accessed concurrently. This is true for values without sharable mutable state (like Int), true for COW types like arrays that have ConcurrentValue elements, and true for internally synchronized types like concurrent hash tables.

That said, I don't think the name is perfect either. We can debate that on the eventual review thread. The major issue I have is related to your other question:

Yes, I dropped it because it is a separable issue and is getting bogged down on definitional problems. Such issues can be handled in a follow-on review.

That said, I think the major concern about naming the protocol ConcurrentValue is that the natural name for ValueSemantic may be just Value... and that may make us think twice about the name ConcurrentValue. That said, this is a conversation for another day IMO.

I agree. I personally prefer explicit conformance, but I understand that some folks prefer implicit conformance. This is a fairly binary decision that can be made at the time of a final review.

One example problem with implicit conformance is cited in the proposal from an earlier discussion. I don't think we should make public conformance be implicit. I think your argument is further fuel for the fire that argues against implicit conformance at all.

No actually, that's really the point. These are two orthogonal things. An escaping closure within an actor is not necessarily concurrent. Similarly a concurrent closure is not necessarily escaping (e.g. parallelMap). Modeling these are two orthogonal things allows precision and avoids conflating different axes of function types.

Here's a silly example of a non-concurrent escaping closure within an actor, which is perfectly safe and correct by construction:

actor MyActor {
   var stateFn : @escaping () -> ()
   func escape(fn: @escaping () -> ()) { stateFn = fn }

   func stuff() {
     var local = 42
     escape { local += 1; print(local) }
   }
    ...
}

It is safe because the actor local state (a capture of local in the stuff function) can only be touched by things that touch stateFn which is itself actor local.

This is the magic that lets ConcurrentValue and @concurrent functions compose correctly: only @concurrent functions are ConcurrentValue's. Because only ConcurrentValues can be shared across actors, you know that any non-@concurrent function is local to the current concurrency domain. This is what provides memory safety of the model.

Yes, I think this warrants further discussion. I was just imagining that a closure expression passed in a context that requires a concurrent context would be inferred to be async. A closure expression that contains an await would also force async on its context. I think that is enough, do you agree?

I do not think that escaping makes sense to conflate into this per the points above, but I'm happy to discuss this and provide more examples if that is not clear.

Yes, this is exactly why this is a bad idea. Let's keep these correctly separated from each other.

Right. I put this in the standard bucket of "interfacing with a concurrency unsafe world is unsafe". Dispatch is an unsafe API, as are other C APIs and other things. I understand that it might seem enticing to "fix" this by conflating escaping and concurrent function types, but you can't achieve that.

The issue is that there is nothing that correlates concurrency in the unsafe world with escaping - it is just as possible (from a type system perspective) to have these issues with non-escaping closures. The Swift rule is that non-escaping functions are bounded by the lifetime of the callee that receives them. You can have fork/join parallelism with non-escaping functions, e.g. things that invoke unsafe equivalents of parallelMap that are implemented in Objective-C or withoutActuallyEscaping.

Conflating escaping and concurrent just makes the pure-swift-with-concurrency model more confusing and less expressive, without closing the safety-hole-with-unsafe-code issue that you are concerned about. I think it is much better to keep them separate, and move the world away from unsafe constructs towards safe ones over time. Dispatch, pthreads, and other unsafe APIs are not going to be safe no matter what we do, just as C APIs using manual memory management aren't safe even if they don't expose an unsafe pointer directly (e.g. because the pointers are wrapped in C structs).

That said, I think it would make sense to invest into introducing Clang attributes that allow marking APIs that take Blocks and C function pointers in a way that would allow Swift to diagnose warnings, and it would make sense to have an attribute that causes the imported-into-swift API to take a @concurrent closure. This is the standard way we handle imported APIs, and this can go a long way to make them more safe and nice to work with in general. I'm just trying to nail down the core language model first.

I'm super happy to adopt that, could you propose wording, or directly write it into the proposal? You should have direct edit access, let me know if not. Alternatively, I can also inject edits if you'd prefer to communicate them another way.

Are you suggesting an additional affordance, or arguing against something in the base proposal? If the former, then I think it should be part of an @actorIndependent proposal. If the later, I'd love to know more, e.g. with an example of what you're thinking about here.

Please let me know if the comments above don't make sense to you or you'd like to discuss them more, thx.

Agreed, this is a critical level of the stack to get right. Thanks!

-Chris

2 Likes