> On 24 Feb 2017, at 20:34, Matthew Johnson via swift-evolution
> <[email protected]> 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
> [email protected]
> https://lists.swift.org/mailman/listinfo/swift-evolution
_______________________________________________
swift-evolution mailing list
[email protected]
https://lists.swift.org/mailman/listinfo/swift-evolution