https://youtu.be/4evjKGXSXbI
今回は、前回に見た The Basics の導入部の「型安全」の続きから、続いてその次の 定数と変数
についてを眺めていきます。変数や定数の特徴や定義方法といった初歩的なところを見ていく機会になるので、せっかくなので細かくいろいろと今だから気付けることをみんなで探してみれたらいいなと思っています。どうぞよろしくお願いしますね。
————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #86
00:00 開始 00:36 型安全 02:13 扱える値の型を明確にする 02:58 型を明記しない言語の場合 05:03 文字列を2倍すると? 07:00 Decimal 型の文字列変換 08:06 動的型付けと静的型付けの特色 10:58 早い段階でエラーを検出 18:04 型推論 22:46 ジェネリクスの話は今回は省略 23:37 同じ型どうしの演算 24:14 余談:JavaScript の数値変換パーサー 25:33 Decimal は誤差を精密に引き継ぐ 27:48 計算性能と静的型付け 31:14 動的型付けの演算のされ方 36:07 解析の複雑化 39:11 同じ型どうしで演算することで齎されるもの 40:31 Swift の演算子は関数 41:52 問題なければ異なる型での演算を許容する方向性 45:42 異なる型を求めることによる安全性の担保 46:57 StringProtocol による最適化 48:30 次回の展望 —————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #86
はい、じゃあ行きますね。今日は「The Basics」の型安全の続きからですかね。前回、この1行目で終わったんですけど、この続きですね。
続きと言ってもそんなに大したことは書いてないので、サクッと見てさらにその次へ行こうかなと思います。「The Basics」の型安全では、「コードで扱える値の型を明確にする」と書いてありますが、「コードで扱える値の型を明確にする」というのは、あくまでもコードでそこに代入したりできる型が明確になるという話です。表向きには逆に不明確にはならないでしょうが、実際には表れてこなくなる部分もありますね。どうでもいい話ですが。
唐突にプレイグラウンドに行ってしまったので、もう一回スライドに戻ります。この2行目、「コードで扱える値の型を明確にする」というところで、「明確にする」というと、純粋に2つの方向性から見れるのかなと。ちょっと話していて気づいたのですが、型推論と混乱して話していたので、おかしくなりましたね。型安全ですから型推論とは別ですね。
型安全だからこそ型が必ず明記されるということですね。型推論を入れると省略もできますが、この変数 value
は Int
型というふうに型が明確にされます。それを明確にすることで、例えば関数で何かしらの動作を示すものが String
型を想定している場合、Int
型の値を間違って代入することがなくなるのが型安全です。
型安全がない言語だとこのあたりが完全に型というものが存在しなくなるため、ちょっと書きづらくなりますね。型安全ではない言語では型を明記しない書き方になり、何でも渡せるような状況になってきます。そうすると予期しない値を渡してしまい、例えば value * 2
のようなコードがあったときに、正しい数字を渡さないと結果がおかしくなるという動きになります。これが型安全じゃない言語ですね。ちょっと言い過ぎな気もしますが。
Swiftは型安全ですから、ちゃんと型を明記しておけば、もし String
だと String
に対して 2倍
することはできなくなります。エラーが検出されるので、間違ったコードを書く可能性が非常に限られてきます。このエラーを整合性を取っていくことが自然にできるのが型安全のメリットでもありますね。
ところで、型安全じゃない言語は暗黙の型変換が行われて辻褄を合わせるという印象があります。JavaScript で 文字列 * 2
するとどうなるんでしょうね。ぱっと思い浮かぶ方いますか?
Swiftの静的片付けは、辻褄が合うようにソースコードを調整するという感じが見て取れます。同じ型じゃないといけない場合、取り得る値の型が限られます。この点からも、型が Int
だから、その中でできることは限られてくる。どちらが先でもいいと思うんですけど、最終的には辻褄が合っていきます。戻り値が Int
型であることから計算の評価式の結果と戻り値の型も一致しますし、受け取る値の型もそれによって必然的に固定されてきます。
これに対して動的片付けの言語の場合は、ランタイムで辻褄を合わせていく感じになってきますね。与えられた値が全体の様子から見て数字であろうと判断し、それを数字に解釈します。それが変換できない場合は NaN
(Not a Number)に辻褄を合わせるという感じです。例えば、奇数と数の掛け算は奇数になるので、それで辻褄が合ったと判断します。こう考えると、辻褄を合わせていくという価値観が見えてきます。
どちらが良い悪いというよりは、両方ともきれいな感じですね。一応、より安全に意図したとおりにコードが動く観点で見ると、最近の主流は型がついた言語、例えばSwiftやTypeScriptといった静的片付けの言語になってきています。なぜ主流がそっちに動いているかという理由の一番大きい点は、開発の早い段階でエラーを発見して修正することが可能になるというメリットがあるからです。
例えばJavaScriptだと、明らかにコードを見てわかるものなら良いのですが、変数などでいろいろやっているとなかなか発見しにくいことがあります。たとえば、 const value = '文字列';
として const result = value * 2;
とした時に、これは期待していた2倍の結果が取れると考えてコードを書くわけですが、変数にうっかり文字列が入っているとエラーになる。この場合、期待していない値が NaN
になってしまいます。このようなエラーは実行時にならないと検出できません。それが特に大きな問題です。動的片付け言語だと、17行目のようなコードになって初めてエラーが検出されるというわけです。
型安全の言語だと、例えばこれは String
だから Int
として取ろうとした時にコンパイル時にエラーが検出されるし、この value
が整数を期待しているのにそうでないことが分かります。動的片付けだと、実際にコードが実行されるまでエラーが分からないことが多いのです。 ただ、もしかするとリザルトが乗ったナンバーになって先行ってしまうから、もう少し先になるとわからないみたいなところもあります。型安全な言語でしっかりと想定ができていれば、16行目、この代入の段階でエラーが検出できます。これがソースコードのタイミングで検出できるというのももちろん大きな利点ですけれども、仮にランタイムに回ってしまったとしても、16行目でエラーが見つかったとデバッガーで知ることができれば直すのが容易です。
例えば、17行目だとバリューが問題だと気づき、バリューが出てきたところを探す必要があります。もっと先になってエラーが発生したときに、果たしてこの16行目がエラーだ、もっと言うと17行目で計算間違いが出ていることを見つけるのは結構難しくなります。そういったところを事前に防げるという意味で、型安全は非常に強力です。
今どきはあまり悩まされないかもしれませんが、昔は「どこでエラーが出ているんだろう」といったことが多かったですよね。動的型付けの問題もありますし、グローバル変数を多用していたこともあります。そのため、世の中的にはグローバル変数を極力使わないようにしようという流れになり、同じように型をちゃんと明記して適切な値を取り扱えるようにしましょうという流れになりました。
この話に関連して、意図した値がちゃんと渡ってきていることを確認するために、「ヌル安全」という前回お話しした内容も絡んできます。これらにより、適切なコードをチェックしながら書ける仕組みが整ったという背景があります。Swiftではこの型安全が非常に重要視されており、このおかげで自然で適切なコードを気持ちよく書けるようになっています。
さらに、この辺りで話すべきことがあれば、ぜひ聞かせてほしいです。型安全に関して思い入れの深い話があれば教えてください。
この型安全があるからこそ、型推論もできるようになります。ソースコードの前後の文脈から「ここはint
型しか取り得ないよね」といった場面を見つけて型の明記を省略できるのです。例えば、1行目から6行目までのコードを丁寧に書くと、「int型です」というように型を厳密に明記することになります。型推論がない言語では、こうした明記が必要となり、冗長なコードが増えがちです。
型推論のおかげで、例えば、引数がint
型である場合、慣習的に「ここはint
型でしょ」ということがわかります。例えば、8行目のアクションの戻り値がint
型で定まっているなら、何も書かなくてもint
型だということは自明です。そのため、型推論によって、右辺がint
型なら左辺もint
型であることがわかるのです。リテラルのデフォルトの型に関しては、以前の勉強会で話しましたし、今後も再び触れる機会があるので、そのときに詳しく話します。 とりあえず、右辺が int
だから、引数も int
でしょう。こういうふうにして、あまりこれだけのコードだと2カ所しか省略できなかったですけど、こうやってあたかも動的型付けみたいな雰囲気のコードになります。動的型付けの利点としては、コードがサクサク書けるという点があります。ここはちょっと語弊があるかもしれませんが、冗長な「ここは何型ですよ」みたいな主役の部分を省略してシンプルなコードになるというメリットがあります。こうした省略の結果、特に「これは何型だ」と言わなくてもいいという点がメリットでもあったりします。
型推論のおかげで、型を指定しないといけない煩わしさがなくなりつつ、ちゃんと型を明記する型安全のおかげでエラーを適切に検出できるようになります。この2つのおかげで、型安全がかなり良い感じに機能してくるんですよね。型推論がなくても問題なく書けますが、型推論があることでずいぶん楽になるのは嬉しい点です。
あとは、ジェネリクスの制約など、そういったところも型安全で生かされてくる部分があります。この話は後にしますかね、ちょっとややこしい話になってくるので。そういった点も含め、型推論と型安全、暗黙の型変換をなるべくしないという方針も重なって、かなり安全な形になっています。
ちょっと余談になりますが、型安全に関連するところとして、Swiftは2つの値を演算する際に、両方の型が一致していないと演算をさせないという基本的な方針があります。昔からあって、今も基本はそうなんですけど、若干その雰囲気が変わってきた部分もあります。ここがちょっと面白い進化だなと思いました。
先にコメントを拾っておきますと、デシマルよりパーサがいいですねという意見があります。デシマルがちょっと特徴的なだけなんですけど、先頭が文字列であれば、あるところ以降文字列、違う、先頭から数字を構成する文字列であれば、そこから先、適当な文字列が登場したタイミングでその先無視しちゃうよということで、例えば 134
になるっていうデシマルの話ですね。
iOSLT会で印象に残っている方もいると思うので、文字列が 134
になるという話で、iOSLT会は面白かったんですけど、ちょうど昨日かな、デシマルを使わないと計算誤差が出てくることがあってハマってしまいました。デシマルは厄介ですね。何が誤差出るんでしたっけね、まあいいか。
とりあえずこのストリングではなくダブル型でいかないといけないんだ。この誤差があるから。デシマルの利点は、十進不動小数点数で表すため、その後の計算で誤差が起こらないという点ですが、初めのデシマル変換でつまずくと、その誤差を延々と引き継ぐことになるんです。ダブル型だと、うまく丸めて最終的にいい感じの結果が出たりしますが、デシマルで計算したらその誤差を延々と引き継いでしまいます。
精密に計算結果が全然違ったということがちょうど昨日起こりました。文字列変換に直してことなきを得ましたが、こういった特徴があります。JavaScriptはダブル型で数字を表現しているので、ダブル型の計算誤差に縛られていますが、まあまあ、ちょっと余談が過ぎましたね。
あとは静的型付けはマシンパワーの向上も確かにあります。オブジェクト指向も昔はそうでしたよね。オブジェクト指向の考え方が世に出たけれど、マシンパワーなどの都合で普及しなかった時期があったと記憶しています。しかし、今やオブジェクト指向はなんなくこなされ、型推論もコンパイルタイムに役立っています。 オブジェクト思考はランタイムのパフォーマンスの都合で、型推論はソースコードの都合で導入されています。今でも複雑すぎる型推論については、コンパイルエラーになるという形を取って、パフォーマンスを維持しています。このように、現代でも型推論は重たい処理の一つと言えるでしょう。
動的に型を決める場合、実行時にマシンパワーが必要です。JavaScriptなどの動的型付け言語は、このような処理を行いますが、特に問題なく動いています。動的に型を決めることは、確かにパフォーマンスには影響しそうですが、実際には許容範囲内で運用されています。
JavaScriptにおいて、動的型付けが選ばれている理由として、ブラウザ上で実行可能であり、マシンパワーを過度に使用しない技術選定という側面があります。また、あまり長大なコードを書くことを前提に設計されていない言語という側面もあるでしょう。スコープが短ければ、型推論や文字列操作も容易に行えます。しかし、大規模システムでは判断が難しくなる場合もあります。
JavaScriptでは、一行ごとにインタープリタが解析できるため、リントで対応する部分も多いです。リント
はコードの品質をチェックするツールです。JavaScriptは動的型付けであるため、リント
の形で実行時に処理を決定することが多いと言えるでしょう。
一方、C言語など静的型付け言語では、条件文でデータがゼロかゼロ以外かで判断します。これは、動的言語とは異なり、あらかじめ型が決まっているからです。C言語よりも高度な言語になると、例えば文字列が空かどうかを判断することもありますが、動的言語ほど柔軟ではありません。
動的型付け言語は、実行時に型を決めるため、マシンパワーが必要です。静的型付け言語は事前に型を解析するため、ランタイムのパフォーマンスは比較的高いです。動的よりも早い処理を実現できます。しかし、動的型付け言語は、ルールの組み込みによって効率的に処理されるため、必ずしもマシンパワーが多く必要というわけではありません。
JavaScriptの場合、足し算やその他の操作についても、実行時に型を確認し、それに基づいて処理を行うことが多いです。例えば、インスタンスオブ
という組み込み関数を利用して、文字列か数値かを判断することができます。このように、ランタイムで型を確認し適切な処理を行うことで、効率的に動作しています。ステートメントが多くても、それを愚直に処理していけば機能します。
このように、動的型付け言語と静的型付け言語は、それぞれの特性に応じた方法で型を扱っており、両者にはそれぞれのメリットとデメリットがあります。 ちょっと話を戻しましょう。昔はBasicという言語があり、それもインタープリター方式でした。一行一行を実行コードに変えていって処理できるというものでした。しかし、解析が複雑になるにつれて静的解析が必要となり、前後関係を考慮する必要が出てきました。これをインタープリター的に実行するのは難しいため、先にコンパイルするという手法が取られるようになり、コンパイル言語が生まれました。
順番が前後しましたが、特徴的なところは、処理の重たさをどこに持っていくかという点です。全体的に見て、それが致命的な遅さにはならないと言えますが、Swift言語のコンパイラをコンパイルすると25分ぐらいかかるので、静的解析でも負担はあります。これを動的解析にしたら大変なことになるでしょう。結局、重さはランタイムにするか、コンパイルタイムにするかの違いです。
話が片推論に飛びましたね。動的型付けについてですが、動的型付けは全然シンプルなので、今どきマシンパワーのおかげで問題ないでしょうね。Basicのインタープリター時代も、Basicコンパイラーを通すと圧倒的に速くなったことがあります。基本的な部分は今も変わらないと思います。やはりマシンパワーのおかげで気にならなくなったのでしょう。コンパイラーも非常に賢くなっています。
話を戻して、型安全についてもう少し話します。Swiftでは、2つの値を演算する際に同じ型でなければいけないという基本的な方針があります。例えば、A
としてInt
型、B
としてInt
型を定義し、A + 10
という変数をとるとします。これなら30が得られるのは当然です。しかし、Int16
とInt8
のように型が異なると、これはコンパイルエラーになります。
この基本原則により、異なる型が混ざったときの安全性が保証され、型推論が効きやすくなります。Swiftの演算子は関数として定義されているため、定義をたどることで確認できますが、演算子の定義をたどるのは少し面倒です。
Swiftの演算子の定義は、型の一致を前提としたジェネリクスを使っています。例えば、Int8
型なら他の型もInt8
を想定します。このように、両側の値が同じ型であることが型推論の基本となります。しかし、Swift 3か4あたりから、型が一致していなくても問題が起こらない場合には、型の一致を保証しなくなってきています。たとえば、比較演算子の場合には型が異なっていても誤差が発生しない場合が多いです。 なので、これに行ったものはちゃんとコンパイルを通すようになって、適切な比較判定をするようになりました。両方が同じ値だったら型が違っても一致したよってなれば、true
になります。こういうふうにインターフェースが途中から入るようになってきて、これは面白い変化だなと思います。このあたりを確かになんか煩わされる必要がないですね。昔は両方が同じ型であるということが約束されているとすると、こういったコードが書けなくなってしまうわけです。しかし、型安全としては当然なんですよ。本来の考え方でいうと、この4行目がコンパイルを通ってしまうというのが異端な感じだったわけです。
これが、じゃあこのコードを書くときにどっちを型キャストしようかと考えたときに、一度思考が止まりますよね。「どっちを型キャストすると安全なんだろう?」と思ったときに、何も細かいこと考えないと、表現力が高い方に合わせていかないといけないとなって、これで通るわけです。そして、うっかりというか型が比較演算の時だから、問題なく比較がパスするよねという発想にもなると言えばなります。問題なく動いてますよね、これね。問題なく動いているところがあります。
ですが、このまま運用させて、あるときにこの変数 A
が Int8
を超えてしまったときにランタイムエラーになるわけです。こういった問題が起こるのですが、Swiftの場合はランタイムエラーになりますけど、他のランタイムチェックをしない言語の場合は全然別の値になって false
になるからいいのか、うっかり 10
で一致する可能性もないとは言えません。まあ、いろいろありますけど、Swift の演算を使用するときれいに適切に処理してくれるので、ちゃんと不一致として進んでくれます。
基本的には両辺の型が一致しないと安全性は担保できないのですが、あえて ==
の時には両辺の一致性を問わないことによって、それによって逆に安全になるというのが面白いですね。この発想の進化具合がちょっと面白いなと思います。型安全とは必ずしも両辺の一致性にこだわる必要はないのかもしれません。まあ、ちょっと話が広がりすぎてる気もしますが、型安全という縛りから少し広がりますね。
では、最近の潮流として、何かしらのテキストを取るときに String
型を取るんじゃなくて、型パラメーターとして StringProtocol
を取ることによって、この関数に対して文字列でも部分文字列でも渡せるようにする、というのが面白いと思います。こういったジェネリクスを活かしたり、辻褄が合うなら型を緩くしたりというところが好きです。
あれ、何か選べた。えーと、ストリングのインデックスじゃないといけないんだ。Int
型じゃなくてね。まあいいや、そこはちょっと今やってると長くなるのでやめておきますね。とりあえずこんな感じで、「型安全だからといってガチガチじゃない」というところが面白いなと思います。これが Swift の特徴ですね。
はい、じゃあ時間になりました。今日は型安全のお話で終わりましたが、次回は定数と変数、このあたりについてゆっくりと見ていきたいと思います。はい、ではこれで今日の勉強会を終わりにします。お疲れ様でした。ありがとうございました。