SE-0306 (Second Review): Actors

AnyObject is a special kind of non-protocol constraint. Other non-class types (such as ObjC block types and class metatypes) also satisfy the AnyObject requirement. AnyObject will not every have any requirements, and you cannot write an extension on AnyObject.

Actor is a much more of a normal protocol. It will have requirements when we get custom executors. One can write an extension on it. And beyond the fact that we actor-isolate methods on the Actor protocol by default, it's not really all that different from other protocols for which Swift can introduce or synthesize a conformance. We aren't trying to rename Sendable to AnySendable because structs and enums can implicitly conform to it.

It may even be that we find a reasonable way to allow classes to explicitly conform to Actor in the future, if they satisfy the requirements appropriately. That capability was removed because we're not sure exactly what it should do, not because it's fundamentally impossible to do.

AnyObject is the odd protocol out here. Usually Any means "type-erased", and we should consider that the stronger precedent to follow.

Doug

14 Likes

If we had a chance to do it all over again, AnyObject should probably have been named Object (and Any could be Value or something else more generic), especially if we eventually grow an any type modifier for explicit type erasure, which has been coming up frequently in the SE-309 review thread.

10 Likes

Is it not even an option to deprecate them and adopt the right names?

typealias AnyObject = Object
3 Likes

I don't think it's worth it to churn the names yet again. I also don't think they're good precedent for making further naming choices.

1 Like

That aside, there are no technical constraints that would prevent this from ever happening? So it‘s more like only a decision then? I just want to understand it better as I cannot tell it from the compilers perspective.

1 Like

This makes sense to me, it sounds like AnyObject is the one that is misnamed, which is a different issue. Actor SGTM as the protocol name.

-Chris

5 Likes

Review is going to close soon, and I am very much concerned about how complicated testing will be without having some form of inheritance. I hope someone can provide their thoughts there.

Inheritance is not necessary for testing. Using inheritance for creating mocks is convenient but error prone if the implementation of the mocked class changes. Since mocking should eliminate unexpected behavior, I think protocols are the right tool for the job.

3 Likes

The intent is not to use mocks to test behavior of what's mocked, but to use mocks to have control points in a test harness to test everything in between, which I think is what you're additionally implying with protocols too. If you use a protocol, you're still going to have to adjust the protocol when the mocked class changes since the protocol's API must correctly reflect the inheriting actor's or class' API.

In the use case I'm referring to there are 2 issues with using protocols.

  1. There are generally 1 and only 1 class per job in the dependency chain of stateful objects in the application. Therefore, writing a protocol per class is just redundant (assuming you have inheritance available).
  2. A user doesn't have control over the API of 3rd party frameworks which often use classes for their core interface. Again, I suppose you could write protocols over those libraries, but writing a set of protocols to wrap over the framework's class structures is redundant work.

Whether or not it's necessary, using a protocol in this case is additional work and an additional maintenance burden for little if any gain.

3 Likes

The problem I see with using protocols is you are forced to implement all the requirements of that protocol for each test harness even if you don't need it. You can't just override the thing you care about in the test.

2 Likes

Your concern is backwards. You want subtype mocks to pick up implementation changes so that your tests change as your implementation changes. This is something that protocols can't provide, as you can't override implementations while calling the original implementation, and you have to match the interface every time your concrete implementation changes.

3 Likes

I would argue that it is the other way around - when the API changes (which is defined by the protocol) the inheriting actor/class needs adjustment. You get both at the same time when using inheritance, but that is in my opinion exactly the problem. You can not cleanly separate the API and the Type.

No, there are at least two: the real thing and the mock.

You just add the needed API to your protocol. It does not matter if the framework is using classes or not, you define the abstraction. Again I would argue it is the other way around: using inheritance for mocking is only possible for classes, protocols are always possible.

1 Like

But you decide what the requirements are. There is no need the mock the whole interface of a class if you just use 3 methods.

Could you provide a small example? I don't understand what you mean by pick up implementation changes.

I mean as a reply this:

My point was that it's not error prone since picking up implementation changes is exactly what you want for your tests. If you change the type, you want any subtype mocks to pick up the changes automatically. If you change a method such that your mock no longer overrides something, the compiler catches it. This is actually more risky using protocols, since you can easily forget to update the protocol requirements and there's no compiler enforcement.

Simple fact of the matter is that there's no 100% replacement for subclassing in Swift. Protocols don't have the same behaviors and have more limitations. So everything that depends on such capabilities will have to be redesigned not to use it.

when the API changes (which is defined by the protocol) the inheriting actor/class needs adjustment.

I don't always have control over the API, it could be be from another team or another library. The protocol must conform to the API, which may be a class, generally, in this use case.

No, there are at least two: the real thing and the mock.

The mock isn't a part of the application. However, following from this logic, including a protocol now means there are 3 things.

using inheritance for mocking is only possible for classes, protocols are always possible.

Yet, other types are not reference types, they don't fall under this use case so they won't need mocking anyway for their tests. Reference types (at least in this use case) are needed for stateful systems that generally interact with the external world. The mocks provide control points to stub out what the external world may look like or to lock in some state and therefore provide a test harness.

But why should I care about the implementation of a dependency of the unit under test? It is about the API, not the implementation. Otherwise you have coupled you unit under test to the implementation of the dependency and that is always bad. Actually, that is exactly the problem.

As I mentioned before, it should be the other way around. Dependencies should be defined only by their interface, not how they are implemented.

I agree, and that is a good thing imo.

That depends entirely on which side of the unit you're talking about. But in any case, your opinions about this usage aren't an argument for restricting this capability, nor are they particularly relevant here. Nor do you need to keep defending the point, as you've already won. Reviewers are still expressing their opinions about the model, it's restrictions, and how it impacts their use. If you don't support inheritance, simply accept there's no replacement for it and move on rather than trying to convince everyone currently using it that they're holding it wrong.

Yes, and that is a good thing imo.

I don't see how only reference types are relevant for stateful systems? My argument is that if you use inheritance for mocking then you always also need protocols for mocking since only classes support inheritance.

That is what I did, and that is the whole point of a forum like this.