1. はじめに
2022/05/18に開催されたQiita Night Swift6がもたらす開発者体験を予測しよう!に「新卒エンジニアによるSwift6与太予想」というタイトルで登壇させていただきました。動画、スライドに関しては以下のURLから確認できるので、ぜひご覧ください!
この記事では、登壇内容のまとめやお話しできなかった部分を書いていこうと思います!
2. 非同期処理とジェネリクスのアップデートについて
まず初めに、非同期処理とジェネリクスのアップデートについて話しました。この2つに関しては、WWDC21の動画やSwift Evolutionなどで、どのようにアップデートされるかについて具体的に言及されています。
そのため、Swift6で大きなアップデートが入ることは現実的であると言えます。
非同期処理のアップデートについて
WWDC21で発表されたasync/awaitについてです。async/awaitは、Swift5.5から正式に利用可能となり、もともとcompletionなどのコールバック関数を用いて書いていた非同期処理や並行処理のコードを簡潔に記述できるようになりました。下記のコードは、最近Laravelも触っており、興味本位で簡単に作ったAPIサーバーからUserIDを取得する処理を書いてみました。
// ① async/awaitを用いてUserIDを取得する処理
func fetchUserId(email: String) async throws -> String {
guard let url = URL(string: "http://localhost:8888/api/login?email=\(email)") else {
return String()
}
var request = URLRequest(url: url)
request.httpMethod = "GET"
let (data, response) = try await URLSession.shared.data(for: request)
if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode != 200 {
}
do {
let userId = try JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions.allowFragments)
let stringUserId = String(describing: userId)
return stringUserId
} catch let decodeError {
throw APIError.decode(decodeError)
}
}
// ② completionを用いたUserIDを取得する処理
func fetchUserId(email: String, completion: @escaping(Result<String?, APIError>) -> Void) {
guard let url = URL(string: "http://localhost:8888/api/login?email=\(email)") else {
return
}
var request = URLRequest(url: url)
request.httpMethod = "GET"
let task = URLSession.shared.dataTask(with: request, completionHandler: { (data, response, error) in
if let error = error {
completion(.failure(APIError.unknown(error)))
}
guard let data = data, let response = response as? HTTPURLResponse else {
completion(.failure(APIError.noResponse))
return
}
if case 200..<300 = response.statusCode {
do {
let userId = try JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions.allowFragments)
completion(.success(String(describing: userId)))
} catch let decodeError {
completion(.failure(APIError.decode(decodeError)))
}
} else {
completion(.failure(APIError.server(response.statusCode)))
}
})
task.resume()
}
async/awaitで書いたfetchUserID()メソッドは18行と短文で書けているかつ、メソッド内のロジックもわかりやすくなっており、コードの可読性も向上しているように思えます。一方、従来の方法のcompletionを用いた方法は、30行とasync/awaitと比較すると12行も多くなり、メソッド内のロジックもわかりづらいように思えます。また、async/awaitの方ではエラーをthrowすることが義務付けられているため、エラーハンドリングが強制され、エラーの考慮漏れの防止にもつながるというメリットがあります。
また、Swift6から@asyncHandlerというアノテーションが追加予定であり、IBActionなどで使用することで、Taskで囲む必要がなくなりそうです!
@asyncHandler
@IBAction func didTapButton(_ sender: UIButton) {
do {
_ = try await fetchThumnail(for: "xxx")
} catch(let error) {
print(error)
}
}
ジェネリクス関連のアップデートについて
ジェネリクスに関しては、swift.org内で提唱されたImproving the UI of genericsの内容に沿って、アップデートされており、今後もアップデートされていくと思われます。最近のアップデートだと、Opaque Result TypeやOpaque Argument Type、Existential anyなどがあります。
① Opaque Result Type
Opaque Result TypeはSwift5.1から導入された機能であり、ジェネリクスの問題点の一つであった型レベルでの戻り値の抽象化の問題を部分的に解決するべく、導入された。(詳しくはSwift Evolution: SE-0244 Opaque Result Typesを参照)
また、Swift5.7ではOpaque Result Typeを活用した拡張機能も追加予定。(詳しくはSwift Evolution: SE-0328 Structural opaque result typesを参照)
問題点
プロトコルを戻り値として指定してあげるとき、戻り値に具体的な型を指定しても、戻り値の型は具体的な型ではなく、プロトコルの型となってしまい、具体的な型の情報は消えてしまうといった問題が存在していた。
func callee() -> Numeric {
Int(30)
}
// Int型の型情報が消去されているため、加算できない
func caller() {
callee() + callee() // ⛔️ Compiled Error: Binary operator '+' cannot be applied to two 'Numeric' operands
}
解決法
上記の問題をOpaque Result Typeを利用することで、コンパイルエラーを防ぎ、かつ実行することが可能となりました。
func callee() -> some Numeric {
Int(30)
}
// Int型の型情報が保持されている
func caller() {
callee() + callee() // ✅ 60
}
② Opaque Argument Type
Opaque Argument TypeはSwift5.7から導入予定の機能であり、ジェネリック関数の複雑な表記法を簡潔にするために、導入されます。(詳しくはSwift Evolution: SE-0341 Opaque Parameter Declarationsを参照)
問題点
ともと具体的な型を用いて実装していた関数を抽象化するためにジェネリック関数を用いていましたが、最終的に出来上がったジェネリック関数のコード上の見た目は、初めの関数の見た目と比較すると、ほぼ違うものとなってしまっています。そういった問題点を解決すべく、後述するanyと対比する意味合いも込めて、引数部分にsomeを指定してあげる機能が導入される予定です。
具体的な問題の流れは以下のとおりです。
① 具体的な型を持った関数
protocol Animal {}
struct Cat: Animal {}
struct Dog: Animal {}
func call(_ animal: Cat) {}
func call(_ animal: Dog) {}
② ジェネリック関数化
func call<A: Animal>(_ animal: A) {}
元の関数と比較するとやはり関数の形としては異なって見えます。
解決法
そこで、someを用いることで、②ジェネリック関数化と同じ挙動をする関数を記述できるようにしました。(someはジェネリック関数のシンタックスシュガーといえます。)
// 具体的な型を持った関数
func call(_ animal: Cat) {}
// ジェネリック関数
func call<A: Animal>(_ animal: A) {}
// Opaque Argument Typeを用いた関数
func call(_ animal: some Animal) {}
③ Existential any
Existential any、つまり「存在型を示すany」は、Swift5.6から導入されたものであり、制約としてのプロトコルと存在型としてのプロトコルの区別をしっかりつけるために導入されました。(詳しくはSwift Evolution: SE-0335 Introduce existential anyを参照)
問題点
プロトコルの役割として「制約」と「型」の2つが存在します。制約としてのプロトコルを利用するときはメモリのオーバーヘッドが発生しませんが、 「型」としてのプロトコルを利用するときは存在型と呼ばれ、メモリのオーバーヘッドも発生してしまいます。現在もSwiftでは制約としてのプロトコルと型としてのプロトコルを区別することは難しく、初学者に対しても高いハードルを設定してしまっているという問題点があります。
// 型としてのプロトコル
func call(_ animal: Animal) {}
// 制約としてのプロトコル
func call<A: Animal>(_ animal: A) {}
上記のコードだと、まだわかりやすい方だが、実際の構造体やクラスを型として指定している変数のなかにプロトコルを型として指定しているものが混ざっていると非常に見分けづらいものとなります。
protocol Animal {}
struct Cat: Animal {}
struct Dog: Animal {}
let cat: Cat = .init()
let dog: Dog = .init()
let animal: Animal = Bool.ramdom() ? cat : dog
解決方法
そこで、プロトコルを型として使用している部分に対してanyキーワードをつけることで、型としてのプロトコルであることを明示しました(現在は、anyがなくてもコンパイルエラーにならないが、Swift6以降はanyをつけないとコンパイルエラーになる予定)。
// 型としてのプロトコル
func call(_ animal: any Animal) {}
let animal: any Animal = Bool.ramdom() ? cat : dog
また、Opaque Argument Typeのsomeと比較することで、someのときは制約としてのプロトコル、anyのときは型としてのプロトコルであるとわかりやすくなりました。
// 制約としてのプロトコル
func call(_ animal: some Animal) {}
// 型としてのプロトコル
func call(_ animal: any Animal) {}
詳しくは、以下の記事を参考・動画にしてみてください!
(近いうちにジェネリクス関連の知見をまとめた記事をあげようかなと思っています...!)
この流れで、Swift6ではリバースジェネリクスをより実現すべく、Protocolに対する制限も徐々に緩和されていくのではないかと考えられます!
ARKitについて
先日のGoogle I/Oでは、ARCore Geospatial APIという、空間スキャンなしでアンカリングできるアップデートが発表されました。詳しくは以下の動画を参考にしてみてください。
一方、ここ最近のARKitのアップデートとしては大きなものは見られない感じがします。ARKitも、ARCoreのアップデートの影響を受けて、MapKitとの連携などをしてくれると面白くなるのかなと思っています。
3. さいごに
今回はいろいろお話しをしましたが、どのようにアップデートされるかの答えは今年のWWDC22にあると思います!(Swiftly approachingともうたっています。)
新製品の発表も含めてすごく楽しみです!!