今日は前回の余談の続きから。ゆめみ iOS 研修課題
を眺めていたら不意に出てきた Equatable
の適用判断周りで感覚的に難しく感じたところがあったので、みんなと意見交換してみたくなったのでそれを行ってみることにしますね。それらを見てから、他に気になるところはないか研修課題のコードを眺めてみたり、引き続き 循環参照
に見られる問題点のあたりを詳しく見ていく予定でいます。どうぞよろしくお願いしますね。
——————————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #218
00:00 開始 00:18 前回の代入演算子についてのおさらい 02:22 代入演算子なのに代入しないのはアリ? 03:40 驚きを与える API は推奨されない 05:36 やはり代入演算子っぽい 08:52 演算子の独自定義は慎重に 09:25 株式会社ゆめみ iOS 研修課題で気になったところ 10:28 Equatable の規模感 12:35 今回の Equatable は、テストを書きやすくする想定 13:46 理屈と実際とのバランス感の難しさ 15:34 Identifiable 16:14 本来 Hashable は等価比較性を持たない 17:23 理屈通りが正しいとは限らない 17:48 Equatable にしなかったときの最善の手は? 20:59 ID での一致性判定は、今回は不適切 22:09 Codable で比較するのは強引な印象 22:37 独自のテスト関数を作る手はある 23:39 落としどころが難しい気がする 24:32 CaseIterable に持つ印象 25:01 全候補を列挙したいときに役立つ 26:48 allCases という名前は適切? 29:06 汎用的なものは冗長になりがち ———————————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #218
勉強会を再開します。前回は脱線してしまったので、その続きを進めます。今オプショナル型で定義されている構造体の変数に対して、アンラップしてから代入している箇所について話しました。オプショナル型の変数に対しても、適切に対処できるように属次的な演算子を自分で定義する方法も紹介しました。
ここで前回のフォローアップとして、プロパティを持つ構造体を定義し、それに対して演算子を定義する例を紹介しました。例えば、アサインメント
という演算子を定義し、その演算子を使用してプロパティに値を設定する方法について説明しました。しかし、このアサインメント
という名称や使い方については批判もありました。
具体的なコード例を挙げましょう。
struct Sample {
var value: Int?
}
var sample = Sample()
// `アサインメント`演算子を定義
infix operator ≈
func ≈ (left: inout Int?, right: Int) {
left = right
}
sample.value ≈ 10
このように、≈
という演算子によってオプショナルな変数に値を代入できます。しかし、これが適した使い方であるかどうかは疑問が残るところです。
特に、アサインメント
という名前は左辺と右辺に値を割り当てるという印象を与えるため、混乱を招く可能性があります。「左辺と右辺に値を割り当てる」という表現は必ずしも正しくないので、適切とは言えないでしょう。
サイドエフェクトが発生するかどうかも重要な点です。通常、=
(イコール)はサイドエフェクトを伴うとみなされますが、==
ならばそうは思われないでしょう。演算子の定義を見て、特定の操作がサイドエフェクトを伴うかどうかを判断するのは難しいものです。
APIデザインガイドラインにも記載されているように、ユーザーが驚くような動作をするのは良くないです。演算子を定義する際には、その名前や動作が直感的であるかどうかを慎重に検討する必要があります。
例えば、オプショナルチェーニングを考慮するような演算子名にすれば問題は少なくなるでしょう。しかし、それでも具体的なコードの動作を把握するのは難しいかもしれません。
最後に、演算子が評価されるかどうかを把握するのは難しいかもしれません。要するに、オプショナル型の場合、その値がnil
であったときに演算子が実行されない状況を想定する必要があります。
このような注意点を踏まえつつ、今後の演算子定義や利用方法を検討していただければと思います。今回の勉強会では、この点に関してもう少し掘り下げていきますので、次回も引き続きよろしくお願いします。 演算子に関する話ですが、独自定義を行う際には慎重であるべきです。特に、アサインメントを想定する際には、変数の値が適切に変化するような演算子を使う必要があります。つまり、値がインアウトになっている演算子を適切に保留しないといけないということです。
次に、興味深い点として「イコータブル(Equatable)」の適用についての話がありました。株式会社ゆめみのiOS編集課題において、特に気になったポイントです。イコータブルを適用することが正しいのかどうか、さらにもしダメだとした場合にはどうするべきか、といった点で議論がありました。
具体的には、iOS編集課題内のソースコードで、YumemiWeather
のレスポンスがEquatable
になっている部分が気になったようです。このレスポンスは、天気の状態、最高気温、最低気温、日付などのプロパティを含んでいます。これらのプロパティをすべて一致させてレスポンスが同じかどうかを判定するのは難しいのではないかという疑問がありました。天気は状況によって異なりますし、取ったレスポンスがたまたま同じであるかどうかを判定するだけでは不十分のように感じます。
SwiftUIの利用においては、Equatable
がいろいろな動作に依存しています。特に画面描画の更新など、さまざまな部分で使われることがあります。現時点では主にテストを書きやすくするために使われているケースも多いようです。
実際にどのようにEquatable
が活用されているかを見てみた結果、テストでの利用のみだったため、なおさら疑問が残ったようです。テスト以外での実用的な利用があるのかどうかによって判断材料が変わる可能性があるとのことです。現在は、テストのためだけに使われている場合が多いですが、それで適切かどうかという議論がありました。
レスポンスがEquatable
である場合、特定のキーが一致すれば同じと見なすのか、全体として一致するかを基準にするのか、具体的な状況に依存するため判断が難しい部分もあります。全プロパティが一致することを条件とする場合、テストコードが書きやすくなる一方で、その表現が適切かどうかはケースバイケースです。
さらに、Identifiable
についての話題もありました。Identifiable
はHashable
を必要とするため、これらの関係性についても調べてみるべきという話になりました。
このように、ソースコードの設計やテスト方法についての議論は非常に多岐にわたり、特にEquatable
やIdentifiable
の使い方には注意が必要です。 そうなんですよ。この「Hashable」と「Equatable」の関係もなかなか面白くて、ここも厳密に突き詰めていくとちょっと違うんです。「Hashable」と「Equatable」は別々なんですよね。「Hashable」が取れる前提に、一致性(Equatable)は必ずしも必要ないわけです。関係性をどちらかで取るとしたら「Hashable」が先にあって、「Equatable」という流れのほうが理屈的には自然かもしれないですね。
ただ、プログラミング的にどれが最適かを考えたときには、これが良かったということなんですね。考え方は色々ありますので、どれが正しいか間違っているかというのは最終的には分からないですね。結局のところ、全てが一致していれば良かったと言っているに過ぎないのです。その後、自分でもこれはレスポンスだよな、みたいなのを考え始めるだけであって、この辺りもっと別の視点から見たら有意義なのかなと思いました。
仮に「Comparable」じゃなかったとしても、それは問題ないです。「Equatable」じゃなかったときにこのテストがどうなるのか、という話ですね。テストがあって、レスポンスをJSONからフェッチして、パラメーターがこれか、なるほど。それでフェッチしたデータをレスポンスで取得する。このレスポンスの違う定義に戻りつつあるかなと思いましたけど、そんなことはないですね。
「東京に」というのはレスポンスの最初の部分でしょうか。全体がよく見えないからわかりませんが、テストがあって、レスポンスをJSONからフェッチして、パラメーターを指定する。このパラメーターからデータを取得するのですが、同じことをしているように見えるけど違うのでしょうか。
レスポンスの内容が一致していたからオッケーということですが、ここの部分をしっかりと見る必要がありますね。レスポンスのIDで比較するという方法も考えられますが、これもどちらでもいいという結論に至りますね。「Equatable」が大げさかなという気もしましたが、「Hashable」があるからJSONに変えて比較するというのも、ランタイムによっては同じになるでしょうが、あまり良くない気もします。一番自然なのは「Equatable」かもしれません。
この辺りが気になったので、皆と話してみたいと思います。もしくは、テスト用に何か別の方法があるか、または自分でどう作るか。これも一つの方法ですね。
この辺りで適当にファンクションを置いておく、あるいはどこか他の部分に置くのも手間がかかりますが、一考する必要があります。 ファンクションとして hit == 2
みたいなものを実装して、それでレスポンスを引き継ぎ取るという普通の方法もありますが、いろいろ考えても結局どれも一緒のような気がします。指標を示すようなことも基本的にはないはずですしね。微妙に何かもうちょっとスッキリする方法がないかなといつも考えますが、こんなものかと思います。
さて、ケースイテラブル(CaseIterable)についてですが、これは全てのケースを列挙できるという意味です。具体的には、このケースイテラブルはどこで使われているのでしょうか。たとえば、レスポンスを生成するときやランダムエレメントを返すときに使われているかもしれません。これはAPIの仕組み上、ランダムに値を返すといった使い方がされています。
また、列挙するということが全項目を開けられる列挙になることで、特定の場面で役立っています。この辺りは詳細に語られてはいませんが、列挙ができるというだけでも意味があります。コード例としては、以下のようになります。
enum WeatherCondition: CaseIterable {
case sunny
case rainy
case cloudy
}
このように、.allCases
を使って全てのケースを列挙することができます。
名前付けについても触れてみますが、allCases
という名前が冗長で、例えば WeatherCondition.all
でもいいのではないかと感じることもあります。しかし、汎用化するとそういう形になるのでしょう。RawValue
についても同様の議論ができます。たとえば、以下のように Raw Representable プロトコルを用いることができます。
enum Condition: String {
case sunny = "Sunny"
case rainy = "Rainy"
case cloudy = "Cloudy"
}
この話題はこれくらいにして、次回はまた順番参照の解消の仕方について戻っていこうと思います。今日はこれで終わりにします。お疲れさまでした、ありがとうございました。