今回は次の話題に移っていく前に、思いのほか Swift 5.7.0 〜 5.7.1 まわりで some
や any
が大きく整備された感があるのに気がついたので、まずはそこから見ていってみようと思います。これらいわゆる 不透明型
と 存在型
のお話、特に後者はこれまでにも幾度と話題にしましたけれど、その時の話と大きく変わったところもあるので、そんな辺りを確認していく回にしますね。
今回は公募が直前ながらも間に合いまして、ゆめみ社外な人の参加なしでの開催になるかもしくは飛び入りでどなたか参加してくださるかもしれない運びとなりました。どうぞよろしくお願いしますね。
——————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #178
00:00 開始 00:25 今回の展望 02:39 不透明な型と存在型についてのおさらい 02:54 存在型とは 03:58 型としてのプロトコル 05:44 関連型を持つプロトコルの存在型 06:53 不透明な型とは 09:14 存在型とボクシング 10:36 不透明な型とボクシング 12:08 共用体っぽい? 12:53 不透明な型から型キャスト可能 13:46 不透明な型は一意に定まる 17:23 配列の要素に不透明な型を使う 20:17 プロトコル型をジェネリクスで扱える 23:32 存在型の暗黙展開 25:43 隠蔽されている型でも展開可能 29:10 存在型が自己準拠していると困る場面 31:30 暗黙展開しても静的メンバーを使えないのは変わらない 32:54 標準ライブラリーにおける主要関連型 34:52 クロージング ———————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #178
では始めていきますね。今日は、本来の流れであればエラーハンドリングの章に入る予定だったのですが、昨日、表参道で勉強会を行いました。その中で話題になった「存在型(Existential Type)」について、場面が盛り上がりました。しかし、普通にプレイグラウンドでコードを書いてみたら動作が期待通りにならず、調べてみたところ、Swiftのバージョン 5.7.0 から存在型と不透明型に関する改善が行われていたことが分かりました。
そのプロポーザルの内容まで完全に見ていませんが、この改善によってGenericsがより一般化され、多くの開発者にとってもGenericsが非常に使いやすくなる、大きな変化だと思います。これはコードを書く人々にとって非常に重要なタイミングポイントだと感じております。
今日は、せっかくですので、この時間を使って存在型と不透明型について詳しく見ていきましょう。特に資料は用意していないので、行き当たりばったりの話になるかと思いますが、途中で疑問点があれば気軽に質問してください。コメントでも遠慮なく投げ込んでくださいね。
まずは、不透明型と存在型の基本的な説明から始めます。まず存在型(Existential Type)についておさらいしましょう。通常、プロトコルが型として使えるという話です。例えば、以下のようなコードがあります。
protocol SomeProtocol {
// プロトコルの定義
}
struct SomeValue: SomeProtocol {
// 構造体の定義
}
// 存在型
let value: SomeProtocol = SomeValue()
このように、SomeValue
がSomeProtocol
に準拠している場合、let value: SomeProtocol
として存在型を利用することができます。これにより、様々な型を共通のプロトコル型として扱うことが可能になります。
次に不透明型についてですが、まずは存在型についてしっかり理解することが重要です。存在型とは、プロトコルに準拠する複数の型を同じプロトコル型として扱えることを指します。ここで重要なのは、存在型を使うことで型の具象的な詳細を隠蔽し、共通のインタフェースを通じて操作することができることです。
例えば、以下のようなコードがあったとします。
class SomeClass: SomeProtocol {
// クラスの定義
}
let value1: SomeProtocol = SomeValue()
let value2: SomeProtocol = SomeClass()
このように、SomeValue
とSomeClass
の両方がSomeProtocol
に準拠している場合、異なる具体型を共通の存在型として扱うことができるのです。 なので、配列などではプロトコルを活用することで、異なる型であっても同じもののように扱えるというメリットがあります。これがプロトコルを用いた存在型の利点です。ここからどう生かしていくかは、経験に伴って広がっていく部分であり、少し難しいところでもあるのですが、存在型について理解しておくことは重要です。
存在型について、簡単な例を挙げます。プロトコルがアソシエイティブタイプを持っている場合、そのプロトコル型を存在型として扱うためには、タイプエリアスを使ってアソシエイティブタイプを具体的な型に指定します。例えば、以下のように書くことができます。
protocol SomeProtocol {
associatedtype AssociatedType
func someMethod(_ value: AssociatedType)
}
struct Example: SomeProtocol {
typealias AssociatedType = Int
func someMethod(_ value: Int) {
print(value)
}
}
Swift 5.7から、アソシエイティブタイプを持っているプロトコルでも、Any
を使って存在型として扱えるようになりました。これにより、例えば次のようにコードを書くことができます。
func someFunction() -> Any {
return Example()
}
この場合、SomeProtocol
に準拠した型であれば、返り値として利用可能です。また、SwiftUIなどを利用していると不透明型(Opaque Type)にも馴染みがあるかと思います。例えば以下のように簡単な例を挙げると、
func makeInt() -> some View {
Text("Hello, World!")
}
不透明型を使うことで、リターンする型が特定のプロトコルに準拠していることを保証しつつも、具体的な型を隠すことができます。これによって、インターフェースを保ちながら内部の実装を変更しやすくなります。
ここでもう一度、存在型の例に戻ります。例えば、以下のように存在型としてプロトコルを返す関数を考えてみます。
func someFunction() -> Any {
return Example()
}
この関数がSomeProtocol
に準拠した型のオブジェクトを返すことで、それが持つメソッドを実行することが可能になります。次のように、SomeProtocol
のメソッドを実行することができます。
let someInstance = someFunction() as? SomeProtocol
someInstance?.someMethod(10)
大切なポイントは、プロトコルによって統一されたインターフェースを通じて、異なる実装を柔軟に扱えることです。存在型を使用することで、プロトコルに準拠した任意の型を統一的に扱うことができ、コードの柔軟性と再利用性を高めることができます。
ただし、存在型を使用すると、オブジェクトがボクシングされることで、メモリ効率がやや低下する可能性がある点にも注意が必要です。例えば、オブジェクトのサイズが8バイトや64ビットなど具体的なメモリサイズとなり、そのラッパーを通じてプロトコルの型として扱うため、余分なメモリが消費される可能性があります。この点を理解した上で、プロトコルと存在型を適切に活用してください。 データの扱いに関して、例えば40バイト取られてしまうことがありますが、実際には8バイトで十分な場合が多いです。しかし、より多くのメモリが必要になることもあります。例えば、どこかのヒープを使う場面などです。大きなメモリを取られるのは問題ではありませんが、透明型というものがあります。
透明型は、内部ではどういう型を返すかが秘密になっていますが、外側ではちゃんと扱うことができます。これを使うと、メモリサイズが最適化されます。例えば、サイズが8バイトになります。これが透明型を使った場合の例です。透明型を使うと、例えば表面上は何も変わっていないように見えますが、裏では最適化が行われ、より効率的にリソースを使用します。こうすることで、高パフォーマンスを維持しながら動作します。
「ユニオン」に関する話もありますが、ユニオンは非常に強力です。ユニオンについては後で調べてみようと思います。ユニオンは使い方によってはリスクも伴うため注意が必要です。
Swift言語の型において、例えばSomeProtocol
という透明型のプロトコルがあります。このプロトコルの特徴として、ある条件に応じて異なる型を返すみたいな処理はできません。透明型にはこういった特性がありますので、特定の条件での最適化が難しい場合もあります。その場合にはAny
を使うことが一般的です。
また、配列に関しても透明型を使用するとサイズを特定できないため、配列の要素が一定の型でないと問題になることがあります。例えばBinaryInteger
の配列を扱う場合、このプロトコルはアソシエイティブタイプを含むので、そのままでは使用できず、Any
を使う必要があります。これにより、異なる整数型が同じ配列に含まれるようになるわけです。
以上が透明型やSomeProtocol
、BinaryInteger
の配列の取り扱いに関する説明です。他にもさまざまな最適化方法や注意すべき点がありますので、これからも勉強を続けていきましょう。 ここで引っかかっている部分があって、変数が重複しているようです。それで、エラーが出ています。プロトコルに関連する記述が残っているので、これは削除できます。そして、アサートが引っかかる部分も修正します。これでようやく動くかなと思います。
動きましたね。このようにすると、サイズが異なってもボクシングでうまく統一されます。これは存在型で以前はできていましたが、サムプロトコルではランタイムエラーで落ちていたことがあります。しかし、Swift 5.7.1からこれが可能になりました。ただし、今のコードではダメです。どういうエラーか見てみましょう。
とりあえず、さっき話したように、サムプロトコルの場合、裏では型そのものが扱われているという話がありました。Swiftの配列は型を決めたらその型しか入れられないため、IntとInt8とInt16は混ぜられません。しかし、全てInt型であれば、Swift 5.7.1から動くようになりました。
その結果、ランタイムエラーで落ちたようです。どこかがおかしいのかを見てみましょう。ログファイルを確認してみましょう。ログファイルで確認するとわかるかもしれません。具体的には、プロトコルを使って昨日作ったコードでやりました。それをエクステンションでIntに対して適用しましたが、エクスコードのバージョンが対応していないようです。
今の状態だと、安定して動かないので、使わないほうが良さそうです。一度置いておいて、別の話に進みたいと思います。
例えば、プロトコルPがあって、ストラクトSがプロトコルPに準拠しているとします。関数サムシングでジェネリック型を使って、例えば型TがプロトコルPに準拠している場合、このインスタンスをプロトコル型として持ち、サムシングに渡すとします。以前はSwift 5.6ではエラーが発生していましたが、今は動くようになりました。
以前は、エクジステンシャル型がそれ自体のプロトコルには準拠していないとみなすルールがありましたが、それが撤廃されました。これにより、例えば配列をプロトコル型として扱うことができるようになり、異なる型が混ざっている場合でも、フォーループで扱えるようになりました。これがSwift 5.7.0から追加されています。これは非常に便利です。 Swiftでは、特定の条件下でSome
やAny
が多く使われるようになります。特にAny
は重要です。Any
がアソシエイティブタイプとして必要となる場合があります。この場合、アソシエイティブタイプをAny
にすることで、正しく動作します。
例えば、以下のようにプロトコルの存在型を使います。
protocol P {
associatedtype Value
var value: Value { get }
}
ここでValue
をAny
に設定します。そして、ジェネリック型に渡すことで、サイズを確認することができます。たとえば以下のコードを考えてみます。
struct Example<T> {
var value: T
}
let example = Example(value: 10)
print(MemoryLayout.size(ofValue: example))
ここでサイズを出力するとしますが、始めは予期しない値が出ることもあります。例えば、print
文を使わないと正しいサイズが得られない場合があります。
初めに40と出力されることを期待してプリントすると、0が出力されることがあります。これはジェネリックの展開の結果かもしれませんが、サイズを確認するための式の中で適切に使われていないためです。正しく実験するためには、もう少し具体的なプロパティを追加して試して見ることができます。
struct Example<T> {
var value: T
var extraValue: Int
}
let example = Example(value: 10, extraValue: 1)
print(MemoryLayout.size(ofValue: example))
プロパティを追加することで、構造体のメモリサイズが期待通りに変化し、動作がより理解しやすくなります。次に、関数を作ってジェネリックTを返す例を考えてみます。たとえば以下のような関数を定義します。
func getValue<T>(_ value: T) -> T {
return value
}
この関数では、インターナルタイプに準拠させることで、めちゃタイプの受け取ったパラメータの動作を確認できます。
存在型もプロトコルとして自己準拠させることができます。たとえば、S
のインスタンスがプロトコルP
に準拠しているかどうか判定する演算では、Any
を使うことができます。
また、歴史的に存在型がプロトコルに準拠していないと見なされていた背景も存在します。これはスタティックメンバーの存在による問題です。static
のメソッドが影響を及ぼす場合、これが理由で対応が複雑になることがありました。しかし、現在のバージョン(例えば5.7.0以降)では、この仕様も改善されてきました。
これからAny
やその他のジェネリック型が多用されることを考慮に入れつつ、プロトコルや型の扱いを練習していくことで、より柔軟で強力なプログラムを書けるようになるでしょう。 スタティックアクションを使って実装を進めていくとき、Genericsを使った場合には、関数が呼ばれた際に具体的な型が分かるので、そのバリューがスタティックバリュー(T)である場合には、アクションの実装が可能になります。しかし、内部的なタイプで準拠していないことが問題になることがあります。例えば、スタティックファンクションのアクションを使用しようとするとエラーが発生する場合があります。
このエラーが具体的にどこのエラーなのかを確認してみると、リターンの箇所でエラーが出ているだけだと分かります。これを修正すると正常に動作するようになります。例えば、S
の時には1、R
の時には2といった形できちんと動作します。
さらに、関数 サムシングアクション
では、存在型として値を受け取るときの操作も説明されていました。この際に、T型のアクションを呼び出そうとすると、40行目の関数内ではどのアクションかがわからないため、コンパイル時にエラーが発生します。この問題は、以前は門前払いされていましたが、現在のコードベースでは改善され、スタティックメンバーを呼び出せないだけで済むようになりました。
Swift 5.7.0においてプロトコルが追加され、独自のプロトコル、例えば SomeProtocol
にアソシエーティブタイプ X
を設定することが可能になりました。このプロトコルを用いると、バリューとして特定のプロトコルの型を使用できるようになります。さらに、言語仕様の変更でプライマリーアソシエーティブタイプを設定することによって、特定の条件に基づく書き方が可能になりました。特に シーケンスプロトコル
の定義が変更され、プライマリータイプが適用できるようになった点は大きな進歩です。こうして、SomeSequence<Int>
のように指定して、アクションに対して Int型
のシーケンスを渡すことができます。
このように、Genericsを使った関数が普通に使われるようになり、今後もどんどん使用されることが予想されます。昨日はいろいろな壁にぶつかりながらも、新しい動きを確認できたので、紹介しておきました。ただ、Someの配列が昨日できたはずなのにできなかったのは少し残念でした。それについては後で調べて面白い発見があればまた紹介します。
では、本日はこれで勉強会を終わりにしますね。お疲れさまでした。ありがとうございました。