Swift 5.9から導入されるValue and Type Parameter Packsについて思いを馳せる
1. はじめに
Swift 5.7以降では some
/ any
という概念を用いることで、Swift 5.6以前では取り扱いが難しかった associated typeを持っていたり、Self ReqruirementsなProtocolなども適切に抽象化できるようになりました。
protocol TweetDataModel {
associatedtype TempType
func markAsRead() -> Self
}
struct ImageTweetDataModel: TweetDataModel {
typealias TempType = Image
func markAsRead() -> ImageTweetDataModel { ~~~ }
}
struct LocationTweetDataModel: TweetDataModel {
typealias TempType = LocationDataModel
func markAsRead() -> LocationTweetDataModel { ~~~ }
}
let tweets: [any TweetDataModel] = [
ImageTweetDataModel(),
LocationTweetDataModel()
]
その一方で下記のようなGenericsを用いたクラスについては取りまとめることが難しいままでした
class APIClient<APIResponse> {
func fetch() async -> APIResponse {
// Call an API...
}
}
// APIClient<ItemApiResponse>やAPIClient<LoginApiResponse>をナチュラルに読みやすい形でまとめて取り扱うことはできなかった
Swift 5.9では、SE-0393-Parameter-PacksというProposalにて、 Value and Type Parameter packsという概念が導入され、上記のようなGenericsを用いたコードを便利に取りまとめて扱うことができるようになります。
本記事では、Proposalで提案されている内容をベースに、どのような変化がSwift 5.9で導入されるのか、具体例を出しながら、ご説明しようと思います。
2. Swift 5.8までの世界での限界
SwiftはProtocol指向でValue TypeとValue Semanticsを中心としたプログラミング言語ですが、他のOOP言語と同じくClassによるポリモーフィズムもサポートしています。
(もちろん、Class TypeとセットでReference Semanticssもサポートされています)
このように、Protocolを利用してポリモーフィズムを表現できる一方で、Swift黎明期からあるAPIClientなどの実装では下記のようにGenericsを利用して返却値型の指定をしているコードがあるかもしれません。
class APIClient<APIResponse> {
func fetch() async -> APIResponse {
// Call an api
}
}
let itemApiClient = APIClient<ItemApiResponse>()
let item = await itemApiClient.fetch() // ItemApiResponseを取得
このようなGenericsを用いたコードの場合、複数のAPIClientをまとめて取り扱おうとして、下記のような記述をしてもコンパイルエラーになってしまいました。
let apiClients: [any APIClient] = [APIClient<ItemApiResponse>()]
// ↑'any' has no effect on concrete type 'APIClient'になる
let apiClients: [APIClient<Any>] = [APIClient<ItemApiresponse>()]
// ↑ Cannot convert value of type 'APIClient<ItemApiResponse>' to expected element type 'APIClient<Any>'になる
これは落ち着いて考えてみれば当たり前の話で、Generics型は型を注入することによって型になるため、指定する型を抽象化すると、それはもう別の型として取り扱うしかないからです。
このようなことから、some / anyが導入されたSwift 5.7以降も、Genericsを用いたコードについては指定する型を抽象化してまとめて取り扱うような事はできず、APIClientの処理を抽象化したいといったような問題の時、我々はそれぞれの処理を愚直に待ち合わせるしか方法がありませんでした。
(Type Erasure
を使うことで複数のインスタンスを取りまとめることは可能ですが、型情報が消失してしまうため、ほとんどのユースケースであまり良い解決策とはならないと考えています。)
3. Swift 5.9以降
Swift 5.9では、Swift5.8以前では不可能だったGenericsに指定する型パラメータの抽象化がサポートされます。
詳細についてはSE-0393-Parameter-PacksというProposalに記載されていますが、Type Packとは下記のようなものです
[ItemApiResponse, UserApiReseponse, DestinationApiResponse]
Value Packとは下記のようなものです。
[ItemApiReseponse(), UserApiResponse(), DestinationApiResponse()]
便宜上[
と]
を利用していますが、概念としてはタプルに近いもので順番に意味があります。
上記の例の場合でいうと、0番目のポジションでは、ItemApiResponse方が指定されており対応するインスタンスはItemApiResponseのインスタンスが指定されており、1番目のポジションでも...という具合です。
プログラム上では下記のようにeach
というAttributeをつけることで、Type Packの利用を宣言し、Genericsに指定する型を抽象化することができます。
func batchApiRequest<each Response>
次に注入される型を抽象化するには下記のように記述することで、Genericsを利用したAPIClientクラスでも、clientsに複数引き渡すことが可能になります。
func batchApiRequest<each Response>(clients: repeat APIClient<each Response>)
次に返却値を下記のように記述することで渡した順番で結果を受け取ることが可能です
func batchApiRequest<each Response>(clients: repeat APIClient<eachResponse>) -> (repeat each Response)
利用する際は下記のように記述します。
let results = batchApiRequest(clients: APIClient<ItemApiResponse>(), APIClient<UserApiResponse>(), APIClient<DestinationApiResponse>())
print(results)
//↑ ItemApiResponse, UserApiResponse, DestinationApiResponseのインスタンスがそれぞれ格納されている
let itemApiResponse = results.0
let userApiResponse = results.1
let destinationApiResponse = result.2
上記のように each
と repeat
Attributesを利用することで、Genericsに指定する型を抽象化し、Genericsのコードであっても便利に抽象化して取り扱うことが可能になりました。
4. 終わりに
以上で触れてきたようにSwift 5.9以降ではeach
と repeat
AttributesによりGenericsのコードであっても便利な抽象化を利用することができるようになります。
Swiftの言語思想としてはsome
/ any
を用いたProtocolによるポリモーフィズムを推しているとは思うのですが、他OOP言語話者にもわかりやすいClassとGenericsを用いたコードを、便利に抽象化して取り扱うことができ、過去のコード資産を活かしていける今回のUpdateは個人的にとても良いと考えています。
主にApple系プラットフォームでの開発に利用されているSwiftが日々進化を続けているのが嬉しく、実際にParameter packを利用して開発を始める日々が待ちきれませんね。
Discussion