Build Insiderオピニオン:小野将之(6)
Swift 3.1のリリースプロセスおよびそれに含まれる変更内容の紹介(後編)
Swift 3.1のリリースが2017年春に迫ってきた。今回は前後編に分けて、そのリリースプロセスや変更内容を解説する。後編ではSwift 3.1に取り込まれることが想定される変更点を取り上げる。
Swift 3.1の主な変更一覧
SwiftリポジトリのCHANGELOGとSwift EvolutionリポジトリのProposal Statusを見ると、Swift 3.1の開発状況が分かる。以下では12月20日時点で最新のDEVELOPMENT-SNAPSHOT-2016-12-15-a
ツールチェインでの実装状況を記す。
実装済み
まずは実装済みのものから紹介しよう。
- SE-0045:
Sequence
プロトコルにprefix(while:)
とdrop(while:)
が追加 - SE-0141:
@available
構文におけるSwiftバージョン分岐 - SR-1009: 具体的な型を用いたジェネリクス制約
- SR-1446: ジェネリクス型の入れ子
- SR-1882: 文字列補間(
String interpolation
)にOptional
型を直接用いると警告が出るように
実装中
現時点のスナップショット版では試せないが実装中であり、基本的にSwift 3.1に入る予定のものには以下がある。
- SE-0042: 未適用のメソッド参照の平坦化
- SE-0075: インポート可能なモジュールによって分岐可能なビルド設定
- SE-0080: 数値変換に失敗したら
nil
を返すイニシャライザー - SE-0104:Protocol-Orientedな
Int
型
実装着手前
Swiftに組み込むことは決定しているが、まだ実装着手に至っていないものもある。Swift 3.1に含められるように目指しているが、前編で述べたタイムリミットである2017年1月16日に間に合わなければ次期バージョン以降に持ち越しとなる。
- SE-0068:
Self
によるクラスメンバーへのアクセス - SE-0082: Swift Package Managerの編集
- SE-0110: 関数引数が単一タプルか複数引数によって、型を区別
- SE-0142:Protocolの
associatedtype
をwhere
句で制約可能に - SE-0143: 条件付きのジェネリクス制約
- SE-0145: Swift Package Managerで依存解決したバージョンを固定(Pin)できる仕組み
- SE-0146: Swift Package Managerで生成するTargetの定義
レビュー中
以下は現在レビュー中だ。
Swift 3.1の変更を先取り
それでは、今実際に試せるSwift 3.1に含まれる予定の変更を見ていこう。コード例は以下の環境によるものだが、基本的にはこれ以降のバージョンであれば同様に動くはずである。
- Xcode 8.2.1
- Swift Toolchainバージョン:
DEVELOPMENT-SNAPSHOT-2016-12-15-a
SE-0045: Sequenceプロトコルにprefix(while:)メソッドとdrop(while:)メソッドが追加
Swift
には、map(_:)
やfilter(_:)
など、多くのコレクション操作メソッドが存在するが、そこに欠けていたprefix(while:)
メソッドとdrop(while:)
メソッドが追加された。
まず、prefix(while:)
メソッドの簡単な使い方を以下に示す。これはコレクションの先頭から走査を開始し、条件に一致する間はその要素を抽出する。
let x = [1, 2, 3, 4, 5]
x.prefix { $0 < 3 } // [1, 2]
// `filter(_:)` と同じ結果
x.filter { $0 < 3 } // [1, 2]
|
上の例では、結果はfilter(_:)
メソッドを使った場合の結果と同じになる。ただし、prefix(while:)
メソッドでは条件に一致するとそれ以降の走査をしないため、評価は3回だけ走るが、filter(_:)
メソッドでは全要素を走査するため評価が5回走る違いがある。このように「ある条件を満たしている間の要素を抽出したい」という用途であれば結果が同じだとしてもprefix(while:)
を用いるのがよい。
ただ、上の例で2つメソッドの呼び出し結果が同じになったのは、コレクションがたまたまソート済みだったからだ。例えばコレクションの要素を逆順にしてから同じ処理を行うと、filter(_:)
メソッドの結果は先の例と変わらないが、prefix(while:)
メソッドの方は1つ目の要素「5」が条件を満たさないため、そこで処理が打ち切られて、結果は空になる。
let y = x.reversed() // [5, 4, 3, 2, 1]
y.prefix { $0 < 3 } // []
// `filter(_:)` と結果が異なる
y.filter { $0 < 3 } // [1, 2]
|
このようにこれら2つのメソッドは一見似ているが全く別物なので、もちろん適宜最適なコレクション操作メソッドを使い分ける必要がある。
drop(while:)
メソッドは、prefix(while:)
メソッドとは逆に「ある条件に合致している間はその要素を除去したい」ときに用い、例えば次のように使える。
let x = [1, 2, 3, 4, 5]
x.drop { $0 < 3 } // [3, 4, 5]
x.reversed().drop { $0 < 3 } // [5, 4, 3, 2, 1]
|
prefix(while:)
メソッドとdrop(while:)
メソッドの追加は「SE-0045」という5月に作られた古めのProposalで提案された。このことから分かるように、これはSwift 3.1の目玉の変更ではなく、単にSwift 3.0に間に合わなかったのが、ようやくSwift 3.1のタイミングで入ることになったということである。単なるメソッドの追加なので、Swift 3.0に詰め込みたかった破壊的変更の絡むProposalと比べて優先度が低かったため対応が遅れた。
SE-0141: @available構文においてのSwiftバージョン分岐
Swift 2.0で、@available
/#available
構文を用いてプラットフォームとそのバージョン指定を行えるようになった。
@available(iOS, introduced: 10)
func f1() { // iOS 10以上でないと呼べない
}
func f2() {
if #available (iOS 10, *) {
// iOS 10以上のときのみ処理される
}
}
|
Swift 3.1では、2つのうちの@available
構文でSwiftのバージョンも指定できるようになる。ただし、#available
構文でのSwiftのバージョン指定には対応しておらず、Swift language version checks not allowed in #available(...)
というコンパイルエラーになってしまう。
func ng() {
if #available(swift 3, *) {
}
}
|
基本的な使用例は次の通りであり、@available構文で修飾されたもの(メソッド、型など)を使えるSwiftのバージョン/非推奨となるSwiftのバージョン/廃止となるSwiftのバージョンを表現でき、対応外のバージョンのSwiftが使用されたときには警告やコンパイルエラーを発生してくれる。f5
メソッドに付けたrenamed
はオプション引数で、これを付けることで廃止になった代わりに新設されたメソッドを示せる。
@available(swift, introduced: 4)
func f3() { // Swift 4以降で使える
}
@available(swift, deprecated: 4)
func f4() { // Swift 4で非推奨(警告表示)
}
@available(swift, obsoleted: 4, renamed: "f3")
func f5() { // Swift 4で廃止(代わりにf3メソッドを使う)
}
|
第1引数にiOS
などを指定した場合は、unavailable
構文によって「そのプラットフォームではバージョンを問わず使えない」ということも表現できるが、swift
を指定した場合はunavailable
指定はできない。これはUnknown platform 'swift' for attribute 'available'
という警告が表示されることで分かる。
@available(swift, unavailable)
func f6() {
}
|
もともとSwiftでは、#if swift(>=N)
マクロによって、次のようにSwiftバージョンによってソースコードを切り替えることはできたが、このマクロを使うとコンパイル時にどちらかの処理が切り捨てられてしまう。
#if swift(>=4)
// Swift 4以降用のコード
#else
// Swift 4より前用のコード
#endif
|
このProposalは、前編で取り上げた-swift-version N
フラグによって過去のSwiftバージョンの互換モードをサポートできるようにする対応(「SR-2582: Add -swift-version command line flag」)にも関連している。
これを実現するには、以下の2つの選択肢があったが、「標準ライブラリの配布をしやすい」などの理由で後者に決まり、実装が行われた。
#if swift(>=N)
マクロを利用して、バージョンごと複数バイナリを生成@available
でSwiftのバージョンも指定できるようにして、複数のSwiftバージョンに対応した単一バイナリを生成
経緯としてはSwift言語自体の開発を円滑にするためのものだったが、通常のアプリ開発においても例えば「Swift 3の制約でどうしてもこの対処が必要だが、Swift 4に含まれるあの対応が入ればもっとシンプルに書ける」というときなどに、あらかじめ@available(swift, deprecated: 4)
を仕込んでおき、その後、Swift 4でビルドした際にコンパイルエラーにする、という活用などもできるであろう。
SR-1009: 具体的な型を用いたジェネリクス制約
まず、Swift 3.0まで「具体的な型を用いたジェネリクス制約」に対応しておらず不便だったことを、Optional<String>
型に新しいメソッドを足す例にとって説明する。
Optional
型は次のようにWrapped
というジェネリクス型で含む値の方を表現するようになっている。
public enum Optional<Wrapped> : ExpressibleByNilLiteral { ... }
|
Optional<String>
型に新しいメソッドを追加したいとすると、素直に考えれば次の「コードA」のような書き方になるであろう。
// コードA
extension Optional where Wrapped == String {
/** nilや空文字の場合はtrue、それ以外のときはfalseを返す */
var isNilOrEmpty: Bool {
return self?.isEmpty ?? true
}
}
|
ただ、この書き方だと、Swift 3.0では次のようなコンパイルエラーとなってしまう。
Same-type requirement makes generic parameter 'Wrapped' non-generic
|
「具体的な型を用いたジェネリクス制約」に対応していない、ということである。
対処としては次のように、String
を直接使わずにそれを包むProtocol
(StringProtocol
)を用意して、型制約としてはWrapped
がStringProtocol
に準拠している(where Wrapped == String
ではなくwhere Wrapped: StringProtocol
)という記述にすれば、制限を回避できる。
protocol StringProtocol {
var value: String { get }
}
extension String: StringProtocol {
var value: String { return self }
}
extension Optional where Wrapped: StringProtocol {
/** 値があればそれを返して、なければ空文字を返す */
var getOrDefault: String {
return self?.value ?? ""
}
}
|
Swift 3.1では、この対処コードを書かずとも、「コードA」の書き方でコンパイルが通るようになった。さまざまなライブラリやアプリのコードでこのような対処がなされている現状なので、この書き方が可能となる恩恵は大きい。
SR-1446: ジェネリクス型の入れ子
クラスの所属・役割を明確にしたり、名前の重複を回避したりするために、型を入れ子で表現することがある。特にSwiftの場合、名前空間がモジュール単位となっていて、細かい粒度で名前空間を用意しにくいこともあり、型の入れ子定義は多用されていると感じている。
ただ、これもジェネリクス型は入れ子にできないという制限があったため、Swift 3.0までは仕方なくジェネリクス型の入れ子をあきらめるという、設計の妥協をせざるを得なかった。
Swift 3.1では、以下のいずれも書けるようになった。
// SwiftリポジトリのCHANGELOGのコード例をそのまま掲載
struct OuterNonGeneric {
struct InnerGeneric<T> {}
}
struct OuterGeneric<T> {
struct InnerNonGeneric {}
struct InnerGeneric<T> {}
}
extension OuterNonGeneric.InnerGeneric {}
extension OuterGeneric.InnerNonGeneric {}
extension OuterGeneric.InnerGeneric {}
|
ジェネリクスの制限については、2016年5月に、swift-evolutionメーリングリストに「[swift-evolution] [Manifesto] Completing Generics」という声明が投稿されていて、ジェネリクスの機能制限の解消は大きな課題の一つであった。
今見た「SR-1009: 具体的な型を用いたジェネリクス制約」と「SR-1446: ジェネリクス型の入れ子」の2件でその一部が解消した。そういう意味では、これらの対応はSwift 3.1における改善の大きな目玉といえるだろう。まだ着手前の「SE-0143: 条件付きのジェネリクス制約」など、まだ残っている制限はあるが、今後もジェネリクスの制限は少しずつ改善されていくであろう。
SR-1882: 文字列補間(String interpolation)にOptional型を直接用いると警告
以下のコードではhello Optional("world")
という文字列が出力される。
let s: String? = "world"
print("hello \(s)")
|
このコードだけ見れば妥当な挙動だが、通常のアプリでは意図せずOptional
の変数を用いて文字列を組み立ててしまうミス(hello world
とするつもりがhello Optional("world")
になるなど)の発生につながる。
Swift 3.1では、このケースで以下の警告が発生するようになる。
String interpolation produces a debug description for an optional value; did you mean to make this explicit?
|
本当にOptional()
で包まれた文字列でよいのであれば、次のようにすれば警告を解消できる。
print("hello \(s as String?)")
print("hello \(String(describing: s))")
|
Optional()
を外したい場合は普通にあらかじめアンラップしてから使うか、次のように?? ""
でデフォルト値を設定するなどすればよい。
print("hello \(s ?? "")")
|
特に、Swift 3.0では「SE-0054: ImplicitlyUnwrappedOptional
型の廃止」によって以前にImplicitlyUnwrappedOptional
型だったものがOptional
型に変わった箇所が多く発生したので、警告で追従漏れに気付けるのは地味ながらうれしい(Swift 3.0と同時にこの対応がなされていればベストだったが……)。
Swift Package Manager(SwiftPM)の新機能を先取り
Swift Package Manager(SwiftPM)という、アップル(Apple)公式のパッケージマネージャーにも大きめの変更がある。2016年12月7日にswift-build-devメーリングリストに「[swift-build-dev] Try out new SwiftPM features!」という投稿があり、DEVELOPMENT-SNAPSHOT-2016-12-07-a
より、開発中の次の機能が使えるようになったことがアナウンスされた。以前は--enable-new-resolver
フラグで明示的に指定したときの挙動だったものがデフォルトの挙動になった次第である。
- Editable Packages - Documentation
- Package Pinning - Documentation
SwiftPMにはSwift 3.0の段階で実装できていない機能がまだまだたくさん残っていることがswift-build-devメーリングリストへの「[swift-build-dev] Swift Package Manager 3.0 Project Status」という投稿でも報告されていた。アップル公式というアドバンテージがあるので将来的にはiOSアプリ開発でのパッケージマネージャーの定番にもなっていくであろうが、まだまだ開発途中の段階なので、しばらくは今定番のCocoaPodsやCarthageを利用する状態が続くだろう。ただ、サーバーサイドSwiftなどクロスプラットフォーム系のプロジェクトではすでにSwiftPMが活発に利用されている。
SwiftPMについては開発が落ち着いたタイミングで紹介記事を書きたいと思っている。
まとめ
前編と合わせて、
- Swift 3.1~4の開発動向
- Toolchains版を用いた開発最新版のSwiftの利用の仕方
- Swift 3.1に含まれるであろう具体的な変更内容
を紹介した。Swift 3.0までは大きな機能追加や破壊的変更が続いていたが、3.1で行われるのは表面的には比較的地味に見えるアップデートになりそうである。3.0までに大きな変更を集中させた成果もあり、互換性担保や残った細かい改善に力を入れられるようになった、ともいえる。
小野 将之(おの まさゆき)
学生時代から情報系の専攻、プログラミングのアルバイトなどでコンピューターに親しむ。
その後、大手SIerを経て、4年ほど前から複数のベンチャー企業でiOSアプリ開発をメインとするようになった。
SwiftはWWDC 2014年にベータ版が発表された直後から、ずっと触り続けている。
2015年からQiitaで多数の記事を書き、好評を集めている(http://qiita.com/mono0926)。
現在は株式会社Vikonaのエンジニアとして、JOIN USのiOS版アプリ開発に加えて、Ruby on RailsによるサーバーAPI開発もこなしている。
※以下では、本稿の前後を合わせて5回分(第2回~第6回)のみ表示しています。
連載の全タイトルを参照するには、[この記事の連載 INDEX]を参照してください。
2. Swiftは3.0で安定するのか? リリース予定日と新機能リスト
2016年後半のリリースが予定されているSwift 3。そのリリースに先駆けて、どんな変更点があるのか、懸案となっている互換性はどうなるのかなどを見ていく。
3. Swiftの開発体制、swift.org/Swift Evolutionリポジトリとは?
次期Swiftに搭載予定の新機能といった最新情報はどこで入手できるのか。Swiftについての情報を常にキャッチアップするために見ておくべきサイトを紹介する。
4. Swift 3.0でなぜ「Cスタイルのforループ」「++/--演算子」などの仕様が廃止されたのか
大規模な破壊的変更が行われる最終的なバージョンといわれているSwift 3.0がついに正式リリース。多数の変更から「廃止」となった言語仕様にフォーカスを当て説明する。
5. Swift 3.1のリリースプロセスおよびそれに含まれる変更内容の紹介(前編)
Swift 3.1のリリースが2017年春に迫ってきた。今回は前後編に分けて、そのリリースプロセスや変更内容を解説する。前編ではリリースプロセス/互換性/開発版のSwiftを利用する方法を取り上げる。