Skip to content

Instantly share code, notes, and snippets.

@CodaFi
Last active October 12, 2023 00:03
Show Gist options
  • Star 9 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save CodaFi/cd66b7d70b5cd8e4e8b433fa2ace378a to your computer and use it in GitHub Desktop.
Save CodaFi/cd66b7d70b5cd8e4e8b433fa2ace378a to your computer and use it in GitHub Desktop.

Modular Swift

Introduction

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-evolution thread

Motivation

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.

Proposed solution

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.

Detailed design

Syntax

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

General Semantics

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 }
}

Modules and Access Control

The semantics of some existing access control modifiers shall also be extended to support module declarations:

  • open and public 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.

Impact on Existing Code

This proposal is intentionally additive. There is no impact on existing code.

Alternatives considered

Explicit Modules Everywhere

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.

Multiple Declarations Instead of Extensions

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() { }
}

Future Directions

Nested Extensions

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.

Deprecations (Source-Breaking Changes)

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 least internal access.
  • @_exported, the private directive to re-export modules today, should be deprecated and removed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment