この記事の要点
- Google Summer of Code(GSoC)は、新しいコントリビューターをオープンソース開発の世界に迎え入れることを目的とした長期のメンターシッププログラムで、Swift プロジェクトは継続的に参加しています。
- 2023 年は 3 件のプロジェクトを実施し、いずれも担当した内容を成功裏に完了しました。
- 成果は、サーバーサイド向けの Memcached 接続抽象、SwiftParser のインクリメンタル再パース、key path リテラル式の型チェック・診断改善の 3 件です。
プログラムの目的
GSoC は、メンターと組みながら Swift とそのエコシステムへのコントリビューションに取り組むことで、オープンソース開発に不慣れな人がはじめの一歩を踏み出せるようにする長期のメンターシッププログラムです。2023 年は 3 件のプロジェクトを走らせ、3 名のメンティーがそれぞれの担当内容を完遂しました。以下では、各プロジェクトの内容を紹介します。
成果
Swift Memcache
メンティー: Delkhaz Ibrahimi さん / メンター: Franz Busch さん
このプロジェクトは、サーバーサイド Swift のエコシステム向けに、ネイティブな Memcached 接続抽象を開発することを目標としました。接続は SwiftNIO を使って実装され、ネイティブな Swift Concurrency の API を提供し、サーバーサイドの他のエコシステムともよく統合されます。Swift Concurrency を使う利点は、structured concurrency によってキャンセルやエグゼキュータの認識が得られ、将来的に分散トレーシングとも容易に統合できる点にあります。
プロジェクトでは Memcache の meta command プロトコルを実装し、基本的な get と set の機能を提供することに注力しました。次は、新しい MemcacheConnection 型を使ってキーに対する値を set し、get する例です。
// Instantiate a new MemcacheConnection with host, port, and event loop group
let memcacheConnection = MemcacheConnection(host: "127.0.0.1", port: 11211, eventLoopGroup: .singleton)
// Initialize the service group
let serviceGroup = ServiceGroup(services: [memcacheConnection], logger: logger)
try await withThrowingTaskGroup(of: Void.self) { group in
group.addTask { try await serviceGroup.run() }
// Set a value for a key.
let setValue = "bar"
try await memcacheConnection.set("foo", value: setValue)
// Get the value for a key.
// Specify the expected type for the value returned from Memcache.
let getValue = try await memcacheConnection.get("foo", as: String.self)
}
get と set の土台ができたあと、delete、append、prepend、increment、decrement への対応が追加され、最後にキーの time-to-live を確認・更新する機能も加わりました。
この MemcacheConnection 型は、コネクションプーリングやリトライ、ノード間でのキー分散といった機能を備えた上位の MemcacheClient を実装するための土台になります。ただし、そうしたクライアントの実装は今回の GSoC プロジェクトの範囲外とされました。
SwiftParser のインクリメンタル再パース
メンティー: Ziyang Huang さん / メンター: Alex Hoppen さん
このプロジェクトは、エディタのシンタックスハイライトのように小さな編集が繰り返し適用される場面に向けて、SwiftParser の性能を改善することを目標としました。インクリメンタルなパースを追加し、変更されていない構文木の一部を再利用することで、大きな性能向上が見込めます。
このプロジェクトで最も難しかったのは、ソースを正しくパースし続けることでした。たとえば次のコードを考えます。
foo() {}
someLabel: switch x {
default: break
}
このソースは FunctionCallExprSyntax と LabeledStmtSyntax としてパースされます。ここから “switch x” の部分を削除したとき、素朴な実装では編集が触れていない foo() {} を関数呼び出しとしてそのまま再利用してしまうかもしれません。しかしこれは正しくありません。削除後は someLabel のブロックが foo() {} のラベル付き trailing closure になるためです。
この問題を解決するため、最初のパース時に各構文ノードについて影響を受けうる範囲を記録する追加情報を集めておきます。その情報を使って foo() の関数呼び出しを正しく再パースし、someLabel をラベル付き trailing closure としてその呼び出しに含めます。
この実装により、インクリメンタルにパースする場合でパースが約 10 倍 高速化される一方で、通常のパースでの性能低下は 2〜3% にとどまりました。
key path 推論と診断の改善
メンティー: Amritpan Kaur さん / メンター: Pavel Yaskevich さん
このプロジェクトは、key path リテラル式の型チェックの性能・診断の改善と、SE-0249 で言語に導入された「関数としての key path」のような新機能の改善に焦点を当てました。
コンパイル時、key path 式のルートと値はこの文脈から key path の型を解決するために順番に型チェックされていました。しかし、型チェッカーが key path のコンポーネントの型やそれらの関係、key path の能力をどう評価するかという設計のために、理解しづらいコンパイルエラーが生じたり、正しいはずの Swift コードの型チェックに失敗したりすることがありました。
このアプローチの問題のいくつかは、次のコード例で示せます。
struct User {
var name: Name
}
struct Name {
let firstName: String
}
func test(_: WritableKeyPath<User, String>) {}
test(\.name.firstName)
コンパイラは次のエラーを出していました。
error: key path value type 'WritableKeyPath<User, String>' cannot be converted to contextual type 'KeyPath<User, String>'
test(\.name.firstName)
^
このエラー診断には複数の問題があります。実際の文脈上の型は WritableKeyPath であり key path は読み取り専用と推論されるべきこと、ソース情報が失われていること、関数 test の呼び出しの引数の問題をコンパイラが指摘できていないことです。
これらの問題に対処するため、key path リテラル式の型チェックの設計を見直しました。まず key path のルートの型を推論してその情報をコンポーネントに伝播させ、コンポーネントに基づいて能力を推論してから key path 式の型を設定する、という流れです。これにより文脈上の型の不一致を診断しやすくなり、これまで失敗していた変換もサポートできるようになりました。このアプローチは性能の改善にもつながります。文脈が key path を期待し、かつルートの型が開発者によって明示されているか文脈から推論できる場合にのみリテラルが解決されるためです。
新しいアプローチによって、コンパイラは次の診断を出すようになりました。
error: cannot convert value of type 'KeyPath<User, String>' to expected argument type 'WritableKeyPath<User, String>'
test(\.name.firstName)
^
学び・今後
3 名のメンティーはいずれも担当プロジェクトを完遂し、その成果はサーバーサイドのエコシステムからエディタのパース性能、コンパイラの型チェック・診断体験まで広い範囲に及びました。GSoC を通じて Swift プロジェクトが新しいコントリビューターを継続的に迎え入れていることがうかがえます。前年の取り組みについてはSwift Summer of Code 2022 まとめでも紹介されています。