> On Aug 18, 2017, at 12:35 PM, Chris Lattner <[email protected]> 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
[email protected]
https://lists.swift.org/mailman/listinfo/swift-evolution