今日は The Swift Programming Language の The Basics
にある assert
と precondition
続きのところから眺めていきます。具体的にはその定義まわりのところから。assert
の定義については既に見終えたくらいなところですけれど、そのあたりで見落としているところがないかから見ていこうと思ってます。よろしくお願いしますね。
—————————————————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #200
00:00 開始 01:33 assert の特徴を再確認 02:31 String の既定イニシャライザー 04:29 オートクロージャーでの既定値指定 05:15 停止位置を特定するための引数 06:17 StaticString が気になる 07:46 UInt なのも気になるところ 09:01 フリーな関数として存在 09:24 戻り値は Void 09:36 StaticString は StrongProtocol に準拠しない 12:41 StaticString を String に変換するには 13:39 StringProtocol を想定する 17:19 assertionFailure の定義 18:29 assertionFailure は @inlinable 21:28 最適化との兼ね合いな可能性? 23:35 設計者の気持ちを考えてみるのも大事 24:46 Assert モジュール 25:49 専門家に聞いてみる機会 29:29 行動すると言うのは大切 30:57 @inlinable の追加情報 32:55 クロージングと次回の展望 ——————————————————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #200
はい、では始めていきますね。今日は、以前触れたアサーションとプレコンディションの定義について再度確認していきます。この部分では標準ライブラリにあるアサート関数を使ってアサーションを追加することを中心に話しました。このスライドでは「アサーション」を「表明」と訳していますが、あまり一般的な用語ではないですね。自分で訳しておいてなんですが。
標準ライブラリのアサート関数の定義を見て、そこから気づく点を確認していたところ、話が脱線しがちになりました。ただ、その脱線も学習に役立つので良いと思います。いろいろな関連話題に触れながら覚えていけるからです。大きく脱線する場合もありますが、それはそれで役に立ちます。
改めて標準ライブラリのアサート関数についておさらいしましょう。アサートは特にインポートする必要がなく、Xcodeの自動補完でも条件式のみが表示されますが、実際にはパラメーターが4つあります。条件式をオートクロージャーで取るための仕様ですよね。それからメッセージもオートクロージャーで取るという話もしました。これ、面白いポイントでしたね。ただここで、ストリングリテラルをオートクロージャーにしていないのが面白いと感じました。普通はダブルコートで書くことが多いのですが、意味が若干違ってきますね。
ストリングのデフォルトイニシャライザーって使う機会ありますか?自分はまず使わないなと思うんです。ただ、書きたくなる気持ちはなんとなくわかります。リテラルを入れたくない気持ちもありますね。リテラルは変換が入るので、具体的なストリングを直接コンパイルする方が効率的かもしれません。明示的にストリングをインスタンス化する必要もなく、最適化がかかると若干早いでしょう。これは意識しておくと面白いポイントです。
それから、この両方のパラメーターがオートクロージャーになっているため、最適化が図られたときに余計な評価を飛ばす工夫がされています。この仕組みについても話しましたね。最適化がかかるとどのような効果が出るかを含めて、面白いポイントを感じます。
また、関数のエラーメッセージの部分にはスタティックストリングが使われているのが特徴的です。デバッグ情報として役立つし、ライブラリ制作者にとっては便利な仕様です。ただ、この部分がなぜスタティックストリングであるのかは気になりますね。 後々ランタイムで動くものなので、なぜここでコンパイルタイムに値を決める必要があるのかという疑問があります。現在はそんなにコンパイルタイムで動いているかどうかはわからないですが、将来的には@upconstant
などが出てきた時にはこの辺が@upconstant
のストリングなどになるのではないかと考えると、なぜここでコンパイルタイムに決める必要があるのかが疑問です。いろいろ意見をもらいましたが、それでもまだちょっと納得していない部分があります。
また、スタティックストリングは普段使わないですよね。コンパイル時に値を決めるという感じですが、普通にlet
でストリングを使いますよね。今だと、結局ストリング型に渡さないといけないというところもあってストリングインターンを作らないといけないという状況が出てくるのが気になります。全然むしろ遅くなる感じがします。はっきり言ってわからないですが、ちょっとスタティックストリングの存在も浮いているような気がします。
アサートの時にもう埋め込んでおけないのか、コンパイルタイムにアサートをチェックする方法が必要だという意見もありました。将来的に@constant
が標準になれば@constant
だけで成立するアサートはコンパイルタイムでチェックできるようになるかもしれません。いくつか意見をもらいましたが、なぜアンサインドインティジャーなのかについても疑問です。APIデザインガイドライン的にはInt
が推奨されていますし、このアサート関数がC言語由来ではなさそうなのに、なぜアンサインドになっているのでしょうか。
また、C言語由来だとUInt32
とかになるはずなので、それも関係ないと思います。もう一つの問題として、このアサート関数はどこのモジュールにも所属していないフリーの関数です。APIデザインガイドライン的にもフリーの関数と書かれていて、ちょっと謎が多いです。
他にアサート系の関数がいくつかありますが、その前にスタティックストリングが気になります。ストリング型はストリングプロトコルに準拠していて、ストリングプロトコルは独自の型に適用することはできないというルールがあります。また、ストリング型とそのサブストリングしか適用されないとコメントに書かれています。スタティックストリングについても見ていったところで、やはり少し疑問が残る部分があります。 エクスプレッシブル・バイ・エクステンディのところですが、これは基本的にストリングの普通のリテラル変換のことですね。リテラルからスタティックストリングに変換できるのも自然な流れです。カスタムストリングコンバーティブルやスタティックストリングコンバーティブルも同様に、カスタムリフレクタブルも欲しいところです。
さて、ここで一旦コードを書いてみましょう。まず、以下のようにリテラルで文字列を定義します。
let staticString: StaticString = "Hello"
次に、通常のStringの変数も定義します。
let normalString: String = "World"
これを使って関数を定義します。ジェネリックな表現を使って何かをする関数です。
func process<T: StringProtocol>(_ input: T) {
print(input)
}
この関数には通常のStringは渡せますが、StaticStringは渡せません。例えば以下のようになります。
process(normalString) // OK
process(staticString) // コンパイルエラー
この場合、StaticStringは若干使いにくい印象です。これまであまり使わなかった理由がここにあります。サブストリングについても見てみましょう。一部の文字列を取り出す場合、インデックス操作が必要です。
例えば、以下のようにしてサブストリングを取り出します。
let substring = normalString.dropFirst(1).prefix(2)
このサブストリングも process
関数に渡すことができます。
process(substring) // OK
しかし、StaticStringは直接渡せないため、以下のようにStringに変換する必要があります。
process(String(staticString))
これは少々面倒ですね。ストリングプロトコルを前提としたパラメータは便利ですが、StaticStringを扱うとなると一手間かかります。これが性能にどう影響するかは微妙なところですが、高頻度に使う場合は大きな差が出る可能性があります。
ジェネリック関数でStringとSubstringを共に処理する際にも、これをうまく扱うことでライブラリが柔軟になります。例えば、以下のようなAPIを提供すると便利かもしれません。
func printContent<T: StringProtocol>(_ content: T) {
print(content)
}
脱線しましたが、基本的にはStaticStringは通常の文字列と比べて使い所が難しく、メリットを感じにくいことがあります。このように、用途に応じて使い分けることが重要です。 アサートの話に戻りますが、先ほど見たアサートの関数の定義以外に、もう一個「assertionFailure
」という関数もありますので、これの定義も一応見ておきました。
デフォルトでは何もパラメーターを取らない形で出てきますが、実際のところは条件式が抜けているだけで、先ほどのアサートと同じ感じです。メッセージは最適化で無視されたときに評価をしなくて済むようにオートクロージャーになっています。また、ライブラリー制作者にとって嬉しい機能として、エラー発生箇所、正確にはアサーションの発生箇所をカスタマイズできる機能が用意されています。
ちなみに戻り値はボイドで、主体がないためフリーの関数となっています。そして、もう一つ気になる点として、「@inlinable
」が付けられていますね。先ほどのアサート関数には付けられていません。この違いは非常に面白いです。なぜ条件式があるとインラインにしないのでしょうか。それぞれのメリットとデメリットについて考えてみましょう。
インライン化すると関数呼び出しではなくて、ダイレクトにコードが埋め込まれ、その結果としてパフォーマンスが向上します。ただし、デメリットとしてはバイナリサイズが増えることがありますね。ランタイムであることには変わりませんが、少し特徴が異なります。
アサート関数はインラインになっていないということは、関数呼び出しの処理が必ず一回入るということですね。それがどういう意味を持つのかはさておき、assertionFailure
はインライナブルとなっていて、使用に応じてインラインにできます。バイナリに埋め込むのか、関数呼び出しにするのかという選択肢があるわけです。
なぜ@inlinable
を使うのか、特にスタックトレースやデバッグの際に分かりやすくなるからでしょうか。積極的に使用しているわけではないかもしれませんが、インライン化することで得られるパフォーマンスの向上やデバッグのしやすさは利点ですよね。しかし、この@inlinable
のメリットを最大限に活かすのは難しいかもしれません。
アサートはインライナブルではないが、assertionFailure
はインライナブルである理由は何かしらあると思います。プレコンディションやプレコンディションフェイラーなども同様に検討されるべきでしょう。
リリースビルド時にassertionFailure
を呼び出すコードを書いておいて、最適化オプションで取り除かれると、関数呼び出しが無駄になるケースが減ります。インライン化することで、無駄な呼び出しを減らし、パフォーマンスの向上を図る意図があるのかもしれません。
これらの関数がインライナブルかどうかという議論は、実際にはパフォーマンスの観点からの選択かもしれませんが、アサート関数もインライナブルであれば、さらにわかりやすくなるのではないかとも思います。プレコンディションも同様に考えられるでしょうが、それがインライナブルでない理由には注意が必要です。
この作者の気持ちを考えることは、ソフトウェア理論の理解を深める上で非常に重要です。正解は分からないかもしれませんが、自分なりの解釈を見つけることが有用です。assertionFailure
にのみインライナブルが付いている理由を考えることは、ソフトウェアの設計や最適化について深く考える良い機会だと思います。
他にこの系統の関数があるかといったことも含めて、暇なときにでも考えてみることで理解が深まるのではないでしょうか。 フェイタルエラーについてはこれぐらいにして、次はインライナブルについて見てみましょう。アサートモデルもちょっと面白いですね。アサートモデルには何が入っているんでしょうか。アサートプレコンディション、アサーションエラー、プレコンディションエラー、フェイタルエラーなど、完璧に揃っていますね。これで全部読みました。アサートモデルとプレコンディションモデルは別々に分かれているんですね。ちょっと個人的には気になる部分です。自分でもやってみたいなと思っています。
この一つのファイルにインライナブルがどうか確認していますが、この部分だけ特別な理由があるのか気になります。こういったことって、WWDCとかに行ってラボで話を聞くと面白いんです。行くのは大変ですが、こういう話をするとSwiftコンパイラーチームの人たちは喜んで話してくれます。やはり彼らはこれが好きでやっているんでしょうね。
最近、確かSlackでコースアップの方法について話せないかということがあったと思います。メールでそんな話を聞いた記憶があります。確かにそれもいいかもしれませんが、やはり直接聞くことで熱量が感じられて面白いんです。WWDCのラボはすごくよかったです。Slackのようなテキストベースのチャットサービスは、その熱量が少し失われる気がします。でも、音声チャットがあれば、リアルほどの効果はないかもしれませんが、少しは補えるかもしれません。
Slackのイベントは毎月やっている感じですね。先月の16日までやっていましたし、11月は14日と18日、12月は16日、10月は17日から21日など、きちんと毎月行われています。ページをリンクでたどってはいませんが、イベントのところに書いてありました。プライベートモードでSlackのテキストベースチャットを通じてAppleエキスパートに質問したり、交流したりできるとのことです。日本語でも対応してくれますし、1対1の2.5分間コンサルティングもあるようです。
これは全然気にしていなかったですが、いい機会ですね。気軽に質問できるというのは貴重な機会です。つながるかどうかは別として、そういうチャンスは大切にしたいです。
また、WWDCでAppleの方にちょっと特殊な質問をしたことがあり、それが印象に残ったようです。何かしら行動を起こすと、それ相応の反応が返ってくるのは当然のことだと思います。なので、少しでも気になることがあるなら質問してみると良いと思います。英語が問題にならない限り、普通は気軽に使えます。インライナブルから他のメソッドを呼び出すと、ABIの整合性が崩れたりして怪しい動作をすることがあります。
インライナブルの内部的な特徴を知ることができれば、よりいい感じになりますが、そこまで洗練させる必要はないかもしれません。インライナブルが時代に通用するのは、ちょっとした技術的な楽しみですね。アサーションエラーとプレコンディションエラーについても深掘りしてみると、またさらに考えやすくなります。これらも非常に興味深いテーマです。
次回は、プレコンディションの詳細な説明や、これまで話してきたプレコンディションとそうでない部分の違いについて話していく予定です。具体的な使い方についても触れます。今日はこの辺で終わりにしましょう。お疲れ様でした。