Apply API Guidelines to the Standard Library
01 何が問題だったのか
Swift 3に合わせて、Swiftらしいライブラリ設計の基準としてSwift API Design Guidelinesが整えられました。しかし肝心の標準ライブラリ自身は、Swift 1系から積み上げられた命名規則を引きずっており、新しいガイドラインに照らすと不整合が目立つ状態でした。標準ライブラリはほぼ全てのSwiftコードから使われ、かつ他のライブラリの手本になる存在です。そのため、ガイドラインそのものの説得力を担保するうえでも、標準ライブラリをガイドラインの模範的な実装(exemplar)に作り替える必要がありました。
この提案は、関連する3つの提案と一体で議論されました。
- SE-0023: Swift API Design Guidelines自体の策定
- SE-0005: Objective-C APIのSwiftへの自動変換ルールの刷新
- SE-0006: 標準ライブラリへのガイドラインの適用(本提案)
標準ライブラリの具体的な課題は、おおむね次のように整理できます。
Type サフィックスに依存したプロトコル命名
SequenceType、CollectionType、IntegerType、ErrorType のように、プロトコル名に Type を付けて同名の具象型と衝突を避ける流儀でした。Swiftらしい名詞的なプロトコル名(Sequence、Collection など)とは方向性が違い、ガイドラインに馴染みませんでした。
「generator」という用語
列挙処理の中心的な概念が GeneratorType・generate()・Generator という名前で表現されていました。しかしSwiftの文脈では、Pythonのような遅延生成の仕組みを連想させる「generator」より、位置を進めて要素を取り出す抽象を表す「iterator」の方が適切でした。
メソッドと形容詞・動名詞の使い分けが崩れている
ガイドラインでは、副作用を伴う操作は動詞形(sort())、副作用のない変換を返す操作は形容詞・動名詞形(sorted())に揃えるべきとされています。ところが当時は次のようにちぐはぐでした。
// 非破壊的なのに動詞形
let ys = xs.sort()
// 破壊的なのに In-Place サフィックスで動詞形を避けている
var zs = xs
zs.sortInPlace()
同様に、reverse()・enumerate() が「元を変えずに新しい値を返す」操作にもかかわらず動詞形になっていました。
冗長な Element サフィックス
minElement() や maxElement() のように、戻り値の型から明らかな Element という単語がメソッド名に残っていました。ガイドラインの「型から分かる情報は名前で繰り返さない」方針に反します。
プロパティとメソッドの使い分けの乱れ
計算の実体がないアクセサがメソッドとして宣言されている、あるいは逆に副作用があるのにプロパティのように見える、といった不整合がありました。たとえば Mirror.superclassMirror()、Collection.underestimateCount()、String.lowercaseString などです。
引数ラベルが前置詞を活かせていない
advancedBy(_:)、removeAtIndex(_:)、indexOf(_:)、joinWithSeparator(_:) のように、前置詞がメソッド名に埋め込まれており、呼び出し側で英文として自然に読めませんでした。ガイドラインの「前置詞は最初の引数ラベル側に出す」方針から外れています。
enum ケースや静的プロパティが大文字始まり
Optional.None/.Some、FloatingPoint.NaN、FloatingPointClassification.NegativeInfinity など、値の名前が大文字で始まっていました。Swiftでは型以外の宣言は小文字始まりが原則なので、ここにもガイドライン違反がありました。
不便な初期化APIやC文字列まわりの非一貫性
UnsafePointer の引数なしイニシャライザ(nil で代用すべき)、Array(count:repeatedValue:) のラベル順(現在の文脈では repeating:count: の方が自然)、String.fromCString(_:) のように本来イニシャライザで表現すべき変換がスタティックメソッドとして提供されている、といった点もガイドラインとずれていました。
細部に残っていたその他の不揃い
PermutationGenerator、MutableSliceable、Bit、Repeat のような、より汎用的な書き方に置き換え可能な型が残っていました。また、SequenceType.lexicographicalCompare(_:)、startsWith(_:)、prefixUpTo(_:) のような名前も、ガイドラインが整ったあとでは最適ではなくなっていました。
02 どのように解決されるのか
標準ライブラリ全体を対象に、Swift API Design Guidelinesに沿うよう名前と形を一斉に見直します。変更は多岐にわたりますが、方針は次のカテゴリに整理できます。ここでは代表的な変更を例とともに紹介します(実際にはさらに多くのAPIが影響を受けています)。
プロトコル名から Type サフィックスを外す
SequenceType を Sequence に、CollectionType を Collection に、IntegerType を Integer に、といった具合にプロトコル名を整えます。具象型と名前が衝突するものは、代わりに Protocol サフィックスを付けて区別します(例: AnyCollectionType → AnyCollectionProtocol、LazyCollectionType → LazyCollectionProtocol)。例外的に ErrorType は ErrorProtocol になります。
// Before
func sum<S: SequenceType where S.Generator.Element == Int>(_ xs: S) -> Int { ... }
// After
func sum<S: Sequence where S.Iterator.Element == Int>(_ xs: S) -> Int { ... }
「generator」を「iterator」に統一する
GeneratorType は IteratorProtocol に、generate() は makeIterator() に、Generator 関連の型名もすべて Iterator 系(IndexingIterator、AnyIterator、EmptyIterator、IteratorSequence など)に改名されます。
// Before
var g = xs.generate()
while let x = g.next() { print(x) }
// After
var it = xs.makeIterator()
while let x = it.next() { print(x) }
動詞形と形容詞・動名詞形を揃える
コレクションに対する非破壊的な操作は -ed / -ing 形に、破壊的な操作(mutating)は動詞の原形に揃えます。
sort()→sorted()、sortInPlace()→sort()reverse()→reversed()enumerate()→enumerated()
// 非破壊: 新しい配列を返す
let ys = xs.sorted()
let zs = xs.reversed()
for (i, x) in xs.enumerated() { ... }
// 破壊: 自身を変更する
var ws = xs
ws.sort()
冗長な Element サフィックスを削る
戻り値の型から分かる Element を落とします。
// Before
let smallest = xs.minElement()
let biggest = xs.maxElement()
// After
let smallest = xs.min()
let biggest = xs.max()
プロパティとメソッドを適切に使い分ける
計算を伴わない、あるいは計算量が一定で副作用のないアクセサはプロパティに寄せます。逆に副作用や大域的な計算を伴うものはメソッドに寄せます。
// Before
mirror.superclassMirror() // メソッド
xs.underestimateCount() // メソッド
"hello".uppercaseString // プロパティ
"hello".lowercaseString // プロパティ
// After
mirror.superclassMirror // プロパティ
xs.underestimatedCount // プロパティ
"hello".uppercased() // メソッド
"hello".lowercased() // メソッド
Optional.unsafeUnwrap(_:) のようなトップレベル関数も、Optional.unsafelyUnwrapped プロパティに整理されます。
前置詞は引数ラベル側へ
メソッド名から前置詞を切り出して、最初の引数ラベルにします。呼び出し側で英文として自然に読めるようになります。
// Before
idx.advancedBy(3)
dict.indexForKey("a")
set.removeAtIndex(i)
dict.removeValueForKey("a")
["a", "b"].joinWithSeparator(", ")
xs.startsWith([1, 2])
xs.indexOf(3)
// After
idx.advanced(by: 3)
dict.index(forKey: "a")
set.remove(at: i)
dict.removeValue(forKey: "a")
["a", "b"].joined(separator: ", ")
xs.starts(with: [1, 2])
xs.index(of: 3)
String の prefixUpTo(_:)・suffixFrom(_:)・prefixThrough(_:) も prefix(upTo:)・suffix(from:)・prefix(through:) になり、RangeReplaceableCollection の replaceRange(_:with:) は replaceSubrange(_:with:) に、insertContentsOf(_:at:) は insert(contentsOf:at:) に、appendContentsOf(_:) は append(contentsOf:) に、removeRange(_:) は removeSubrange(_:) に改められます。
enumケースと静的プロパティを小文字始まりにする
Optional.None/.Some → .none/.some、FloatingPoint.NaN → .nan、FloatingPointClassification.NegativeInfinity → .negativeInfinity、Mirror.DisplayStyle.Struct → .struct、PlaygroundQuickLook.Text(...) → .text(...) のように、値の名前を小文字始まりで統一します。
// Before
let x: Int? = .Some(1)
if Double.NaN.isNaN { ... }
// After
let x: Int? = .some(1)
if Double.nan.isNaN { ... }
ポインタ・初期化API・C文字列まわりの整理
UnsafePointer系の引数なしイニシャライザは廃止し、nilリテラルで代用します。- 型パラメータ名
MemoryをPointeeに、プロパティmemoryをpointeeに変更します。 Array(count:repeatedValue:)はArray(repeating:count:)に、String(count:repeatedValue:)もString(repeating:count:)になります。String.fromCString(_:)はString(cString:)やString(validatingUTF8:)といったイニシャライザに置き換わります。
// Before
let buf = UnsafeMutablePointer<Int>()
let xs = Array(count: 3, repeatedValue: 0)
let s = String.fromCString(cStr)
// After
let buf: UnsafeMutablePointer<Int>? = nil
let xs = Array(repeating: 0, count: 3)
let s = String(validatingUTF8: cStr)
役割の重複する型の削除
標準ライブラリにあった Bit、PermutationGenerator、MutableSliceable は削除されます。Bit の代わりに Int を、PermutationGenerator の代わりに map や添字アクセスを、MutableSliceable の代わりに Collection where SubSequence: MutableCollection を使います。
同様に、Repeat<Element> は Repeated<Element> に改名されたうえで、repeatElement(_:count:) というトップレベル関数が追加され、型を直接呼び出さなくても繰り返しコレクションを作れるようになります。
partition や split などアルゴリズムの整理
partition(range:isOrderedBefore:) の range 引数は廃止し、範囲を切りたい場合はスライシングと組み合わせる設計にします。split の引数も maxSplit → maxSplits、allowEmptySlices: Bool = false → omittingEmptySubsequences: Bool = true のようにガイドラインに沿った名前へ整えられます。
// Before
let parts = text.characters.split(
maxSplit: .max, allowEmptySlices: false) { $0 == "," }
// After
let parts = text.characters.split(
maxSplits: .max, omittingEmptySubsequences: true) { $0 == "," }
lexicographicalCompare(_:) は lexicographicallyPrecedes(_:) に、CollectionType.indexOf(_:where:) は index(of:) / index(where:) に分けて命名されます。
既存コードへの影響と移行
標準ライブラリの広範囲にわたる改名のため、変更は大規模なソース互換性破壊を伴います。Swift 2から3への移行時には、マイグレータおよびコンパイラの renamed アノテーションが組み合わされ、旧名の使用に対して新しい名前を提案するエラーメッセージとFix-itが提供されます。このAPI差分自体が、移行に必要な情報の主要なリファレンスとして機能しました。結果として、標準ライブラリはSwift API Design Guidelinesの模範的な実装となり、以降のSwiftのライブラリ設計の方向性を決定づけました。