> 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

Reply via email to