SE-0306 (Second Review): Actors

That is what I do.

Except all you're doing is saying they're doing it wrong, not actually providing useful suggestions or workaround for the issue. It's not useful to reply to someone's concern by saying, in essence, "just don't do that".

Apparently our perception about what a useful comment is differs as much as our opinion about inheritance. Since this is off topic, we should not further discuss this.

To get back to this concern, I don't see an easy way around it. You've already mentioned some options, but we can summarize.

  • Replace subclass mocks with protocol mocks. This has the advantage of being applicable to other types as well, but several disadvantages that have already been noted. These include having to maintain a complete duplicate interface of the type you're testing, as well behavioral differences that make certain things harder or impossible to test.
  • Expose customization points that can also be used for testing. Alamofire does this quite a bit, as we don't do protocol or subclass mocking. Instead, we have specific APIs that allow us to hook into specific events. These are useful enough we ship them publicly, but they also make a good testing interface.
  • Move away from mocking altogether. This can be difficult but also valuable, as mocks can often blind us to what we're actually testing. If you can be satisfied with testing the input and output of a system, rather than whether particular APIs are called or not, this approach can be much easier than constantly mocking every type under test.

In the end it'll probably require some combination of all of these approaches, and others, to replace the capabilities of subtyping, not just for testing but in general.

Appreciate your feedback and summary. The first option I'd love to avoid. The second option seems doable in what I own but not other libraries (though I appreciate the thoughtfulness in Alamofire). The third I do sometimes attempt but from experience it can be tedious and brittle without having some control points on larger systems.

Overall, I still think removing inheritance is going to create some bottlenecks in testing and development. I'll end with that.

  • What is your evaluation of the proposal?
    -0.5, lacking any form of inheritance will make testing larger stateful pieces more challenging. Quoting what I stated earlier

Considering this, I would very much prefer a way to subclass/subactor, if only to test actors. I think Chris' suggestion (2) "Support for inheritance but without designated/required initializers, class methods, etc." would work for most cases, as long as (a) functions can be overridden and (b) mutable test variables can be added, to the sub-actor.

  • Is the problem being addressed significant enough to warrant a change to Swift?
    Absolutely, and I'm very excited for actors in swift otherwise.

  • Does this proposal fit well with the feel and direction of Swift?
    I think lacking any inheritance is going to come as a surprise to a lot of engineers and create a new set of challenges.

  • If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?
    A bit of Scala, but I don't know the semantics deeply enough to do a hard comparison. Otherwise, I understand the theoretical implications well enough, and other than the lack of inheritance the proposal is in line with what I was hoping.

  • How much effort did you put into your review? A glance, a quick reading, or an in-depth study?
    I've been keeping up with various proposals and manifestos for a long while. I only felt as if I needed to voice an opinion once inheritance was taken out however.

1 Like

FYI, the core team has accepted this proposal, and no decision has been made about whether actors will support inheritance. Please see the announcement post for more information.

1 Like

Thank you everyone for participating in the review. The Core Team has decided to accept this proposal, including the modification to make let bindings to be isolated by default like other declarations are proposed to.

The ongoing discussion here is still interesting and relevant to the near-term evolution of actors, so I think it is OK to leave this thread open.

9 Likes

I think it was already mentioned somewhere, but I sadly couldn't find it:

Why classes/protocols couldn't be actors?

Isn't an actor simply an isolated class/protocol propagating the isolated property onto all class/protocol members?

Is it because of existing ABI constraints and non-retroactive subtyping compatibility?

@Jon_Shier your comments above about mocking are very interesting, and we can consider adding inheritance to actors in the future if this become a major concern in practice. Note however, that the same concerns apply to structs and enums as well, and we're very unlikely to ever add inheritance to them.

I'd personally prefer that we improve ability to write tests across the board, not just apply OO based techniques to classes.

-Chris

14 Likes

Technically yes, these concerns apply to all types, not just actors. In practice, however, mocking largely isn't necessary for typical value type usage. In most cases you can simply use the real struct or enum in your tests because they don't have the internal complexity that typically necessitates mock-based testing. However, as @rex-remind originally mentioned, since classes are often used to encapsulate logic with complex internal mutation or async behaviors, mocking can be the easiest way to test particular invariants.

For instance, in Alamofire we must always be certain of the order of various async operations within our Request subclasses. This includes both our own internal actions, as well as our interaction with various URLSession delegate callbacks from outside. There's really no other way to check for race conditions or changes in URLSession behavior. We currently use a protocol to hook into these actions (EventMonitor) with specific callbacks along the way. However, this interface is huge (40(?) events and counting), creates a bit of runtime overhead, and must be manually updated as we add functionality. But we pay this price because it's useful both internally and externally (largely for loggers) so we ship it publicly. Requiring this sort of overhead, rather than it being a choice API designers make, would be rather onerous when subtyping offers a simpler solution, even if it's one with its own drawbacks.

