- Proposal: SE-0005
- Authors: Doug Gregor, Dave Abrahams
- Status: Accepted (Rationale)
- Review manager: Doug Gregor
This review is part of a group of three related reviews, running concurrently:
- SE-0023 API Design Guidelines
- SE-0006 Apply API Guidelines to the Standard Library
- SE-0005 Better Translation of Objective-C APIs Into Swift
These reviews are running concurrently because they interact strongly (e.g., an API change in the standard library will correspond to a particular guideline, or an importer rule implements a particular guideline, etc.). Because of these interactions, and to keep discussion manageable, we ask that you:
- Please get a basic understanding of all three documents before posting review commentary
- Please post your review of each individual document in response to its review announcement. It's okay (and encouraged) to make cross-references between the documents in your review where it helps you make a point.
This proposal describes how we can improve Swift's "Clang Importer", which is responsible for mapping C and Objective-C APIs into Swift, to translate the names of Objective-C functions, types, methods, properties, etc. into names that more closely align with the Swift API Design Guidelines being developed as part of Swift 3. Our approach focuses on the differences between the Objective-C Coding Guidelines for Cocoa and the Swift API Design Guidelines, using some simple linguistic analysis to aid the automatic translation from Objective-C names to more "Swifty" names.
The results of this transformation can be seen in the Swift 3 API Guidelines Review repository, which contains Swift projections of Objective-C APIs in Swift 2 (swift-2
branch) and Swift 3 (swift-3
branch) along with partially-migrated sample code. One can also see the overall changes by comparing the two branches.
The Objective-C Coding Guidelines for Cocoa provide a framework for creating clear, consistent APIs in Objective-C, where they work extraordinarily well. However, Swift is a different language: in particular, it is strongly typed and provides type inference, generics, and overloading. As a result, Objective-C APIs that feel right in Objective-C can feel wordy when used in Swift. For example:
let content = listItemView.text.stringByTrimmingCharactersInSet(
NSCharacterSet.whitespaceAndNewlineCharacterSet())
The APIs used here follow the Objective-C guidelines. A more "Swifty" version of the same code might instead look like this:
let content = listItemView.text.trimming(.whitespaceAndNewlines)
The latter example more closely adheres to the Swift API Design Guidelines, in particular, omitting "needless" words that restate the types already enforced by the compiler (view, string, character set, etc.). The goal of this proposal is to make imported Objective-C feel more "Swifty", providing a more fluid experience for Swift programmers using Objective-C APIs.
The solution in this proposal applies equally to the Objective-C frameworks (e.g., all of Cocoa and Cocoa Touch) and any Objective-C APIs that are available to Swift in mix-and-match projects. Note that the Swift core libraries reimplement the APIs of Objective-C frameworks, so any API changes to those frameworks (Foundation, XCTest, etc.) will be reflected in the Swift 3 implementations of the core libraries.
The proposed solution involves identifying the differences between the
Objective-C Coding Guidelines for Cocoa and
the Swift API Design Guidelines to build a
set of transformations that map from the former to the latter based on
the guidelines themselves and other observed conventions in
Objective-C. This is an extension of other heuristics in the Clang
importer that translate names, e.g., the mapping of global enum
constants into Swift's cases (which strips common prefixes from the
enum constant names) and the mapping from Objective-C factory methods
(e.g., +[NSNumber numberWithBool:]
) to Swift initializers
(NSNumber(bool: true)
).
The heuristics described in this proposal will require iteration, tuning, and experimentation across a large body of Objective-C APIs to get right. Moreover, it will not be perfect: some APIs will undoubtedly end up being less clear in Swift following this translation than they had been before. Therefore, the goal is to make the vast majority of imported Objective-C APIs feel more "Swifty", and allow the authors of Objective-C APIs that end up being less clear to address those problems on a per-API basis via annotation within the Objective-C headers.
The proposed solution involves several related changes to the Clang importer:
-
Generalize the applicability of the
swift_name
attribute: The Clangswift_name
attribute currently allows limited renaming of enum cases and factory methods. It should be generalized to allow arbitrary renaming of any C or Objective-C entity when it is imported into Swift, allowing authors of C or Objective-C APIs more fine-grained control over the process. -
Prune redundant type names: The Objective-C Coding Guidelines for Cocoa require that the method describe each argument. When those descriptions restate the type of the corresponding parameter, the name conflicts with the omit needless words guideline for Swift APIs. Therefore, we prune these type names during import.
-
Add default arguments: In cases where the Objective-C API strongly hints at the need for a default argument, infer the default argument when importing the API. For example, an option-set parameter can be defaulted to
[]
. -
Add first argument labels: If the first parameter of a method is defaulted, it should have an argument label. Determine a first argument label for that method.
-
Prepend "is" to Boolean properties: Boolean properties should read as assertions on the receiver, but the Objective-C Coding Guidelines for Cocoa prohibit the use of "is" on properties. Import such properties with "is" prepended.
-
Lowercase values: The Swift API Design Guidelines have non-type declarations lowercased. Lowercase non-prefixed values whenever they are imported, including enumerators (whether they end up in Swift as enum cases or option sets) and any other properties/functions (e.g., a property named
URLHandler
will be lowercased tourlHandler
). -
Adopt Comparable to classes that implement
compare(_:) -> NSComparisonResult
: The objective-c classes that implement compare all have declared a capability of being compared in an ordered manner.Comparable
formalizes this declaration into an implementable operator by the import process.
To get a sense of what these transformations do, consider a portion of
the imported UIBezierPath
API in Swift 2:
class UIBezierPath : NSObject, NSCopying, NSCoding {
convenience init(ovalInRect: CGRect)
func moveToPoint(_: CGPoint)
func addLineToPoint(_: CGPoint)
func addCurveToPoint(_: CGPoint, controlPoint1: CGPoint, controlPoint2: CGPoint)
func addQuadCurveToPoint(_: CGPoint, controlPoint: CGPoint)
func appendPath(_: UIBezierPath)
func bezierPathByReversingPath() -> UIBezierPath
func applyTransform(_: CGAffineTransform)
var empty: Bool { get }
func containsPoint(_: CGPoint) -> Bool
func fillWithBlendMode(_: CGBlendMode, alpha: CGFloat)
func strokeWithBlendMode(_: CGBlendMode, alpha: CGFloat)
func copyWithZone(_: NSZone) -> AnyObject
func encodeWithCoder(_: NSCoder)
}
And the same API imported under our current, experimental implementation of this proposal:
class UIBezierPath : NSObject, NSCopying, NSCoding {
convenience init(ovalIn rect: CGRect)
func move(to point: CGPoint)
func addLine(to point: CGPoint)
func addCurve(to endPoint: CGPoint, controlPoint1 controlPoint1: CGPoint, controlPoint2 controlPoint2: CGPoint)
func addQuadCurve(to endPoint: CGPoint, controlPoint controlPoint: CGPoint)
func append(_ bezierPath: UIBezierPath)
func reversing() -> UIBezierPath
func apply(_ transform: CGAffineTransform)
var isEmpty: Bool { get }
func contains(_ point: CGPoint) -> Bool
func fill(_ blendMode: CGBlendMode, alpha alpha: CGFloat)
func stroke(_ blendMode: CGBlendMode, alpha alpha: CGFloat)
func copy(with zone: NSZone = nil) -> AnyObject
func encode(with aCoder: NSCoder)
}
In the latter case, a number of words that restated type information
in the original APIs have been pruned. The result is closer to
following the Swift API Design Guidelines. For example, this shows
that Swift developers can now copy any object conforming to the
NSCopying with a simple call to foo.copy()
instead of calling
foo.copyWithZone(nil)
.
An experimental implementation of this proposal is available in the
main Swift repository. There are a set of compiler flags that one can
use to see the results of applying this proposal to imported
Objective-C APIs (e.g., via the script in
utils/omit-needless-words.py
) and to Swift code itself. The flags
are:
-
-enable-omit-needless-words
: this flag enables most of the changes to the Clang importer (bullets 1, 2, 4, and 5 in the prior section). It is currently suitable only for printing the Swift interface to Objective-C modules (e.g., viaswift-ide-test
) in the Swift master branch and Swift 2.2 branch, and is enabled on the Swift 3 API Guidelines branch. -
-enable-infer-default-arguments
: this flag enables inference of default arguments in the Clang importer (bullet 3 in the prior section). -
-swift3-migration
: only available on the Swift 2.2 branch, this flag performs basic migration from Swift 2 names to the Swift 3 names via Fix-Its. Tied together with other compiler flags (e.g.,-fixit-code
,-fixit-all
) and a script to collect and apply Fix-Its (inutils/apply-fixit-edits.py
), this flag provides a rudimentary migrator that lets us see how Swift code would look under the proposed changes, updating both declarations and use sites.
To actually get the "Swift 3 experience" of compiling code using these names, one can use the Swift 3 API Guidelines branch, which enables these features by default along with the changes to the standard library.
This section details the experimental implementation of rules 2-5 in prose. The actual implementation is available in the Swift source tree, mostly in the omitNeedlessWords
functions of lib/Basic/StringExtras.cpp.
The descriptions in this section are described in terms of the incoming Objective-C API. For example, Objective-C method names are "selectors", e.g., startWithQueue:completionHandler:
is a selector with two selector pieces, startWithQueue
and completionHandler
. A direct mapping of this name into Swift would produce startWithQueue(_:completionHandler:)
.
Objective-C API names often contain names of parameter and/or result types that would be omitted in a Swift API. The following rules are designed to identify and remove these words. [Omit Needless Words]
The matching process described below searches in a selector piece for a suffix of a string called the type name, which is defined as follows:
-
For most Objective-C types, the type name is the name under which Swift imports the type, ignoring nullability. For example,
Objective-C type Type Name float
Float
nullable NSString
String
UIDocument
UIDocument
nullable UIDocument
UIDocument
NSInteger
NSInteger
NSUInteger
NSUInteger
CGFloat
CGFloat
-
When the Objective-C type is a block, the type name is "
Block
." -
When the Objective-C type is a pointer- or reference-to-function, the type name is "
Function
." -
When the Objective-C type is a typedef other than
NSInteger
,NSUInteger
, orCGFloat
(which follow the first rule above), the type name is that of the underlying type. For example, when the Objective-C type isUILayoutPriority
, which is a typedef forfloat
, we try to match the string "Float
". [Compensate for Weak Type Information]
In order to prune a redundant type name from a selector piece, we need to match a substring of the selector that identifies the type.
A couple of basic rules govern all matches:
-
Matches begin and end at word boundaries in both type names and selector pieces. Word boundaries occur at the beginning and end of a string, and before every capital letter.
Treating every capital letter as the beginning of a word allows us to match uppercased acronyms without maintaining a special lists of acronyms or prefixes:
func documentForURL(_: NSURL) -> NSDocument?
while preventing partial-word mismatches:
var thumbnailPreview : UIView // not matched
-
Matched text extends to the end of the type name. Because we accept a match for any suffix of the type name, this code:
func constraintEqualToAnchor(anchor: NSLayoutAnchor) -> NSLayoutConstraint?
can be pruned as follows:
func constraintEqualTo(anchor: NSLayoutAnchor) -> NSLayoutConstraint?
Conveniently, matching by suffix also means that module prefixes such as
NS
do not prevent matching or pruning.
Matches are a sequence of one or more of the following:
-
Basic matches
-
Any substring of the selector piece matches an identical substring of the type name, e.g.,
String
inappendString
matchesString
inNSString
:
-
func appendString(_: NSString)
-
Index
in the selector piece matchesInt
in the type name:
func characterAtIndex(_: Int) -> unichar
-
Collection matches
-
Indexes
orIndices
in the selector piece matchesIndexSet
in the type name:
-
func removeObjectsAtIndexes(_: NSIndexSet)
- A plural noun in the selector piece matches a collection type name if the noun's singular form matches the name of the collection's element type:
func arrangeObjects(_: [AnyObject]) -> [AnyObject]
-
Special suffix matches
-
The empty string in the selector piece matches
Type
or_t
in the type name:
-
func writableTypesForSaveOperation(: NSSaveOperationType) -> [String] func objectForKey(: KeyType) -> AnyObject func startWithQueue(: dispatchqueue_t, completionHandler: MKMapSnapshotCompletionhandler)
-
The empty string in the selector piece matches one or more digits followed by "D" in the type name:
func pointForCoordinate(_: CLLocationCoordinate2D) -> NSPoint
In the examples above, the italic text is effectively skipped, so the bold part of the selector piece can be matched and pruned.
The following restrictions govern the pruning steps listed in the next section. If any step would violate one of these rules, it is skipped.
-
Never make a selector piece entirely empty.
-
Never transform the first selector piece into a Swift keyword, to avoid forcing the user to escape it with backticks. In Swift, the first Objective-C selector piece becomes:
- the base name of a method
- or the full name of a property
neither of which can match a Swift keyword without forcing the user to write backticks. For example,
extension NSParagraphStyle { class func defaultParagraphStyle() -> NSParagraphStyle } let defaultStyle = NSParagraphStyle.defaultParagraphStyle() // OK
would become:
extension NSParagraphStyle { class func `default`() -> NSParagraphStyle } let defaultStyle = NSParagraphStyle.`default`() // Awkward
By contrast, later selector pieces become argument labels, which are allowed to match Swift keywords without requiring backticks:
receiver.handle(someMessage, for: somebody) // OK
-
Never transform a name into "get", "set", "with", "for", or "using", just to avoid creating absurdly vacuous names.
-
Never prune a suffix from a parameter introducer unless the suffix is immediately preceded by a preposition, verb, or gerund.
This heuristic has the effect of preventing us from breaking up sequences of nouns that refer to a parameter. Dropping just the suffix of a noun phrase tends to imply something unintended about the parameter that follows. For example,
func setTextColor(_: UIColor) ... button.setTextColor(.red()) // clear
If we were to drop Color
, leaving just Text
, call sites
would become confusing:
func setText(_: UIColor) ... button.setText(.red()) // appears to be setting the text!
Note: We don't maintain a list of nouns, but if we did, this rule could be more simply phrased as "don't prune a suffix leaving a trailing noun before a parameter".
-
Never prune a suffix from the base name of a method that matches a property of the enclosing class:
This heuristic has the effect of preventing us from producing too-generic names for methods that conceptually modify a property of the class.
var gestureRecognizers: [UIGestureRecognizer] func addGestureRecognizer(_: UIGestureRecognizer)
If we were to drop GestureRecognizer
, leaving just add
, we end
up with a method that conceptually modifies the
gestureRecognizers
property but uses an overly generic name to
do so:
var gestureRecognizers: [UIGestureRecognizer] func add(_: UIGestureRecognizer) // should indicate that we're adding to the property
The following pruning steps are performed in the order shown:
-
Prune the result type from the head of type-preserving transforms. Specifically, when
- the receiver type is the same as the result type
- and the type name is matched at the head of the first selector piece
- and the match is followed by a preposition
then prune the match.
You can think of the affected operations as properties or non-mutating methods that produce a transformed version of the receiver. For example:
extension NSColor { func colorWithAlphaComponent(_: CGFloat) -> NSColor } let translucentForeground = foregroundColor.colorWithAlphaComponent(0.5)
becomes:
extension NSColor { func withAlphaComponent(_: CGFloat) -> NSColor } let translucentForeground = foregroundColor.withAlphaComponent(0.5)
-
Prune an additional hanging "By". Specifically, if
- anything was pruned in step 1
- and the remaining selector piece begins with "
By
" followed by a gerund,
then prune the initial "
By
" as well.This heuristic allows us to arrive at usage of the form
a = b.frobnicating(c)
. For example:
extension NSString { func stringByApplyingTransform(_: NSString, reverse: Bool) -> NSString? } let sanitizedInput = rawInput.stringByApplyingTransform(NSStringTransformToXMLHex, reverse: false)
becomes:
extension NSString { func applyingTransform(_: NSString, reverse: Bool) -> NString? } let sanitizedInput = rawInput.applyingTransform(NSStringTransformToXMLHex, reverse: false)
-
Prune a match for any type name in the signature from the tail of the preceding selector piece. Specifically,
From the tail of: Prune a match for: a selector piece that introduces a parameter the parameter type name the name of a property the property type name the name of a zero-argument method the return type name For example,
extension NSDocumentController { func documentForURL(_ url: NSURL) -> NSDocument? // parameter introducer } extension NSManagedObjectContext { var parentContext: NSManagedObjectContext? // property } extension UIColor { class func darkGrayColor() -> UIColor // zero-argument method } ... myDocument = self.documentForURL(locationOfFile) if self.managedObjectContext.parentContext != changedContext { return } foregroundColor = .darkGrayColor()
becomes:
extension NSDocumentController { func documentFor(_ url: NSURL) -> NSDocument? } extension NSManagedObjectContext { var parent : NSManagedObjectContext? } extension UIColor { class func darkGray() -> UIColor } ... myDocument = self.documentFor(locationOfFile) if self.managedObjectContext.parent != changedContext { return } foregroundColor = .darkGray()
-
Prune a match for the enclosing type from the base name of a method so long as the match starts after a verb. For example,
extension UIViewController { func dismissViewControllerAnimated(flag: Bool, completion: (() -> Void)? = nil) }
becomes:
extension UIViewController { func dismissAnimated(flag: Bool, completion: (() -> Void)? = nil) }
Some steps below prune matches from the head of the first selector piece, and some prune from the tail. When pruning restrictions prevent both the head and tail from being pruned, prioritizing head-pruning steps can keep method families together. For example, in NSFontDescriptor:
func fontDescriptorWithSymbolicTraits(_: NSFontSymbolicTraits) -> NSFontDescriptor
func fontDescriptorWithSize(_: CGFloat) -> UIFontDescriptor
func fontDescriptorWithMatrix(_: CGAffineTransform) -> UIFontDescriptor
...
becomes:
func withSymbolicTraits(_: UIFontDescriptorSymbolicTraits) -> UIFontDescriptor func withSize(_: CGFloat) -> UIFontDescriptor func withMatrix(_: CGAffineTransform) -> UIFontDescriptor ...
If we instead began by pruning SymbolicTraits
from the tail of
the first method name, the prohibition against creating absurdly
vacuous names would prevent us from pruning "fontDescriptorWith
"
down to "with
", resulting in:
func fontDescriptorWith(_: NSFontSymbolicTraits) -> NSFontDescriptor // inconsistent func withSize(_: CGFloat) -> UIFontDescriptor func withMatrix(_: CGAffineTransform) -> UIFontDescriptor ...
For any method that is not a single-parameter setter, default arguments are added to parameters in the following cases:
-
Nullable trailing closure parameters are given a default value of
nil
. -
Nullable NSZone parameters are given a default value of
nil
. Zones are essentially unused in Swift and should always benil
. -
Option set types whose type name contain the word "Options" are given a default value of
[]
(the empty option set). -
NSDictionary parameters with names that involve "options", "attributes", or "info" are given a default value of
[:]
.
Together, these heuristics allow code like:
rootViewController.presentViewController(alert, animated: true, completion: nil) UIView.animateWithDuration( 0.2, delay: 0.0, options: [], animations: { self.logo.alpha = 0.0 }) { _ in self.logo.hidden = true }
to become:
rootViewController.present(alert, animated: true)
UIView.animateWithDuration(
0.2, delay: 0.0, animations: { self.logo.alpha = 0.0 }) { _ in self.logo.hidden = true }
If the first selector piece contains a preposition, split the first selector piece at the last preposition, turning everything starting with the last preposition into a required label for the first argument.
As well as creating first argument label for a significant number of APIs, this heuristic eliminates words that refer only to the first argument from call sites where the argument's default value is used. For example, instead of:
extension UIBezierPath { func enumerateObjectsWith(_: NSEnumerationOptions = [], using: (AnyObject, UnsafeMutablePointer) -> Void) } array.enumerateObjectsWith(.Reverse) { // OK // .. } array.enumerateObjectsWith() { // ?? With what? // .. }
we get:
extension NSArray { func enumerateObjects(options _: NSEnumerationOptions = [], using: (AnyObject, UnsafeMutablePointer) -> Void) } array.enumerateObjects(options: .Reverse) { // OK // .. } array.enumerateObjects() { // OK // .. }
*For Boolean properties, use the name of the getter as the property name in Swift. For example:
@interface NSBezierPath : NSObject
@property (readonly,getter=isEmpty) BOOL empty;
will become
extension NSBezierPath { var isEmpty: Bool } if path.isEmpty { ... }
Currently, in comparing protocols, for example developers usually have
to extend NSDate
to make it to conform to Comparable
, or use
compare(_:) -> NSComparisonResult
method of NSDate
directly. In
this case Using comparison operators on NSDate
s will make the code
more readable, such as someDate < today
, rather than
someDate.compare(today) == .OrderedAscending
. Since the import process
can determine if a class implements the objective-c method for
comparison all classes that implement this method will then be imported
as adopting Comparable
.
A survey of Foundation classes reveals not just NSDate but a few other classes that would be affected by this change.
func compare(other: NSDate) -> NSComparisonResult func compare(decimalNumber: NSNumber) -> NSComparisonResult func compare(otherObject: NSIndexPath) -> NSComparisonResult func compare(string: String) -> NSComparisonResult func compare(otherNumber: NSNumber) -> NSComparisonResult
The proposed changes are massively source-breaking for Swift code that
makes use of Objective-C frameworks, and will require a migrator to
translate Swift 2 code into Swift 3 code. The -swift3-migration
flag described in the Implementation
Experience section can provide the basics
for such a migrator. Additionally, the compiler needs to provide good
error messages (with Fix-Its) for Swift code that refers to the old
(pre-transformed) Objective-C names, which could be achieved with some
combination of the Fix-Its described previously and a secondary name
lookup mechanism retaining the old names.
The automatic translation described in this proposal has been developed as part of the effort to produce the Swift API Design Guidelines with Dmitri Hrybenko, Ted Kremenek, Chris Lattner, Alex Migicovsky, Max Moiseev, Ali Ozer, and Tony Parker.
The addendum of comparable was originally proposed to the core-libraries mailing list by Chris Amanse and modified to fit to this proposal after review by Philippe Hausler.