> On Aug 18, 2017, at 12:35 PM, Chris Lattner <clatt...@nondot.org> wrote: > >> (Also, I notice that a fire-and-forget message can be thought of as an >> `async` method returning `Never`, even though the computation *does* >> terminate eventually. I'm not sure how to handle that, though) > > Yeah, I think that actor methods deserve a bit of magic: > > - Their bodies should be implicitly async, so they can call async methods > without blocking their current queue or have to use beginAsync. > - However, if they are void “fire and forget” messages, I think the caller > side should *not* have to use await on them, since enqueuing the message will > not block.
I think we need to be a little careful here—the mere fact that a message returns `Void` doesn't mean the caller shouldn't wait until it's done to continue. For instance: listActor.delete(at: index) // Void, so it doesn't wait let count = await listActor.getCount() // But we want the count *after* the deletion! Perhaps we should depend on the caller to use a future (or a `beginAsync(_:)` call) when they want to fire-and-forget? And yet sometimes a message truly *can't* tell you when it's finished, and we don't want APIs to over-promise on when they tell you they're done. I don't know. > I agree. That is one reason that I think it is important for it to have a > (non-defaulted) protocol requirement. Requiring someone to implement some > code is a good way to get them to think about the operation… at least a > little bit. I wondered if that might have been your reasoning. > That said, the design does not try to *guarantee* memory safety, so there > will always be an opportunity for error. True, but I think we could mitigate that by giving this protocol a relatively narrow purpose. If we eventually build three different features on `ValueSemantical`, we don't want all three of those features to break when someone abuses the protocol to gain access to actors. >> I also worry that the type behavior of a protocol is a bad fit for >> `ValueSemantical`. Retroactive conformance to `ValueSemantical` is almost >> certain to be an unprincipled hack; subclasses can very easily lose the >> value-semantic behavior of their superclasses, but almost certainly can't >> have value semantics unless their superclasses do. And yet having >> `ValueSemantical` conformance somehow be uninherited would destroy Liskov >> substitutability. > > Indeed. See NSArray vs NSMutableArray. > > OTOH, I tend to think that retroactive conformance is really a good thing, > particularly in the transition period where you’d be dealing with other > people’s packages who haven’t adopted the model. You may be adopting it for > their structs afterall. > > An alternate approach would be to just say “no, you can’t do that. If you > want to work around someone else’s problem, define a wrapper struct and mark > it as ValueSemantical”. That design could also work. Yeah, I think wrapper structs are a workable alternative to retroactive conformance. What I basically envision (if we want to go with a general `ValueSemantical`-type solution) is that, rather than being a protocol, we would have a `value` keyword that went before the `enum`, `struct`, `class`, or `protocol` keyword. (This is somewhat similar to the proposed `moveonly` keyword.) It would not be valid before `extension`, except perhaps on a conditional extension that only applied when a generic or associated type was `value`, so retroactive conformance wouldn't really be possible. You could also use `value` in a generic constraint list just as you can use `class` there. I'm not totally sure how to reconcile this with mutable subclasses, but I have a very vague sense it might be possible if `value` required some kind of *non*-inheritable initializer, and passing to a `value`-constrained parameter implicitly passed the value through that initializer. That is, if you had: // As imported--in reality this would be an NS_SWIFT_VALUE_TYPE annotation on the Objective-C definition value class NSArray: NSObject { init(_ array: NSArray) { self = array.copy() as! NSArray } } Then Swift would implicitly add some code to an actor method like this: actor Foo { actor func bar(_ array: NSArray) { let array = NSArray(array) // Note that this is always `NSArray`, not the dynamic subclass of it } } Since Swift would always rely on the static (compile-time) type to decide which initializer to use, I *think* having `value` be non-inheritable wouldn't be a problem here. > It would be a perfectly valid design approach to implement actors as a > framework or design pattern instead of as a first class language feature. > You’d end up with something very close to Akka, which has provides a lot of > the high level abstractions, but doesn’t nudge coders to do the right thing > w.r.t. shared mutable state enough (IMO). I agree that the language should nudge people into doing the right thing; I'm just not sure it shouldn't do the same for *all* async calls. But that's the next topic. >> However, this would move the design of the magic protocol forward in the >> schedule, and might delay the deployment of async/await. If we *want* these >> restrictions on all async calls, that might be worth it, but if not, that's >> a problem. > > I’m not sure it make sense either given the extensive completion handler > based APIs, which take lots of non value type parameters. Ah, interesting. For some reason I wasn't thinking that return values would be restricted like parameters, but I guess a return value is just a parameter to the continuation. I guess what I'd say to that is: 1. I suspect that most completion handlers *do* take types with value semantics, even if they're classes. 2. I suspect that most completion handlers which *do* take non-value types are transferred, not shared, between the actors. If the ownership system allowed us to express that, we could carve out an exception for it. 3. As I've said, I also think there should be a way to disable the safety rules in other situations. This could be used in exceptional cases. But are these three escape valves enough to make safe-types-only the default on all `async` calls? Maybe not. >> To that end, I think failure handlers are the right approach. I also think >> we should make it clear that, once a failure handler is called, there is no >> saving the process—it is *going* to crash eventually. Maybe failure handlers >> are `Never`-returning functions, or maybe we simply make it clear that we're >> going to call `fatalError` after the failure handler runs, but in either >> case, a failure handler is a point of no return. >> >> (In theory, a failure handler could keep things going by pulling some >> ridiculous shenanigans, like re-entering the runloop. We could try to >> prevent that with a time limit on failure handlers, but that seems like >> overengineering.) >> >> I have a few points of confusion about failure handlers, though: >> >> 1. Who sets up a failure handler? The actor that might fail, or the actor >> which owns that actor? > > I imagine it being something set up by the actor’s init method. That way the > actor failure behavior is part of the contract the actor provides. > Parameters to the init can be used by clients to customize that behavior. Okay, so you imagine something vaguely like this (using a strawman syntax): actor WebSupervisor { var workers: [WebWorker] = [] func addWorker() -> WebWorker { let worker = WebWorker(supervisor: self) workers.append(worker) return worker } actor func restart(afterFailureIn failedWorker: WebWorker) { stopListening() launchNewProcess() for worker in workers where worker !== failedWorker { await worker.stop() } } … } actor WebWorker { actor init(supervisor: WebSupervisor) { … beforeFatalError { _self in await _self.supervisor.restart(afterFailureIn: self) } } … } I was thinking about something where `WebSupervisor.addWorker()` would register itself to be notified if the `WebResponder` crashed, but this way might be better. -- Brent Royal-Gordon Architechies _______________________________________________ swift-evolution mailing list swift-evolution@swift.org https://lists.swift.org/mailman/listinfo/swift-evolution