- Proposal: SE-NNNN
- Authors: Robert Widmann, Jaden Geller
- Review Manager: TBD
- Status: Awaiting review
Almost every major programming language supports some form of modular programming through constructs like (sub)modules, packages, or interfaces. Swift, though it provides top-level modules to organize code under, does not provide a complete implementation of any of these concepts, which has led instead to the proliferation of access control levels. This has not proven an effective way to decompose programs into manageable parts, and exposes the need for a real system of modules to solve this modularity problem once and for all.
Separation of code into distinct islands of functionality should be a first-class construct in the language, not dependent on external files and tools or filesystems. To that end, we propose the introduction of a lightweight module system for Swift.
Swift has reached a point in its evolution where rich libraries and large
projects that take on many dependencies have matured significantly. To
accomodate the information-hiding and semantics-signalling needs of these users
at the time, Swift began its access control story with just three access
modifiers: public
, private
, and internal
then grew fileprivate
and
open
as the need to express locality of implementation and "subclassability"
arose respectively. In doing so, Swift's access control scheme has become anti-modular.
We propose the introduction of a lightweight module system for Swift. More than simply namspaces, a module declaration interacts with Swift's access control to provide an API boundary that allows better control over an interface's design.
A module is a named region that introduces a lexical scope into which declarations may be nested. The name of the module can be used to access these member declarations. A module, like other aggregate structures in Swift, may be extended with new declarations over one or more translation units (files).
We propose a new declaration kind, module-decl
be added to the language.
A proposed grammar using the new module
keyword is given below:
GRAMMAR OF A MODULE DECLARATION
module-declaration -> `module` module-identifier module-body
module-name -> identifier
module-body -> { module-members(opt) }
module-members -> module-member module-members(opt)
module-member -> declaration | compiler-control-statement
GRAMMAR OF A DECLARATION
+ declaration -> module-declaration
Syntax and semantics for imports, as it already supports referencing submodules imported from C and Objective-C modules, remains unchanged:
// The outermost module is given explicitly
// by passing `-module-name=Foo` or exists implicitly, as today.
// module Foo {
public class A {}
module Bar {
module Baz {
public class C {}
}
public class B {}
}
let message = "Hello, Wisconsin!"
// } // End declarations added to module Foo.
To consume this interface:
// imports all of Foo, Foo.Bar, and Foo.Bar.Baz
import Foo.Bar.Baz
// imports Foo.A as A
import class Foo.A
// imports Foo.Bar.B as B
import class Foo.Bar.B
// imports Foo.Bar.Baz.C as C
import class Foo.Bar.Baz.C
A module declaration may only appear as a top-level entity or as a member of another module declaration. The following code is therefore invalid:
module Foo {
class Bar {
module Baz {} // error: module declaration cannot be nested inside type 'Bar'
}
}
To extend an existing module declaration, simply
reference its module name in an extension
declaration.
// In module 'Foo'
module Bar {
public class A {}
module Baz {}
}
extension Bar {
public struct B {}
}
extension Bar.Baz {
public enum C { case D }
}
The semantics of some existing access control modifiers shall also be extended to support module declarations:
open
andpublic
declarations are exportable by a module for external consumption by clients of the module.internal
declarations scope over the entire module and any derived submodules and are not exportable for external consumption. They are, however, importable for internal consumption.
By default, to preserve encapsulation of interfaces, modules are "sealed" and
may only be "opened" by explicit named import. However, it is often desirable
to export a module and a set of submodules or even modules from external
dependencies along with a given interface. We propose the public
keyword be
used for this purpose:
// Defines top-level module "Foo"
//module Foo {
public import Foo.Bar.Baz
public import Foundation.Date
//}
Which then causes the following (sub)modules to be imported into scope along with
Foo
:
// imports Foo, Foo.Bar.Baz, and Foundation.Date
import Foo
To support existing Swift packages that cannot have opted into modules, and to
preserve the scriptable nature of Swift, module declarations shall be optional. Any
Swift program that does not declare at least one top-level module explicitly is considered part
of an unnamed special "Global Module" with the same rules of access control as
today. To give declarations in the Global Module an explicit module without
using a module declaration, use the -module-name
flag.
This proposal is intentionally additive. There is no impact on existing code.
Declarations in the top-level of a program exist today in the top-level of the corresponding module. If desired, this module declaration could be required to be explicit like so:
module Foo {
module Bar {
module Baz {}
}
}
However, we feel that imposing such a requirement not only complicates the
outermost scope, it requires inserting needless extension Foo {}
scopes in
every file. It also violates the principle of progressive disclosure by forcing
all new adoptees of Swift to learn what a module is without actually using the
module system.
Instead of declaring the submodule once and extending it elsewhere, we could allow a submodule to be "declared" multiple times, merging the contents of each. This would avoid syntactically privileging the initial declaration, but would make it possible to accidently reuse a submodule name. Though this syntax would feel more similiar to C++ namespaces, it would significantly differ from Swift's existing syntax for defining and extending named scopes.
module Foo {
func bar() { }
}
module Foo { // an "extension"
func baz() { }
}
Nested module extensions may be "expanded", as it were, to the following:
module Foo {
module Bar {}
}
extension Foo {
extension Bar {}
}
However, this syntax is currently not enabled in general in Swift. This problem should be revisted in a future proposal.
The system described above is intended to be entirely source and binary compatible. Nonetheless, in its design we feel we have obviated certain existing features and recommend their deprecation in future proposals:
fileprivate
access can be recreated by creating a private "utility submodule" containing declarations of at leastinternal
access.@_exported
, the private directive to re-export modules today, should be deprecated and removed.