Hi everyone! I'm excited to announce that I'll be working on [SR-15410] Can't document extensions with DocC · Issue #210 · apple/swift-docc · GitHub!
Since most of you probably haven't seen my name tag before: My name's Max, I'm studying informatics in Munich, Germany and I'm very excited to be contributing to the Swift ecosystem!
The question about DocC and how to document extensions to external types originated in the forum thread DocC and extensions? and it quickly became apparent that there are some important UX questions to be solved. Therefore I thought I'd kick of my contribution efforts with a pitch focused on the UX of this new feature.
First off, I want to thank everyone who participated in the forums post linked above, you'll probably find some of your ideas in my concept! Special thanks to @franklin for guiding me trough this process and his initial feedback on this pitch!
Introduction
DocC does not include extensions to a type defined in an external module in the documentation catalogue, even though the extension and its contents are declared in the documented module.
Consider the following potential addition to the SlothCreator package:
/// A type that generates sloths.
public protocol SlothGenerator {
/// Generates a sloth in the specified habitat.
func generateSloth(in habitat: Habitat) throws -> Sloth
}
public extension Collection where Element == Habitat {
/// Generates one ``Sloth`` per ``Habitat`` in the collection.
///
/// - Note: Unfortunately, neither this comment nor the function itself is included in
/// the documentation catalogue yet.
func mapToSloth(using generator: SlothGenerator) throws -> [Sloth] {
try self.map(generator.generateSloth(in:))
}
}
/// A type that generates names for sloths.
public protocol NameGenerator {
/// Generates a name for a sloth.
///
/// - parameter seed: A value that influences randomness.
func generateName(seed: Int) -> String
}
/// An array of strings conforms to ``NameGenerator``. Each time
/// ``generateName(seed:)`` is called, it returns the element identified
/// by the given seed.
extension Array: NameGenerator where Element == String {
public func generateName(seed: Int) -> String {
self[seed % self.count]
}
}
Neither of the two extensions is added to the documentation catalogue. That is, both Collection/mapToSloth(using:)
and Array/generateName(seed:)
are not listed. Furthermore, they cannot be referenced using their identifiers and therefore it is also not possible to include them in the documentation catalogue using manual curation. Finally, Array
(with Element == String
) is also not listed among the Conforming Types of NameGenerator
.
Motivation
Swift encourages us to use extensions on external types and therefore we should also be able to document such extensions accordingly. As I already mentioned, this capability has also been requested and discussed on the Swift forums before.
While there are possibly infinite use cases, I think that especially the growing ecosystem of SwiftUI packages could benefit hugely from this addition. A large part of their public API surface may be made up of view modifiers, which are usually exposed as function-extensions on SwiftUI's View
type.
Proposed Solution
We extend the default top level sections in the sidebar (e.g. Structures
, Classes
, or Functions
) by one element named Extensions
. This section lists one element per external extended type (not per extension). Extensions to locally defined types are not listed there. An alternative naming suggestion would be "Extended Types". This is a bit longer and maybe less elegant, but I think it gives a more precise description of the content. Furthermore it might prevent users from looking for extensions to internally defined types in that section.
The documentation page for these external extended types only lists its locally defined members and conformances. The grouping/positioning of these members follows the usual rules. One can still manually curate the type's page to achieve whatever grouping is desired, of course.
This approach roughly follows the precedent set by the documentation framework Jazzy. You can check out a Jazzy documentation here.
Note that this also means that documentation added to extension blocks (not to the elements inside the extension block) is ignored. This would e.g. apply to the comment on Array
's conformance to NameGenerator
in the example above.
Future efforts therefore might include incorporating such comments if they belong to a protocol conformance or there is only one extension block for the respective type, as well as improving the automatic grouping using various heuristics. Those could include whether or not two members are defined in the same extension block and if such block states a protocol conformance.
Member Identifier
Swift allows us to shadow imported types by declaring a type with the same name locally. Therefore just using TYPE_NAME/MEMBER_NAME
might be ambiguous:
/// This is a custom structure called ``Int64``, which is different
/// from the `Swift/Int64` from the standard library.
public struct Int64 { }
public extension Swift.Int64 {
/// An extension to the `Swift/Int64` type from the
/// standard library.
public static var one: Self {
1
}
}
I think the most sensible solution is to prefix the external type with its module name (MODULE_NAME/TYPE_NAME/MEMBER_NAME
), i.e. in the above example Swift/Int64/one
would be the valid identifier, whereas just Int64/one
would be invalid, as the local Int64
type does not have a property called one
.
One could allow usage of the simpler identifier without the module name if the external type is not shadowed locally. However, this option could be added at a later point in time without source compatibility problems, therefore I don't consider it a priority right now.
Type Identifier
The more interesting question is with the identifier for the external type itself. Currently, the solution trivially seems to be MODULE_NAME/TYPE_NAME
. Unfortunately this could cause conflicts with [SR-15431] Support DocC references to symbols defined in another module · Issue #208 · apple/swift-docc · GitHub down the road. The description of this feature request already suggests usage of the exact same identifier for the type definition in the external module. That is, we have to decide whether e.g. Swift/Int64
should link to the page in the local documentation catalogue listing the local extensions or to the external documentation catalogue with the original type declaration.
My preferred solution would be a local first strategy, i.e. if the link only refers to the type itself, it links to the local documentation catalogue's page listing the locally defined extensions. This page then always contains a link to the respective page in the external documentation catalogue (once [SR-15431] Support DocC references to symbols defined in another module · Issue #208 · apple/swift-docc · GitHub is implemented), e.g. in the "Framework" section of the right column. Naturally, if there are no local extensions to the external type in question, the link directly refers to the type's page in the external documentation catalogue.
Here's an example of how this would look like with the Xcode frontend (of course the proposed changes are not exclusive to Xcode):
Alternatives Considered
Specific Syntax for Differentiating Type Identifiers
An alternative to the semi-ambiguous MODULE_NAME/TYPE_NAME
identifier would be to augment either of the two identifiers with a special flag, e.g.:
-
Swift/Int64-swift.extension
links to the local extension page (if valid) -
Swift/Int64
always links to the types declaration in the external documentation catalogue
I dislike this approach as it results in a less intuitive syntax. Of course there is auto-complete in Xcode, but from time to time people also tend to use this syntax outside of Xcode.
"Extending Module Name" Based Identifiers
A third option for type and member identifiers would be to use the extending module name as a prefix. That is, the page listing all local extensions would be identified by EXTENDING_MODULE_NAME/TYPE_NAME
(e.g. SlothCreator/Int64
), whereas the original declaration of the extended type in the external module would be referenced by DECLARING_MODULE_NAME/TYPE_NAME
(Swift/Int64
). Locally defined members would then be identified by EXTENDING_MODULE_NAME/TYPE_NAME/MEMBER_NAME
, whereas other members would be identified by DECLARING_MODULE_NAME/TYPE_NAME/MEMBER_NAME
.
However, then we have to look at the shadowing problem again:
/// This is a custom structure called ``Int64``, which is different
/// from the `Swift/Int64` from the standard library.
public struct Int64 {
// some members ...
}
public extension Swift.Int64 {
// some members ...
}
/// This is an extension to a custom structure called `Int64`, which is different from `Swift/Int64` AND the local ``Int64`` types.
public extension SomeOtherPackage.Int64 {
// some members ...
}
Assuming this example was set in the SlothCreator
package, the local documentation pages of all three types would be identified by SlothCreator/Int64
. The solution to resolve this conflict would be adding a unique code to the type name as it is already done in ambiguous situations for member identifiers, i.e. something like SlothCreator/Int64-59i2x
. Identifiers for members would also require this postfix on the TYPE_NAME
in certain situations.
While I would be open to this approach, I feel like it could be very confusing to some as it semantically differs from the way we reference the respective types and members in Swift itself.
One Page per Extension Block
We usually group code that is semantically similar into one extension block. Therefore, instead of collecting all extensions to one external type on a single page, one could also create one page per extension block.
In that case (type) identifiers would have to include some code unique to the respective extension block (e.g. Swift/Int64-61i0x
) so that we can unambiguously identify the various pages belonging to the same external type. This would not be necessary for member identifiers.
What's beneficial about this approach is that comments above extension blocks always have an appropriate place in the documentation catalogue. Furthermore, there would be a clear rule for automatic grouping with predictable results that can be influenced by the grouping of extensions in the source code.
However, I feel like this would result in having very many extension pages with very little content each for most code bases. Furthermore, this concept would contradict DocC's concept of the "Topics" section on a type's page which is specifically designed to allow for discussion of different natures of the same type.
Discussion Points
Of course, please share any feedback or ideas you have! However, I'm especially looking for input on the following topics:
- Opinions on identifiers. Do you agree with my choice or would you prefer one of the other options? Do you see problems with any of the options I oversaw? Do you see any better alternatives?
- What do you think about the naming of the new sidebar section ("Extensions" vs "Extended Types")? Do you think many people would look for extensions to locally declared types in an "Extensions" section? Do you have any other suggestions on how one could reduce the risk of confusion there?
- What is your opinion on comments above extension blocks? Is that something we should embrace in DocC, or do you think this is unnecessary or even bad style? I personally write such comments from time to time, especially if the extension block adds a protocol conformance to the type. This decision might have some influence on the implementation approach, so it might not be too easy to change later on.
Based on how this discussion goes I'll either iterate on this pitch to incorporate bigger changes or start exploring implementation strategies in more depth.
Either way, I'm very much looking forward to your thoughts on this!
~ Max