Skip to content

Instantly share code, notes, and snippets.

@michelf
Created March 5, 2017 21:05
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save michelf/779b1bc26a778051b6231b5639665e18 to your computer and use it in GitHub Desktop.
Save michelf/779b1bc26a778051b6231b5639665e18 to your computer and use it in GitHub Desktop.
Draft proposal for submodules in Swift.

Really Simple Submodules

Motivation

The goal of this proposal is to provide lightweight intra-module boundaries so you can avoid exposing every internal declaration within a big module to all other files in the module.

Not a goal: addressing the file-level access fileprivate/private or scoped/protected debates. This is left to other proposals.

Summary

This proposal adds the declarations submodule and import submodule. It also limits the visibility of internal to files with a matching submodule or import submodule declaration.

Submodules are never exposed outside of the module. They only change the visibility of internal declarations, so there is no point in exposing them publicly.

Submodules are not bound to directories, nor are they necessarily hierarchical.

This change is purely additive and introduces no source compatibility issue.

Details

A submodule <name> declaration at the beginning of a file contains an identifier with the submodule name:

submodule Foo

internal func foo() {}
public func pub() {}

internal declarations within that file are only visible to other files sharing the same submodule name. The submodule only protects internal declarations: public declarations in the file are visible everywhere (in other submodules and in other modules).

A file can be part of more than one submodule:

submodule Foo
submodule Bar

internal func achoo() {
	foo() // available in Foo (from other file)
}

This makes the internal achoo function visible within both the Foo and Bar submodules. Also note that it makes internal members of both submodules Foo and Bar visible within the file.

A file can access internal declarations of a submodule without having to expose its own internal functions to the submodule with import submodule:

submodule Test
import submodule Foo

internal func test() {
	foo() // internal, imported from Foo
	achoo() // internal, imported from Foo
	pub() // public, so always visible
}

Finally, when a file has no submodule declaration, its internal declarations are visible everywhere in the module and all its submodules:

--- Hello.swift ---
// no submodule declaration
internal func hello() {}

--- World.swift ---
submodule World
internal func test() {
	hello() // visible!
}

Nitpicky Details (Conflicting Declarations)

Declaring internal things that share the same name in two separate submodules is not a conflict:

--- Foo1.swift ---
submodule Foo1
class Foo {} // added to Foo1

--- Foo2.swift ---
submodule Foo2
submodule Foo3
class Foo {} // added to Foo2 and Foo3

(Note: It would be a conflict if one of them was public, because public declarations are always visible everywhere inside (and outside of) the module.)

Attempting to use both from another submodule will create ambiguity errors. You can disambiguate using the submodule name as a prefix:

--- Main.swift ---
import submodule Foo1
import submodule Foo2
import submodule Foo3
let f0 = Foo() // ambiguity error
let f1 = Foo1.Foo() // good
let f2 = Foo2.Foo() // good
let f3 = Foo3.Foo() // good

Best to avoid this for your own sanity however.

Alternatives Considered

Conflicting Declarations

Instead of allowing conflicting symbols in different submodules, we could continue to disallow conflicting internal declarations even when they belong to different submodules. This would make the design simpler, as it is closer to how internal currently works and prevent ambiguity errors from arising when importing multiple submodules. The behavior would be a little surprising however.

We could also simplify by removing the ability to use the submodule name as a prefix to disambiguate. This has the advantage that if you put a type inside of a submodule with the same name, no conflict can arise between the name of the type and the name of the submodule. Disambiguation would have to be done by renaming one of the conflicting declarations. Since this ambiguity can only affect internal declarations (submodules only group internal declarations), requiring a rename will never break any public API. But forcing a rename is not a very elegant solution.

import syntax

Renaming import submodule to import internal. Having "internal" in the name could make it clearer that we are giving access to internal declarations of the submodule. But it also make the import less relatable to the submodule declaration in other files.

Future Directions

Submodule-Private

While a submodule-private access modifier could have been added to this proposal, the belief is that this proposal can live without it, and not having this greatly reduce the little details to explore and thus simplifies the design.

In many cases you can work around this by putting "private" stuff in a separate submodule (somewhat similar to private headers in C land). For instance:

--- Stuff.swift ---
submodule Stuff
submdoule StuffImpl

func pack() { packImpl() }

--- StuffImpl.swift ---
submodule StuffImpl

func packImpl() { ... }

This will not work for stored properties however. A future proposal could suggest allowing stored properties in extensions to help with this.

And a future proposal could also add submodule-private to limit visibility of some declarations to only those files that are part of a specific module.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment