Edit: The idea of explicit memberwise initializers is great. Before I start babbling my thoughts I want to clarify that I did not follow or read the previous threads of this discussion.
The pitched design is quite intuitive, but it has its quirks. When someone new to the language reads (internal...)
in an intializer I believe that lots of question will come up:
- What is this?
- Why is there an access modifier?
- Why are there trailing
...
?
Personally I think only the first question really matters when you discover this feature, but I also think that the packs should not be that cryptic. I would like you to consider an alternative design that might be a little verbose, but it tries to solve a few more problems by also preserving better discoverability, consistency with other language features and potential re-usage in the future.
The direction of this idea is leaned towards the things the pitch already has mentioned:
- Prefix implicit property packs with
#
- Different semantics for the implicit property packs
But I would like to take the change and expand or change these things a little.
- We should use a
#
prefixed construct for example #colorLiteral
.
- If possible we should not use access modifiers directly as this creates more confusion and does not tell the reader what this construct possible mean.
- For the simplicity let's call this
#member
(we can debate on the name later).
- Such
#member(...)
construct contains either a pattern, a range or a combination of both things.
-
#member
always describes an exact set of access modifiers.
- A pattern can be composed with
|
or &
to express concatenation or an OR like extraction of members.
#member(private) // will extract only all `private` members from top to bottom
#member(public & private) // will extract first all `public` then `private` members
#member(private & public) // will extract first all `private` then `public` members
#member(private | public) // will traverse from top to bottom an pick any `private` or `public` members
#member(public | private) // same meaning
- A range can be expressed by using two access levels with an
...
infix operator in between them (only closed ranges should be supported). The order of the access levels does not matter as a range is a simplification of a composition of multiple access levels using |
.
// assuming this order:
// private - fileprivate - internal - public - open
#member(private | fileprivate | internal | public) // explained above
#member(private ... public) // same meaning as the example one line above it
#member(public ... private) // order does not matter - same meaning
- We could also extend the pattern to allow explicit inclusion of members regardless their access level, which also allows us to provide an alternative custom label (the original pitch is already doing that).
// * picks everything from top to bottom that is private ... public
// and overrides label of `someMember`
// * if `someMember` is not in that range, this is an error
// * if `someMember` is for example `open` the compiler could provide
// a fix-it and suggest using `&` instead of `|`
#member(private ... public | (label someMember))
The extraction of the members prevents generating duplicate entries. That means that if a previous pack or pattern extracted some specific member from the ordered set of members, it won't be added with the next pack expansion that overlaps. This should simplify things as it does not need to check for overlapping patterns.
You should be able to create any possible pattern with this approach with a little trade-of in pattern definition.
// Attention, this is a COMPLEX pattern and not the common case.
public init(
#member(public),
label argument: Type,
#member(private & (fileprivate | internal))
)
// Assume there is a non-public `argument` member.
// Then we can define the same pattern differently.
public init(#member(public & (label argument) & private & (fileprivate | internal)))
It's likely that in most common cases the user will only need to define ranges such as #member(private ... public)
. We could go one step deeper and use open ranges to reduce the verbosity.
#member(private...) // we just re-invented what the proposal had as `private...`
#member(private ... open) // basically the same
#member(...internal) // different direction
#member(private ... internal) // same
Overall I think we should cover this feature into a #
prefixed construct as it is most likely extendable in the future if would decide to allow it for methods. Then we could add extra constraints:
#member(...public, [.computed, .stored])
To signal the feature more obviously I thin we could call it #property
.