With a subclass we could hook into just events that we care about for any particular test(s). Maintenance is only required when the particular events we care about change, and there's no runtime overhead in the library in general (aside from that inherent in classes, which we would use anyway). In most ways it's the simpler solution. Given the complexities of the workflows I expect actors to encapsulate, there really isn't a good replacement for this functionality.

Personally, I'm not a huge fan of mocking as a testing strategy, largely due to scars from a project that overused Obj-C dynamic mocking (through the Kiwi library) causing a variety of issues. But there are certainly cases where polymorphic or subtype mocks are appropriate and limiting or removing the ability of the language to build them doesn't seem like a good idea when the payoff is so small.

2 Likes

One thing I’ve been wondering is whether we envision people customising the executor of an actor (e.g. an actor provided by a library). If so, that would seem to either require subclassing (and an open actor), or for actors to store their executor as an ivar which is set by an initialiser. Both require the library author to predict that somebody might want to customise the executor.

So then I thought - what about if we used a wrapper struct? Let’s say it contains only 2 things - an instance of an actor, and a custom executor on which to isolate the actor’s data members. Can a struct even conform to big-a Actor? Will it be treated like any other small-a actor by the rest of the language if it does so? (e.g. calling a function on the struct is implicitly async)

Theoretically, if all calls to that struct were isolated to some executor, shouldn’t it be able to synchronously access the wrapped actor’s properties? I think that’s what would be required in order to actually customise the executor without inheritance or support from the library author.

It should be safe to do that... unless, of course, the actor internally enqueues other work on its executor, because it wouldn’t know that it’s being synchronised externally. We would need big-a Actor‘s serialExecutor getter to do some magic and figure out what its “effective executor” is.

TLDR - I’m not sure it’s going to be feasible to grab some pre-made actors from third-party libraries and compose a finely-tuned system out of them by customising executors.

According to the proposal, no, things wrapping actors cannot conform to Actor, as you can't manually add conformances at all. In my previous discussions on replacing subclassing, I thought about keeping my class hierarchy while encapsulating an actor to manage my mutable state. The obvious drawback there is the manual nature of that wrapping and the fact I'm no longer an actor and so have no Actor conformance. It's unclear how useful that conformance will be be, but it's definitely a loss of some kind.

1 Like

Maybe we can allow inheritance for struct/enums/actors in debug build for unit testing? Just a thought.

You can always use the facade pattern to achieve this. This is one of the very important principles of Swift with property abstraction and of course functions. This is effectively trivial for properties (just throw a property wrapper on). With functions you still have to do a bit of a dance to abstract them, but function wrappers could improve this someday.

OOP inheritance is only one way to solve these problems. I don't agree with the claim that "because mocking with inheritance is used in some languages and some other systems" that it is the best way to solve these problems in Swift.

8 Likes

We've been discussing facades via protocols in several posts. They aren't a viable test-only mocking solution since they depend on the product under test being built with them in mind so you can swap out your test version. They're also a relatively high maintenance burden, especially if you don't offer them publicly as part of your API. They also only work for testing incoming calls or property access, you can't use them to test your actual implementations. So while they can work in some situations, they can't cover all of the cases subtyping can.

I'm not sure what you mean with property wrappers or theoretical function wrappers. How would they help you build a mock? I guess they could help you provide a way to see changes to various bits of state, but you'd still have to build the type using the wrappers in the first place. I've never heard of such an approach, could you link to an example?

Who are you quoting here? No one has said that in this thread. Nor has anyone claimed that inheritance is the single best method of testing. I would claim that it does things you simply can't achieve any other way in Swift, but our discussion here has been particularly focused around testing and mocking and alternatives to subclassing.

What I would like to see is an example from someone who prefers inheritance that shows how it is harder or even impossible to test something without inheritance. Without examples it is just a claim that actors would benefit from inheritance in testing.

I think examples should come from both sides of the debate here.

I was/am hoping to see examples/recommendations for testing actors from the core team and co. I’m sure they’re receiving significant feedback from teams actively migrating code.

1 Like

I really think the examples should come from team inheritance since the claim is that it is beneficial for testing.

1 Like

My understanding from Joe's post is that this proposal has been accepted. I think it would make sense to continue this on a new discussion thread. That will give it more visibility among people who are interested in further evolution of actors.

5 Likes