It would be interesting if Swift supported placement allocation of class instances. As long as your API can clearly delineate the lifetime of a component reference, you could use reference semantics directly rather than rebuilding it with UnsafePointer or inout
when necessary.
In the ecs scenario reference semantics arenāt favourable for certain parts of the api, so it needs to be value types and inout so that the api can control in place mutation and perform copies when necessary.
The struct-style allocation of classes could be emulated using a Box class which wraps a typed pointer, and uses dynamic member lookup in combination with key paths to be a kind of transparent wrapper. And the pointer could point into an array of structs, but that would only work in certain situations, and it would be unsafe if used incorrectly.
Itās not necessarily great to pass a gigantic component by value. So you can imagine an ECS API that allowed clients to choose to use a class for heavyweight components. Passing the system update function pointers to continuous memory should be roughly the same in performance to enumerating the component storage and passing the components to the system by value.
Unfortunately using a class currently requires sacrificing the ability to control placement of the component in memory.
Most components arenāt very big in my experience at least (health, position, etc), and mostly you could avoid copying by just using the mutating apis, but if you used a class as a component, any concurrent scheduling that uses information about which components each system needs to mutate. I could relatively easily add a wrapper struct to the library that just wraps a class component, like UnsafeRefComponent or something so that the loss of safety guarantees is clear, in which case Iām perfectly fine with people using classes at their own risk.
Another reason that using structs is better than classes in this situation is that conclusive thread safety for classes requires locking of some sort because you canāt know what other code has access to the class. Whereas with a struct you can control exactly who has write access and when they have write access. Actors hide this but itās still not as good as structs for performance.
And I know with careful programming these issues can be avoided by game devs, but the game im working on has a plugin system and plugin developers canāt be expected to know the ins and outs of swift or the ecs. I like making APIs as foolproof as possible, and with class components this isnāt really possible.
Although, if this feature doesnāt get added, a runtime error using metatypes is good enough given that most components will be used within the first few seconds of connecting to a server, so it would be unlikely for a fatal runtime error relating to the use of class components to go unnoticed.
I think I understand what youāre getting at.
There could be a protocol for exhaustive checking if a motivating example is found, but Iād argue that the separation line shouldnāt be enum or not. Whether something is a struct, class, actor or enum should mostly affect implementation details such as layout and access synchronization rather than that typeās semantics. Immutable properties, for example, can be expressed in protocols without requiring conforming to AnyObject. Actors pose more of an exception to this rule, but in the case of exhaustive checking there are clear examples of exhaustive structs. One of them is Bool, which some users may want to replicate in their own code for bridging with a particular platform or API. In this case, it would make sense to wrap any underlying enum in a struct to only allow initialization with Boolean literals.
Now all this assumes that exhaustive checking is, in fact, something that should be enforced. As @Jumhyn said, one could easily add a default case when switching over an enum, which arguably defeats the whole purpose of creating a protocol. This raises the question of where should this protocol be used as a constraint. Are there potential result-builder APIs that could benefit from such constraints? All in all, exhaustive checking is quite hard to enforce as an API developer, which is why documentation is arguably more important than a compiler diagnostic. Even with an error, if users donāt understand why all cases should be handled, theyāre likely to work around such a loose constraint.
Yes, I agree here. In some cases exhaustive checking will solve the problem.
How can such struct be implemented outside swift compiler in my own code?
That was me alluding to a possible future direction. A hypothetical Bool could look like:
// In the standard library
protocol StaticCaseIterable {
associatedtype AllCases : Collection = [Self] where Self == Self.AllCases.Element
const static let allCases: AllCases { get }
}
// In my library
import WrappedLibrary
struct MyBool: ExpressibleByBooleanLiteral, Equatable, StaticCaseIterable {
let _base: WrappedLibrary.Bool
@_transparent
init(booleanLiteral value: Bool) {
_base = value ? WrappedLibrary.bool_true : WrappedLibrary.bool_false
}
@_transparent
static func == (a: Self, b: Self) { ... }
const static var allCases: [Self] { [true, false] }
}
Are there cases where you think exhaustive checking is not the answer? Also, do you think it would be safe if your API mandated enums but users could easily just add a default case when switching over an enum, or are you looking for a compiler feature to warn against default cases even on enums?
Iām not sure if Iām on the right track here, because the code example you gave earlier in the thread listed AnalyticsEventParam
as a concrete type. But am I understanding correctly that you want your library to allow the library user to define AnalyticsEventParam
and you want your library to enforce that itās an enum?
It sounds like you want users to list all cases exhaustively when processing that type they provide, which is why you want it to be an enum. But that is not something even an enum can provide reliably: users are still free to write a default
case when switching over enums - you canāt force them to do otherwise. And someone else pointed out that certain associated values on enum cases make exhaustive switch cases difficult or impossible anyway.
I think this is just a case of suggesting in your docs that users use an enum for this type. As you wrote yourself, if users want to define this type as something other than an enum, isnāt that their problem?
Enforcing ergonomics of a library is not the role of generic constraints. Enforcing capabilities is. The fact that we have an AnyObject
constraint is not because users might āhold it wrongā otherwise, but because reference types can ādoā something value types cannot. Again, itās about capabilities, not ergonomics. It seems youāre asking for the latter, which just isnāt a thing in Swift. Maybe with that in mind we can steer the discussion to whether thatās a good idea ā personally, Iām not convinced itās necessary.
None of this is arguing against a hypothetical ValueSemantics protocol of course. But I understood the OP is not really trying to make that argument to begin with.
This will be an additional benefit, not goal. As I wrote, enum is used to express strongly Typed key-value pair. Every case has an associated value, cases have associated values of different Types. Is there any ideas how can it be expressed using struct? From my point of view this is not implementation details in this concrete situation, though in many other situations I also think so. implementation detail or not depends on the task ā on generic code we often care about Type of value no matter what nominal type it is, in this case I care about API design and ergonomics. Enum is the only option to express such idea.