> On 24 Feb 2017, at 20:34, Matthew Johnson via swift-evolution > <swift-evolution@swift.org> wrote: > > I didn't expect submodules to be a part of the Swift 4 discussion. When it > came up I was pleasantly surprised. I have been thinking about the design of > a submodule system for quite a while but was planning to wait until it was > clearly in scope to draft a proposal. Now that the topic has been introduced > I decide to write down the design I've been thinking about, along with the > motivation and goals that underly it. I understand submodules may not be in > scope for Swift 4 but wanted to contribute this design while the discussion > is fresh in everyone's mind. > > I am including the contents of the proposal below. You can also find it on > Github: > https://github.com/anandabits/swift-evolution/blob/scope-based-submodules/proposals/NNNN-scope-based-submodules.md > > <https://github.com/anandabits/swift-evolution/blob/scope-based-submodules/proposals/NNNN-scope-based-submodules.md> > > I am very much looking forward to everyone's feedback! > > Matthew > Scope-based submodules > > Proposal: SE-NNNN > <file:///Users/Matthew/Dropbox/Matthew/Development/__notes/__swift/evolution/NNNN-scope-based-submodules.md> > Authors: Matthew Johnson <https://github.com/anandabits> > Review Manager: TBD > Status: Awaiting review > Introduction > > This proposal describes a submodule system based on the principle of strictly > nested scopes. The design strives to follow the Swift philosophy of offering > good defaults while progressively disclosing very powerful tools that can be > used by experts to solve complex problems. > > Motivation > > Swift currently provides two kinds of entities that provide system boundaries > without directly introducing a symbol: modules and files*. > > Modules introduce an ABI boundary, a name boundary, and a scope boundary. > Files introduce a scope boundary that carries no additional semantics. > Swift currently lacks the ability to introduce a name and scope boundary > without also introducing an ABI boundary. Such a boundary would be naturally > situated halfway between in terms of strength and ceremony. The lack of such > a boundary significantly limits our abiltiy to structure a large Swift > program. Introducing a way to form this kind of boundary will provide a > powerful tool for giving internal structure to a module. > > *The important aspect of a file in Swift is the logical scope boundary it > introduces. The physical file system representation is incidental to this. > The appendix on file system independence discusses this in more detail. > > Goals > > Any discussion of submodules inevitably reveals that there are very different > perspectives of what a submodule is, what problems submodules should be able > to solve, etc. This section describes the design goals of this proposal in > order to facilitate evaluation of both the goals themselves as well as how > well the solution accomplishes those goals. > > The primary goal of this proposal are to introduce a unit of encapsulation > within a module that is larger than a file as a means of adding explicit > structure to a large program. All other goals are subordinate to this goal > and should be considered in light of it. > > Some other goals of this proposal are: > > Submodules should help us to manage and understand the internal dependencies > of a large, complex system. > Submodules should be able to collaborate with peer submodules without > necessarily being exposed to the rest of the module. > A module should not be required to expose its internal submodule structure to > users when symbols are exported. > It should be possible to extract a submodule from existing code with minimal > friction. The only difficulty should be breaking any circular dependencies. > Some additional non-functional requirements for the solution are: > > Submodules should not negatively impact runtime performance. WMO should be > able to see across submodule boundaries. > Submodules should not negatively impact build performance. Ideally they will > improve build performance by giving the compiler more high-level information > about internal dependencies. > Deferred goal: > > It is not an immediate goal to support submodules in single-file scripts. The > appendix discussing file system independence discusses some ideas that could > be used to support single-file scripts in the future. > Proposed solution > > There are several relatively orthogonal aspects to the design of a submodule > system. A design must answer the following questions: > > How is code placed in a submodule? > How are symbols in one submodule made available to another submodule or > module? > How do submodules interact with access control? > This proposal diverages a little bit from the usual proposal format to > faciliate discussion of alternatives within the context of each aspect of the > chosen design. In each case an alternative could be substituted without > compromising the overall design. > > Note: This proposal uses the term “top-level submodule” to distinguish the > scope of code that is not explicitly placed in a submodule from the module as > a whole. The top-level submodule is in most respects identical to any other > parent submodule. There are two differences: 1) it is the only submodule that > can export symbols outside of the module and 2) any open or public symbols it > declares are automatically exported according to their access modifier. > > Placing code in a submodule > > Each file is part of the top level submodule by default. > > A submodule declaration may be used at the top of a file: > > Only the first non-comment, non-whitespace line may contain a submodule > declaration. > The submodule decalaration looks like this: submodule MySubmoduleName > A submodule declaration may not be prefixed with the module name. > Submodule names form a hierarchical path: > > The fully qualified name of the submodule specified by > Submodule.InnerSubmodule is: MyModuleName.Submodule.InnerSubmodule. > In this example, InnerSubmodule is a child of Submodule. > A submodule may not have the same name as any of its ancestors. This follows > the rule used by types. > Submodules may not be extended. They form strictly nested scopes. > > The only way to place code in a submodule is with a submodule declaration at > the top of a file. > All code in a file exists in a single submodule. > A module is made up of strictly nested scoped that look like this: > > <scope-based-submodules.png> > The hierarchy of nested scopes in scope-based submodules > Alternatives > > Grouping mechanisms > > There are several other ways to specify which submodule the top-level scope > of a file is in. All of these alternatives share a crucial problem: you can’t > tell what submodule your code is in by looking at the file. > > The alternatives are: > > Use a manifest file. This would be painful to maintain. > Use file system paths. This is too tightly coupled to physical organization. > Appendix A discusses file system independence in more detail. > Leave this up to the build system. This makes it more difficult for a module > to support multiple build systems. > Require all files to include a submodule declaration > > We could require all files to include an explicit submodule declaration. > However, that would be a breaking change and would violate the principle of > progressive disclosure. Users should not need to know about submodules if > they don’t use them. > > Allow submodule references to explicitly state the name of the module > > The module name is implicit throughout the scope of the entire module. > Specifying it explicitly is redundant. Prohibiting explicit mention of the > module name offers more flexibility for combining submodules into build > products. > > Visibility of submodules > > The export statement is used to modifiy the visibiltiy of a submodule within > the module. It is also used by the top-level module to publish submodules to > clients of the module. > > All submodules are implicitly exported with module-wide visibility by default > (and hidden outside of the module by default*). > All submodules are implicitly available for export outside the module. > A submodule may use an explicit export statement to modify the visibility of > a descendent submodule. I don’t see the need for modifying the visibility of descendent modules. Can you give me an idea why its important enough to warrant all that extra syntax? > export statements are only allowed at the top level of a file. > *The exception to this is that open and public symbols in the top-level > submodule are always exported exactly as declared. > > Top-level export > > All export statements consist of an access modifier, the export keyword, and > a submodule name: > > open export ChildSubmodule > > When this export statement appears in the top-level submodule, ChildSubmodule > becomes available for import by clients of the submodule with the fully > qualified name Module.ChildSubmodule. public exports are also available. When > public is used all published symbols in the exported submodule have a maximum > visiblity of publicregardless of how they were declared. > > Top-level public and open export statement may be modified with the following > options: > > A submodule may be published under a different external name using the export > as NewName syntax*. > @implicit causes symbols from the submodule to be implicitly imported when > the module is imported. > @inline causes the symbols from the submodule to appear as if they had been > declared directly within the top-level submodule. > @implicit may be combined with renaming but @inline may not appear along with > either of them. > *When a submodule is renamed for export with the as clause its internal name > does not change. A submodule always has the same fully qualfied name > everywhere within its module. > > Here are some example export statements: > > // All symbols in `Child1` are available for import by clients as `Module.Foo` > // These symbols are *not* imported automatically when a client imports the > module with `import Module` > public export Child1 as Foo > > // All symbols in `Child2` are available for explicit import by clients as > `Module.Child2` > // The symbols are also automatically imported when a client imports the > module with `import Module` > // When the symbols are imported implicitly they retain the fully qualified > name prefix of `Module.Child2` > @implicit open export Child2 > > // All symbols in `Child3` are available for explicit import by clients as > `Module.Foo.Bar` > // The symbols are also automatically imported when a client imports the > module with `import Module` > // When the symbols are imported implicitly they retain the fully qualified > name prefix of `Module.Foo.Bar` > @implicit open export Child3 as Foo.Bar > > // All symbols in `Child4.Grandchild` appear to clients as if they had been > declared > // directly in the top-level submodule. > // If the process of inlining the symbols produces duplicate symbols a > compiler error is produced > // at the site of one or both of the `export` statements. > @inline public export Child4.Grandchild > > // All symbols in `Child5.Grandchild` are available for explicit import by > clients as `Module.Foo` > // along with the symbols declared in `Child`. > // The symbols are also automatically imported when a client imports the > module with `import Module` > // When the symbols are imported implicitly they retain the fully qualified > name prefix of `Module.Foo` > // As with `@inline`, when two submodules are given the same external name a > duplicate symbol error may occur. > @implicit public export Child5.Grandchild as Foo > One interesting observation is that both Child1 and Child5.Grandchild are > renamed to Foo. The symbols declared former is not implicitly imported by > import Module but the latter is, despite having the same fully qualified name > prefix. > > Exports within the module > > A submodule may bound the maximum visibility of any of its descendent > submodules by explicitly exporting it: > > // `Child1.Grandchild` may be exported by the top-level module, but only with > `public` visibility. > // `Child1.Grandchild` may not be exported as `open`. > public export Child1.Grandchild > > // `Child2` is exported with `internal` visibility. > // Because `internal` is scoped to the submodule level *only* the parent > submodule can see `Child2`. > // No submodules except the direct parent of `Child2` (the current submodule) > are allowed to `import Child2`. > // This also implies that the `Child2` may not be exported to clients because > the top-level > // submodule is not able to see or reference `Child2` at all. > internal export Child2 > The access modifier may specify a scope internal or greater. > Only the direct parent of a submodule may specify the internal modifier. A > grandparent cannot hide a grandchild from its parent. > If a descendent includes an export statement for the same submodule, the > access modifier must be no greater than the access modifier specified by the > descendent. An ancestor may provide a tighter bound to visibility but may not > increase visibility. An attempt to increase visibility results in an error. > Note: If a submodule is not visible none of its descendents is visible either. > > Alternatives > > Use import access modifiers to export a submodule > > The semantics of placing a bound on the visibility of a descendent submodule > is significantly different than the semantics of importing symbols from a > submodule into the current lexical scope. Mixing the semantics of the two is > confusing. > > Restrict the visibility of a submodule to its parent unless the parent > explicitly exports it. > > Users should be able to use submodules without needing to place export > statements in every parent submodule. Module-wide default visibility for > submodules is analagous to internal default visibility for symbols. > > Require all submodules to be visible module-wide. > > This removes an important tool for bounded collaboration within a complex > system. A parent submodule should be allowed to have a child submodule(s) > which are implementation details of the parent and not exposed to the rest of > the module. > IMHO, the implementation details of a submodule should live in that submodule, not in a child submodule. Shouldn’t the main purpose of submodules be to help organise publicly available code into name-spaces, and not add to add scoping mechanisms for implementation?
For example, looking at C#’s System.Xml library, the namespaces clearly help to separate code, like serialisation classes from more mundane Xml reading classes, etc... System.Xml System.Xml.Linq System.Xml.Resolvers System.Xml.Schema System.Xml.Serialization System.Xml.Serialization.Advanced System.Xml.Serialization.Configuration System.Xml.XmlConfiguration System.Xml.XPath System.Xml.Xsl System.Xml.Xsl.Runtime > Allow renaming to be used by export statements within the module. > > A submodule should have the same fully qualified name everywhere it is used > within a single module, whether that be the declaring module or a client > module. The declaring module and client modules may see different names, but > each sees a name that is consistent fully qualified name everywhere the > submodule is referenced. > > Allow @inline to be used by export statements within the module. > > As with renaming, a symbol should have a single fully qualified name > everywhere within a single module. > > Allow @implicit to be used by export statements within the module. > > This would reduce the visibility of internal dependencies. If we find that > import-per-submodule becomes boilerplate-y this is an easy feature to add > later. > > Importing submodules > > Submodules are imported in exactly the same way as an external module by > using an import statement. There are a few additional details that are not > applicable for external modules: > > Circular imports are not allowed. > A submodule may not import any of its ancestors. > Relative child and sibling names are allowed using the same rules that apply > to nested types. > Access control > > An access modifier applies an upper bound to the scope in which a symbol or > submodule is visible. With the introduction of submodules, internal now > applies at the level of a submodule: only the current submodule may see an > internal entity. We need a new way to specify module-wide visibility. > > This proposal builds on Option 2 in the proposal Fix Private Access Levels > which reverts private to the Swift 2 meaning (equivalent to fileprivate) and > uses scoped for the Swift 3 scoped access feature. It does this by allowing > the scoped access modifier to be parameterized with a scope reference. By > defaults it references the scope in which it appears, but any ancestor scope > may be specified as a parameter. > > The paremeterization of the scoped access modifier provides a simple yet > powerful way for a submodule to bound the visibility of a descendent. > > Some examples of using scoped exports are: > > submodule Parent > > // `Grandparent` and all of its descendents can see `Child1` (fully > qualified: `Grandparent.Parent.Child1`) > // This reads: `Child1` is scoped to `Grandparent`. > scoped(Grandparent) export Child1 > > // `Child2` is visible throughout the module but may not be exported for use > by clients. > // This reads: `Child2` is scoped to the module. > scoped(module) export Child2 > With parameterization, scoped has the power to specify all access levels that > Swift has today: > > `scoped` == `private` (Swift 3) > `scoped(file)` == `private` (Swift 2 & 4?) == > `fileprivate` (Swift 3) > `scoped(submodule)` == `internal` > `scoped(public) scoped(internal, inherit)`* == `public` > `scoped(public)` == `open` > This design is a direct generalization of the principle underlying Swift’s > existing access control system. It unifies the semantics of the system under > the single elegant mechanism of ancestor scope references. > > While it is possible to specify all access levels using scoped that is not > recommended. The aliases public, private(Swift 2) and internal provide > excellent default access levels that don’t require a user to think about > scope hierarchies. Using the default access levels when possible calls extra > attention to cases where a different choice was made. > > *This is a conceptual model. This proposal does not introduce the inherit or > override parameter to access modifiers. It could be added in the future as a > way to bound inheritance within a module. It would work similarly to > private(set) does in Swift today. > Like we’ve had time to discuss, I’m really not sold on this solution as it adds a fair amount of complexity, and complexity is exactly what people have been complaining about in the current model. The complexity I’m not sure we need: new keyword more than 1 way to express the same semantics the parametrisation of the keyword > Aside > > The parameterization of scoped also allows us to reference other scopes that > we cannot in today’s system, specifically extensions: scoped(extension) and > outer types: scoped(TypeName). > > Alternatives > > If we don’t adopt the approach of parameterizing scoped our options for > access control include: > > Submodules are only allowed to see public and open symbols from other > submodules > > A module-wide scope is highly desirable. People might avoid using submodules > if this is not available. > > > This approach also creates a lot more friction when refactoring. A possible > workaround to the lack of a module-wide scope in this system is to place code > in a non-exported submodule and declare symbols public. Even with the > workaround, extracting a submodule may not always be possible or desirable > and the public access modifiers required would be misleading. It would be > much better to be able to state our intent directly. > > Use internal to cover the whole module and private to cover a submodule > > One suggestion that has appeared is the idea of removing fileprivate and > making private be submodule-wide. internal would remain module-wide. This is > too coarse - many people want a file-level scope. > I agree that this is a bad idea. > internal is Swift’s default access modifier. A symbol with default access > modifier should not be able to cross a submodule boundary implicitly. > > Add the moduleinternal access modifier > > This is about as ugly as fileprivate. > What about the alternative of not having a submodule scope at all? Submodules would see all internal access in all submodules, as long as it’s imported at the top of the file. I think the divide comes from the fact that I ant “namespaces” as a name grouping mechanism and you want “submodules” as a scope grouping mechanism. > Detailed design > > Export errors > > Multiple exports of the same submodule > > If a submodule exports the same descendent more than once and the semantics > of the declarations are not identical an error is produced. > > Symbol flattening > > When a submodule is exported by the top-level module using the @inline > attribute it is possible that there will be conflicting symbol definitions in > the child and the top-level submodule (or other inlined submodules). This > results in a compiler error at the site of the conflicing @inline export > statements. > > Overlapping renames > > As with flattening, when two or more submodules are given the same external > name symbol conflicts are possible. This also results in a compiler error at > the site of the conflicting export as statements. > > Access errors during export if the specified access modifier exceeds maximum > > An error is produced when an export statement includes an access modifier > greater than the bound provided for the exported submodule by a descendent of > the exporting submodule. > > Source compatibility > > This proposal is purely additive. That said, it would be a breaking change > for the standard library to move existing API into an externally visible > submodule. > > Effect on ABI stability > > This proposal is purely additive. That said, it would be a breaking change > for the standard library to move existing API into an externally visible > submodule. > > Effect on API resilience > > This proposal is purely additive. > > Future directions > > Selective export and import > > The ability to import and export individual symbols would be a very nice to > have. > > Scoped import > > The ability to import modules, submodules, and symbols into any lexical scope > would be nice to have. > > Bounded inheritance > > It could be useful to have the ability to bound inheritance within a module. > This could be accomplished by inroducing inherit and override parameters for > access modifiers (which would work similarly to the existing set parameter). > > Appendix A: file system independence > > The submodule design specified by this proposal is file system independent. > The only relationship it has with the physical file system is that a file > introduces an anonymous scope boundary which is referenced by scoped(file) or > fileprivate (Swift 3) or private (Swift 2 and 4?). > > The logical role of a “file” in this design is to provide a boundary of > encapsulation that is even lighter weight than a submodule: it doesn’t hide > names. All declarations are implicitly available not only within the file but > also across the file boundary (modulo access control). Files are to > submodules as submodules are to modules. > > If a future version of Swift were to eliminate files in favor of some kind of > code browser it would still be very useful to have the ability to form a pure > scope boundary (with no additional semantics). A scope declaration could be > used to do this. Scope declarations could have an optional (or required) name > and could even be nested. In this system privatewould reference the nearest > anonymous ancestor scope declaration (or would be removed if we don’t allow > anonymous scopes). > > The logical structure of this design can be directly translated into a > grammar that could be represented directly with syntax. Such a grammer could > be used to support scripts with submodules. An example follows: > > // A module contans a single implicit, anonymous submodule. > // submodule { > // A submodule may contain `scope` declarations (i.e. files) as well as > other submodules. > // An anonymous scope is equivalent to a file in current Swift. > // If we introduce lexical scopes we would probably require them to be > named explicitly. > // This example uses the anonymous scope in order to most closely match the > role files play in the current system. > // Because `scope` does not provide a name boundary all names declared in > one scope > // are visible in other scopes (modulo access control) > scope { > // Top-level declarations go here. > // This is equivalent to the top level of a file in Swift today. > // It is also equivalent to the top level of a file that does not > contain > // a `submodule` declaration in this proposal. > > // It would be possible to allow nested, named scopes. > // A scope name participates in the scope and name hierachies. > // However, it does not form a name boundary like a submodule does. > scope Named { > // This declares the static variable `Named.foo` > // `scoped(file)` references the nearest anonymous ancestor scope. > // It is used in this example for specificity. > // Real code would use the alias `private` or `fileprivate` > // If we introduce explicit scope syntax we would probably want a > better name to refer > // to the nearest anonymous scope than `file` or we may just require > all scopes to have a name. > scoped(file) var foo: String > } > // `Named.foo` is visible here > } > // `Named.foo` is not visible here. > > submodule Foo {} > submodule Baz {} > submodule Buzz { > // Equivalient to a file in current Swift. > scope { > // submodule declarations go here. > // This is equivalent to the top level scope of a file that contains > the `submodule Foo` declaration. > } > scope {} > submodule Baz {} > } > //} > Appendix B: namespace style submodules > > It is possible to design a system that allows a name boundary to be formed > without also forming a scope boundary. A natural consequence of this is that > symbols may be placed into a namespace-style submodule in many (unlimited) > scopes via extension (even extension outside the module is theoretically > possible). Allowing this runs contray to both of the two primary goals of > this proposal (encapsulation and structure). > > Allowing a submodule to be extended in multiple scopes precludes the > possibility of submodule internal visibility. A submodule internal access > modifier could still be defined but it would not provide the guarantee it > purports to. The submodule can be opened by extension anywhere within the > module. If a lazy developer wants to access a submodule internal symbol from > a distant subsytem all they need to do is add an extension and wrap the > submodule internal symbol with a new symbol offering higher visibility*. In > such a system there is the same wide gap between file scope and module scope > that exists today. > > Allowing a submodule to be extended in multiple scopes precludes the ability > to introduce real structure to a module. We are able to introduce structure > to the names but not the module itself. The structure of a submodule in such > a system may be widely dispersed throughout the module. It is not part of a > strictly hierarchical structure of scopes which each having a single > designated location within the larger structure. > > What you do get from name boundaries that do not also form a scope boundary > is a soft form of symbol hiding (soft because all submodules are available > for import or extension anywhere within the program). This does provide some > value, but not nearly as much value as is provided by a name boundary that is > accompanied by a scope boundary. > > Another downside to namespace-style submodules that are open to extension is > that they are much less likely to facilitate improved build performance > because they don’t add any physical structure to the system. > > Finally, lexical submodules have the potential to be confusing. If submodules > form a name boundary (even a soft one) an import statement is required to > access the symbols declared inside a submodule. Is code that surrounds a > lexical submodule declaration able to see the symbols it declares without > importing them? Most developers will expect the symbols to be available. It > is probably necessary to make an exception to name boundary for the > surrounding lexical context. However, if an exception is made then this > system relies heavily on a file to provide a bound to the implicit symbol > import. > > *It is worth observing that the ability to violate encapsulation via > extension (or subclassing) is one of the primary reasons Swift does not offer > type-based access modifiers such as typeprivate or protected. The do not > offer true encapsulation at all. They are a statement of intent that cannot > really be verified in the way that is desired. They form a permeable rather > than a hard boundary. > > _______________________________________________ > swift-evolution mailing list > swift-evolution@swift.org > https://lists.swift.org/mailman/listinfo/swift-evolution
_______________________________________________ swift-evolution mailing list swift-evolution@swift.org https://lists.swift.org/mailman/listinfo/swift-evolution