https://youtu.be/cXvlci1dn2Y
本日は、前回から眺めていっている The Basics
の 定数と変数の出力
の続きです。この前は print
関数の基礎とそこから Swift の表現力の高さみたいなところを窺いましたけれど、今回はもう少し視野を広げて Swift 全体に根付いているテキスト出力周りの特徴を見ていけたらいいなと思っています。どうぞよろしくお願いしますね。
————————————————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #102
00:00 開始 00:49 前回の要所 01:44 今回の展望 02:40 値を表示していると言える? 04:26 クラスのテキスト表現 05:00 名前空間の添えられた型名 07:12 MainActor の定義とテキスト表現 08:09 さまざまなインスタンスのテキスト表現 10:08 値というよりインスタンスをテキスト表示 11:44 文字列を受け取るよりも扱いやすい 18:09 print 関数の実装まわり 22:50 組み込みの文字列変換処理 24:54 文字列変換を言語がサポート 26:11 String(describing:) 27:55 文字列変換のカスタマイズをサポート 30:32 文字列変換を標準とする考え方 31:38 String(describing:) に関する追加情報と、次回の展望 —————————————————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #102
はい、じゃあ今日は「定数と変数の表示」というセクションに入っていきます。前回からの続きですね。前回は print
関数の素晴らしさについて話したと記憶しています。Swiftの print
関数がその表現力を存分に生かしたものになっているという話題でした。
まず、その振り返りとして、スライドでどのように話したかを見ていきましょう。print
関数はグローバル関数として提供されており、適切な場所に出力できるという特徴があります。これには、デフォルトで設定されたセパレーターとターミネーターが含まれており、非常に柔軟に利用できます。このデフォルトパラメータについては、参照すべき内容が他の文献にも書かれているので、興味がある方はそちらも見ると良いでしょう。
さて、前回話した中で少し違和感を覚える部分があったかもしれません。特に、「一つ又は複数の値を適切な出力装置に表示する関数」という説明ですね。この「値」という言葉に少し違和感を覚えるかどうかについて、少し深堀りしてみましょう。
たとえば、以下のように変数 a
に 10 を代入して print(a)
とした場合、10が表示されます。これは確かに「値」を表示していますよね。
var a = 10
print(a)
次に、ストラクトを使う場合を考えてみましょう。以下のように、Value
ストラクトを作成し、その中に a
と b
というプロパティを持たせた場合、次のように print
を実行すると、Value(a: 1, b: 2)
と表示されます。
struct Value {
var a: Int
var b: Int
}
let value = Value(a: 1, b: 2)
print(value)
これは、「値」を表示していると言っても良いです。ただし、ストラクトは値型で、そのインスタンスを表示するとその値が出力されます。
では、これがクラスだった場合はどうでしょうか。そのあたりを考えてみます。クラスの場合、たとえば Object
クラスが print
されると、少し意味が変わってきます。
class Object: NSObject {
var a: Int
var b: Int
init(a: Int, b: Int) {
self.a = a
self.b = b
}
}
let object = Object(a: 1, b: 2)
print(object)
この場合、実行してみると Object
の型名やメモリアドレスが出力されることになります。これは NSObject
を継承しているためで、純粋なクラス型の表示とは異なります。たとえば、DispatchQueue
などの非 NSObject
継承クラスの場合はどうなるかを試すことも考えられます。
さらに、コメントでいただいた WidgetKit
を試してみます。以下のようにして、特定のクラスや構造体を表示することも可能です。
import WidgetKit
if let shared = WidgetCenter.shared {
print(shared)
}
このように、print
関数は基本的なデータから非常に柔軟な表現が可能です。これによって、さまざまなオブジェクトや値を視覚化することができるのです。これが Swift の強力な機能の一部であることが理解できるでしょう。
今後も、このような具体的な例を交えながら進めていきたいと思います。質問や試してみたいことがあれば、遠慮なくお知らせください。 とりあえず、名前空間が出てきていますね。こうやって、MainActor
をMainActor
として定義されているんです。定義をちゃんと辿れなかったのですが、ここでt
って何でしょうね。でも面白そうなので、ちょっとプリントしてみますか。アクターのプリントをしたことがあまりないので試してみます。
Shared
って前に教えてもらったものですが、それを使ってこうすると、これも名前空間が付いて出てきますね。なるほど。標準で構造体以外の型が入った感じですね。いろいろ試してみましたが、プリントを使ったときにちょうどいい感じです。
これがアタイ型で、クラスだとすると、こっちがアクターで、いろんな表示のされ方があります。例えば、関数をプリント関数にしたらどうなるでしょうか。こういうふうに試してみると、Function
って出てきました。ファンクションとしてだけ表示されるんですね。クロージャーを取って、例えばInt
を取ってInt
を返すクロージャーを実行せずにプリントしてみるとどうなるのでしょうか。
ここでパラメーターを書いているから、こうしないといけないんですね。そして、a
とやると何が表示されるかというと、構文は合っているか確認します。引数リストと戻り値とリターンが表示されるんですね。いろんなものを表示できるんですよ。
このときに、まずそれがアタイなのかが気になります。アタイという概念を超えている感じもしませんか。さっきのスライドのところが気になったのですが、いろんなものが表示されるんですね。一つまたは複数のインスタンスを適切な出力に表示する、という特徴を持っているんです。
改めてプリント関数の定義を見ていくと、Any
型を取得するようになっています。プリント関数はAny
型を受け取って、それを表示するという感じです。これ、面白いと思いませんか?普通のプリント関数でしたらモジュールを取ると思うんですよ。一般的な言語で。
従来の言語、例えばC++やJava、Pascal、Perlなどでは、Any
を取るようになっていないのが一般的ですが、SwiftではこれはAny
型を取るようになっています。これが従来の言語の仕様とは違って面白いところです。
例えば、Aを表示しようとするときに、Aをモジュール変換しないといけないですよね。ストリングインターポレーションを使うのが手軽です。この値を表示しようとしたときにどうするか、昔のC++やCの場合、モジュールで表現するときにどうするかを考えました。
例えば、インスタンスがあって、Aが値のA、Bが値のBといった風に書くとします。こう書いたときに、表現が一般的だし、違和感ないですよね。他の行もモジュールで表示したいとき、型の名前だからtype(of:)
で取って、モジュールで表示するようにします。このように、インターポレーションを使って表示する方法もいろいろあります。 とにかく今はこうやってモジュールに変換していかないといけない、ということでプレッシャーがかかるわけですね。この他にも同じようなことが求められています。
例えば、このバリュー型を表示するとき、18行目のような表現方法を使います。これがファンクション、例えばonClick
のようなもので渡ってきて、それでセンダーがボタンを持っている場合を考えます。ボタンが持っているバリューをテキストラベルのストリングバリューに入れるとき、センダーのバリューをモジュールしないといけません。この時は例えば、A = センダーのバリュー[A]
などと書いて、B = センダーのバリュー[B]
というようにインターポジションを使って書きます。これでやっと代入ができ、ラベルに表示するという流れになります。
このonClick
が呼ばれる前に現在のバリューがどうなっているかを出力したい場合も、同じように書く必要があります。この場合、一般的には Aは何で、Bは何だ
というふうに、状態をそのまま表示するという発想になります。このように表示する方法はその都度考えないといけませんが、煩わしいですよね。しかし、プリント関数を使うことで、バリューやオブジェクトを簡単に表示できるようになります。これが画期的なことだと思います。
この便利な機能を支える技術として、Swiftにはストリング変換が言語仕様として組み込まれています。これはとても面白い部分です。
Zoomのコメントで関連する情報も共有されています。例えば、プリントの実装についての話がありました。面白いのは、実装ではカスタムストリングコンバージブルなどの理由で文字列を判定しています。通常の文字列の場合はそのまま使います。
また、スタンダードIOや出力先の設定についても話がありました。固定出力になっていますが、外部に公開すれば出力先を変更できるのではないでしょうか。そして、受け取ったバリューが文字列ならそのまま出力するし、TextOutputStreamable
ならストリームに投げる、という実装になっています。カスタムストリングコンバーティブルやカスタムデバッグストリングコンバーティブルに準拠していれば、それぞれの表示を使います。
さらに、リフレクションが有効なら、それを使うというディレクティブもあります。これらに準拠していなかった場合のデフォルトの表示にするコードもあります。このようにして、できるだけ多様なバリューを適切に表示できる仕組みになっているわけです。
以上が、Swiftにおけるプリント関数やストリング変換についての話でした。 とりあえず、それにも準拠していなかったらミラーを取ります。それで終わらなければ、デフォルトの表示をするというコードがどこかに埋まっているのです。多分、この関数の呼び出し元なんでしょうね。呼び出しのところやアンダースコアのスプリントで、チャットの2個目のやつを見ると、普通にターミネート(終了)しているだけなんです。
アイテムをアンロックするだけなんですよね。コードが変わったのかもしれませんが。これで全部のパターンをカバーできているようですね。今表示中のやつで終わってしまう場合は、Swiftにはリフレクションがあるので、リフレクションを使ってAdHocプリントを実行しているということです。リフレクション対応していない言語だと、こういった表示が出て終わってしまうんでしょうか。
上のコードが全部対応しない場合はリフレクションが無効なので、他の方法が見えるかもしれませんが、そんなものを見たことがないですね。
リフレクション対応しているから、MacでSwiftを使っている限り、そのような表示を見ることはないでしょう。重要なところをゆっくり話していくので、理解できない方は適当に聞き流してもらっても構いません。
文字列表現に関してはまず String
があり、あとは独自のカスタム表現がありますね。オプショナルの処理に見えますが、この関数が何かは正確には分かりません。オープンエクステンションに関するものかもしれませんね。
存在型に関連しているかもしれないですね。条件を満たした上で、カスタムデバッグのストリングコンバートという形になっています。オプショナル型で、この辺りはよく見る動きですね。
ただ、このプリント文が本当に全部網羅されているのかは微妙ですが、動いているのでしょう。とりあえず、これは置いておきます。
それから、文字列の話に戻りますが、Swiftは基本コンセプトとして、どんなインスタンスも文字列表現できるという言語仕様を持っています。つまり、どんな型のインスタンスを受け取ってもコンソールにプリントできる仕様になっているので、プリント文は定義として Any
を受け取れるというのが面白い特徴です。
最終的に文字列に変換しないと表示はできません。そのため、全てのインスタンスをテキスト表現できると言っている言語仕様が存在しており、それがよく言われる StringConvertible
という考え方です。全てのインスタンスは文字列に変換でき、その方法として String
のイニシャライザーなどが用意されています。この String
のディスクライビングというイニシャライザーの定義はジェネリックでサブジェクトを取る形になっていて、これは何の制約もかけていないため、 Any
と同等です。 では今回の内容を整え直します。
全く一緒って問題ないんですよね。なので、ディスクリプションはインスタンス Any
を取るっていう定義になっていて、この中で任意のインスタンスを適切な文字列に変換するっていう実装がされています。
もしかして、プリントじゃなくてこっちかもしれない。プリントはなんか複雑なことをしていそうです。オプショナルも渡せますよね。ちゃんとオプショナルじゃないやつ、オプショナルのカッコみたいな。ちょっと見てみましょう。
こうやって組み込みの文字列変換が用意されていて、これによって定数だろうと独自の型だろうと文字列化できるっていう動きになります。最終的にこれが表示されるみたいな雰囲気。組み込みでいろいろやっているので、ちょっと違うんですけど、基本的にはこうです。
あと他にも話したいことがいっぱいあるので、また次回に詳しくこの辺りをお話しします。ストリングコンバーティブルという考え方があります。これは言語仕様に組み込まれています。
たとえば、さっきの value
がちょうどいいかな。だから、ここですね。value
を表示すると独自の表示方法になります。あらかじめ言語に組み込まれている表示方法になるわけです。ただ、もう少し違う表現方法にしたいときには、カスタムの文字列表現方法にカスタマイズすることが言語仕様として提供されています。
それがカスタムストリングコンバーティブルという考え方です。これをプリントすることができるわけです。行が変わっちゃいましたけど、これがまどろっこしいわけですよ。それを独自の表現にしたいというときのためにカスタムストリングコンバーティブルというプロトコルがあらかじめ用意されていて、独自の表示方法を提供することができるようになっています。
つまり、たとえばこの value
型をカスタムストリングコンバーティブルに準拠させて、それでこのプロトコルに規定されているディスクリプションを実装して、ここが独自の表現方法を定義するところです。たとえば "AとしてAです BとしてBです" という感じです。ここは Any
じゃなくて文字列を返す形になります。
こうしてあげると、さっきの value
が "A" とか "B" みたいな表現になっていたのが、ちゃんと独自の表現方法に変わってくれます。とりあえず、こういうふうにカスタマイズできるようになっていて、これによってどんなインスタンスであろうと文字列化してコンソール出力してくれるという点がポイントです。この辺りがカスタムストリングコンバーティブル周りの面白いところですね。
この話の詳細には触れませんが、Swiftの言語仕様では、ストリングコンバーティブルを言語仕様の基本としてカスタムストリングコンバーティブルを提供するという考え方があります。シンプルに考えちゃうと、文字列表現できるかどうかをカスタムにしないで、ストリングコンバーティブルにして、基本的に文字列表現できないけどストリングコンバーティブルに準拠したものであればコンソール出力できるよ、みたいな発想をしちゃいそうなんですが、それが全く違う。
全てのものをストリングコンバーティブルで必要に応じてカスタムできるという発想がすごく面白いです。
いろいろと話しているうちにもう時間になってしまいましたので、次回はカスタムストリングコンバーティブルとか、他にもいろんなカスタムの仕方があるのでその辺りを紹介しようと思います。とりあえず今日はこんな感じで文字列表現の特徴的なところ、Swiftの紹介ができて良かったです。
ストリングのディスクリプションの中身を見ると、プリントがアンロックされています。面白いのは自身をインアウトで指しているところです。出力先を。自身はストリーマブルなんですね。これも興味深いです。
興味がある人はソースコードのコメントなどを読んでみてください。今日はふわっとしたお話になりましたけど、これで終わりにします。お疲れ様でした。ありがとうございました。