https://youtu.be/x78Bur_Sx6w
今回は The Basics の 定数と変数
について、この間は「複数の宣言を単一行で宣言」する話をしましたので、その続きから、さらにその宣言方法を詳しく眺めていきますね。宣言する際に留意したい事柄ですとか、型注釈まわりをじっくりと見ていく感じになりそうです。よろしくお願いします。
————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #92
00:00 開始 00:59 変数と定数を同時に宣言できない 03:05 変更されないなら常に定数を使う 03:32 定数の利用を意識した言語仕様 06:02 変数を使うことは間違いではなさそう 07:11 定数と変数の差異を意識した言語仕様 12:10 reduce 関数に見る変数の活用 17:00 reduce が使う記憶領域 19:42 reduce 関数を自作してみる 24:50 変数の影響範囲 26:45 実際のところ変数は使われている? 29:26 外側に副作用を齎さない変数 31:14 変数の使用も視野に入れてみる 32:24 再起呼出で reduce を作ってみる 42:09 再起呼出を用いた自作 reduce の汎用化 47:46 再起呼出を用いた自作 reduce のおさらい —————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #92
では、始めていきましょう。今日は定数と変数についてのお話を続けていきます。前回は構文的なところに入りましたので、今日はその続きです。変数や定数の考え方については前々回(79回目かな)で見ていましたよね。今回は、Swiftで具体的にどう定数や変数を記述するかに注目して進めていく回になります。
前回、1行で複数の変数や定数をまとめて宣言できることについてお話ししました。ここで補足としてお伝えしたいのは、let
と var
は混在して宣言することはできないという点です。
プレイグラウンドで例を挙げると、こんな感じですね。
let a = 1, b = 2, c = 3
このように、let
を使って a
, b
, c
という複数の定数を1行で宣言することはできます。しかし、以下のように var
を混ぜることはできません。
let a = 1, var b = 2
これはできません。適切なエラーが出るので、わかりやすいですね。
一行でまとめて宣言するのは、場合によって便利なこともありますが、変数と定数は性格が異なるので、一般的には行ごとに分けて宣言するほうが良いでしょう。
次に進みますね。今、変数と定数の宣言に関する観点でいくつか補足事項を紹介しておきます。これは『The Swift Programming Language』に書かれていたノートからの抜粋です。
まず、「保存された値が変更されることがないなら、常に定数で定義すること」というルールが書かれていました。言われてみればその通りですよね。値が変更されるかどうかを基準にするなら、確かにその通りです。
また、Swift的には定数を頻繁に使用することが推奨されています。言語仕様を見てみると、定数を扱ったほうが何かと良い感じに仕上がるようになっているため、変数を使うよりまず定数を優先的に使っていくというスタンスが推奨されています。
これについて、保存される値が変更される必要がない場合は定数を使いましょう。変更が必要な場合には変数を使う、ということを言いたいのです。この考え方を抑えるだけでも、より安全なコードを書くことができるでしょう。
つまり、変数は保存された値を変更できるようにしたいときに限って使用することが大切です。これが大事なポイントです。それ以外の場合は定数を選びましょう。
さらに、関数型プログラミングに親しみのあるプログラマーほど変数を使いたがらない傾向がありますが、Swiftの言語仕様ではそこまで変数(var
)が嫌われているわけではありません。ただし、可能な限り定数(let
)を使うことが推奨されています。
このような考え方を念頭に置いて、Swiftのコードを書く際にはできるだけ定数を使い、必要なときだけ変数を使うというスタンスを取ると良いでしょう。 なので、変数には変数の使い道があると思います。個人的には、躊躇なく使っていきたいと感じています。その点について一つ補足したいのですが、まずはもう一つ補足しておきたいところがあります。
最初の方で「なるべく定数を使っていこう、その方が安全に働くよ」とお話ししました。この点についても補足したいと思います。定数(let
)は書き換えができないという魅力があります。Swiftでは定数を使って様々な制御を行います。例えばストラクトを定義する際に、mutating
関数を作ると、このmutating
が付けられたメソッドはlet
で定義した定数からは呼び出せません。こういった制御があるため、Swiftの言語仕様は定数と変数を意識して設計されています。
これは、値が予期しないところで変更されるのを防ぐために非常に重要な役割を果たしています。ただ、変数(var
)になるとそのメリットが活かせないこともあります。値型という考え方もあり、うまくいくケースもあるため、定数だからといったメリットがすべてではないかもしれません。それでも、定数にしておけば内容を書き換えられないというメリットがつながることがあります。
他に話そうと思ったのは、値型の話です。例えば、コンカレンシー(並行処理)の話ですが、構造体が安全性の観点でよくできています。構造体をSendable
に準拠させるとき、構造体の場合は基本的に値型で、自動的にインスタンスを参照する際に複製されるため、スレッドセーフが基本的に成り立ちます。
さて、ここで試したいことがあります。コピーオンライト(Copy-On-Write)がSendable
じゃないんじゃないかと思うのですが、実際にはSendable
ですよね。このあたりが気になっていましたが、今は時間がないのでとりあえずやめておきます。
シンプルな構造体の場合はSendable
となります。ただ、クラスの場合は必ずしもSendable
とは限らないため、Sendable
をつけるわけにはいかないのです。言語仕様的には、必ずしもその値がSendable
とは保証できないため、プログラマーがきちんと書く必要があります。その上で、言語としてはSendable
として扱うよという感じの雰囲気のコードになります。
話を戻すと、変数を使っていきますが、変数が常に危険というわけではありません。この話からわかるのは、変数も適切に活用される場合があるということです。例えば、reduce
メソッドが良い例です。このメソッドは、配列の値を初期値(initialResult
)として、次の値を得る関数になります。具体的に書くと、reduce(initialResult: 0) { (result: Int, element: Int) -> Int in return result + element }
といった形になります。
スローズはリスローですよね。 reduce
関数について話をしていますね。reduce
を使うとき、結果を導出するために Result
と Element
を使います。これらは通常、定数として扱われます。例えば、Result
に Element
を足してその結果を返すコードを書くとします(今回は整数型 Int
です)。
通常の reduce
では以下のようになります。
let sum = numbers.reduce(0) { result, element in
return result + element
}
この場合、Result
と Element
は定数です。このコードは、前回の結果と現在の要素を受け取り、計算して結果を返すという形になります。
さらに、定数を中心にしたコードの書き方は、一般的に関数型プログラミングの一環として使われます。変数ではなく定数を使うことで、意図せぬ変更を防ぎ万全を期すことができます。
一方、変数を使ったメソッドもあります。例えば、reduce(into:)
を使うと、結果を変数として更新していく形になります。この場合、inout
パラメーターを使います。
let sum = numbers.reduce(into: 0) { (result: inout Int, element: Int) in
result += element
}
inout
として渡された result
は、関数内で更新され戻り値は不要になります。この方法では、一時的なインスタンスを作る回数が減るため効率が良くなる場合があります。
言語仕様的には、reduce
関数を使うときに必要なメモリ確保の違いがあります。通常の reduce
では、結果を格納するために毎回新しいインスタンスを作成しますが、reduce(into:)
の場合、最初に一度だけメモリを確保すれば良いのです。また、最適化によっては、不要なメモリ操作が削減されることもあります。
最終的にどちらのメソッドを使うかは、特定のケースでの効率性や読みやすさによります。興味があれば、自分でコードを書いて違いを体感してみると良いでしょう。 リデュースとしてイニシャルバリューを取り、それでその後に関数をin
と渡すと、ちょっと見にくいかもしれませんが、上のリデュースの例と照らし合わせてもらえれば分かると思います。こちらが定数にこだわったコードになりますね。
まず、リデュースを使う例です。排列内の要素数だけreturn value
をpredicate
と回帰的に呼び出さなければなりません。これを見やすくするために、配列のエクステンションとして実装しましょう。
では、具体的なコードを見てみましょう。まず、リデュースの実装をエクステンションで追加します。以下のように書くのが良いでしょう:
extension Array {
func customReduce<Result>(_ initialResult: Result, _ nextPartialResult: (Result, Element) -> Result) -> Result {
var result = initialResult
for value in self {
result = nextPartialResult(result, value)
}
return result
}
}
ここで、各要素に対してresult
に値を足していきます。仮に今回の例で取り扱う型をInt
に絞るとすると、次のように実装できます:
let array = [1, 2, 3, 4, 5]
let sum = array.customReduce(0) { result, value in
result + value
}
print(sum) // 出力: 15
このようにすることで、計算結果を簡単に得られます。また、変数を使わずに定数だけで記述するのが安全です。その理由としては、予期しないところで値が変更されることを防ぎ、変数が常に正しい値を持つことを保証しやすくなるという点があります。
関数を使うとき、その関数がひとつの役割を持つようにカプセル化することができます。これにより、変数やその影響範囲を、関数内に限ることができ、非常に安全で簡潔なコードが書けるようになります。
このように、定数と関数をうまく利用することで、コードの読みやすさや安全性を高めることができます。特にSwiftではこのようなプログラミングの考え方が非常に重要だとされます。 このコードがそんなに複雑でなければ、その影響範囲ってたかが知れているところがあって、そうすると「この程度だったら変数を使ったって問題ないよね」っていう感覚があります。そういった感覚で見れば、変数もそんなに悪いものじゃないという捉え方もできるのかなと。パフォーマンス向上などいろんな面で、純粋な変数として見た場合に、変数を状態を保持するために使うのは一般的だと思うんです。今話しているのはローカルな変数のことで、あくまでも自分の価値観として「変数は世間的に意味嫌われている感」がなんとなくあります。でも、現状はどうなんでしょう。変数って普通に使いますか?ローカル変数、そんなには使わないんですかね。何か制限をかけて使ったら負けだと思っているので、別の解決方法があるんだろうなって思いますが、この構図だとしょうがないかなって感じます。
直接reduce
を使う場合、それ以上の方法がない気がします。急にreduce
の中身だとこれしかない気がしますね。例えば、self.map
に対してmap
じゃできないか、reduce
の自前実装はこれしかないですね。最近の呼び出し(例えばクロージャやプロミス)で対応できる方法も多いですが、基本はreduce
に頼る形になりますね。
今の話を聞いてから考えてみましたが、変数はコメントでは意外と多用されているというフィードバックもいただきました。確かに「使ったら負け」って感覚があるかもしれないけれど、簡潔さや分かりやすさの方が優先されるという観点からすると、それでも使うべきなのかなと思います。特にreduce
とかのプロトコルエクステンションの中では仕方がないかなと。
いわゆる関数型プログラミングを実現するための環境では変数(var
)を使うことは避けられないかもしれませんが、それ以外ではなるべくlet
だけで実現しようとする考え方が強いですね。副作用として変数が嫌われる理由は理解できますが、reduce
の実装では副作用を外に与えないので、それであれば問題なく使えるのではないでしょうか。
要は、なるべく変数を使わないためにreduce
という非常に便利な機能を活用して、変数を極力使わないという選択肢があります。こういったアプローチが他にも存在すると思いますが、何か新しいツールや機能を提供する場合でも、自分自身のコードの中でvar
を使う場面が出てくる可能性があります。
ここで重要なのは「どの場面で変数を使うべきか」という発想がコードを書いている中でできるかどうかです。そして、それがコード全体のクオリティに大きく影響を与えると思いますね。なるべく使わないというよりも、意外と使ってもいいのではないかという柔軟な考え方を持つことで、もっと良いコードが書けるのではないかと感じました。
reduce
以外の実装に挑戦してみますか?再帰呼び出しは意外と難しい部分もありますが、最近はできるようになってきた感じがします。でも、やっぱり再帰呼び出しは難しいですよね。それが今ここで判明する気がします。再帰呼び出しって本当に難しくなかったですか?なんとなく書けるようになってきた気がすると、自然と進歩していくものですよね。
使う場面は限られるかもしれませんが、考え方として覚えておくと便利です。再帰呼び出しを使うと非常に楽になるロジックもあるので、その時にはちょっと考える価値があると思いますね。 このArrayのエクステンションに対する再帰呼び出しが、また感覚が違って難しいですね。単なる関数ならもうちょっと楽なんですけど、そうするとリザルトとして、要は戻り値としてリザルトでしょ。これに対して再帰呼び出ししたいわけですけど、この時にリザルトにプレディケートをかけていきたいなと。プレディケート、リザルトプラスプレディケートで、直前の答え、初期値、次の値、リザルトでしょ。初期値はね、リザルトはここじゃないね。リザルトでしょ。それで最初の要素をここに渡したいわけだから、ファースト。
まずファーストが空なら終わりっていう条件が欲しいですね。そうするとスイッチになるのかな。再帰呼び出しはね、この終了条件がまず大事になってくるんですよね。スイッチファーストで、ケース、インデックスで取るようなもの。必要じゃないですかね。インデックスないといけない。捨てていけば大丈夫だと思うんですよね。なので、ここでケースレッド、バリューとして、なかったら先に書いちゃおうかな。ケースニルだった場合には、これでリターンリザルトですよね。終了条件書けた。1個クリア。
次、そうじゃなかったとすると、リザルトにプレディケート。合ってんのかこれ。ファースト? ここファーストでいいの? プレディケートファースト。初期値とリザルトとそれとファーストをまず実行して、その上でさらにプレディケートするんだ。これを初期値にして、本当かこれ。初期値にしてここでドロップファーストしたやつに対してリデュースするの。リデュース今作ってるやつね。
ここはちょっとこんがらがってきたな。このあたりをうまくやらないといけない。これに初期値として、一回変数に受けなきゃダメなのか。おとなしく変数に受けてみようか。一回ね、let partialResult
としてプレディケートで、ここまで良さそうね。初期値と初期値で計算する。計算して、その後リターンでプレディケートに、とりあえずドロップファーストをしないと続きが書けないからこう書いたとして、どうなんだ。
プレディケートしないといけない。違うか、それはリデュースに任せればいいのか。リデュースとして自分自身ね、ここのリデュースを書くためにはドロップファーストってサブシーケンスですよね。だからここをもうちょっと汎用的に書けばいいと思うんですけど、これでリデュースでパーシャルリザルトとそれとプレディケート。プレディケート直接? じゃあそのままプレディケートを渡す。渡すですね。あれ、いいのか。このプレディケーター計算をしてくれて、でも良さそうじゃない、これ。ちゃんと渡ってるのかリザルト。できてんじゃない、これ。間違いになってしまった。
エクステンションで再帰呼び出しをするっていうのが、またふわふわしてますけど、ここで再帰呼び出しするでしょ。これをね、違う、プレディケートだからここでリデュースでここで再帰呼び出しするでしょ。今ちょっと認識してないな。ちょっとゆっくりやってみますか。これ再帰呼び出しがリデュースじゃなくて、ちょっと名前変えてみようかな。リデュース2にしよう。すごいひどい名前ですけどね。リデュース2にして、これで呼び出せるのかな。エクステンションArrayヒントとか、コレクションにしにきて、えーと、そうだな、どうするかな。ふーんと、アレイがあって、でドロップファーストでしょ。ドロップファーストしたアレイのリデュース2が呼ばれてないね。ここが間違ってんのかな。ここがニルになってる? プレディケートで、ふーんと、ここニルになってるかな。
プレディケートがイントを返してて、オプショナルイント、えーと、どこがオプショナルイントなんだ。プレディケートの結果がオプショナルイントって何か違いますよね。ここがビックリマークだとエラーが出ない。開いてますよね。どこがだ。ファースト、あ、ここだ。間違えたバリューね。うん、そだそだ。で、これでリデュース2がラベルがいるのか。こうしてあげて、でこれでドロップファーストしてリデュース2が再帰呼び出しされる。良さそうじゃないですかね。
で、これで、これで完成じゃないですか。で、これでリデュース2を使ってみましょうね。バリューズあるね、バリューズ。で、リデュース2、で0、でプレディケート。2で実行すると、動いてくれるかな。いい感じですね。普通のリデュースと合わせてみます。 バリューズはリデュースで 0 でプラス 5ですね。この2つの値が一致すればオッケー。15が出ましたね。これを2倍にするとオーバーフローするかどうか試してみます。
オーバーフロー禁止の演算子ではなく、オーバーフロー許可の演算子を使ってみましょう。&*
を使ってみます。同じ答えが出れば、ちゃんと動いていることになりますね。もう少し汎用的なコードに変えてみます。例えば、エレメントを Int
ではなく、計算可能な BinaryInteger
にします。
BinaryInteger
にしてしまえば、エレメントが特に Int
にこだわる必要がなくなります。ここでエレメントに統一し、リデュースのメソッドも汎用的に変更します。もともとの Int
用のリデュースメソッドはもう不要です。
さて、これで動くか試してみましょう。動けばいいですが、実際のコードでそのまま試してみましょう。もう1つ変えたいことがあるので、ついでに変更します。配列 (Array
) よりもコレクション (Collection
) を使った方が汎用性が高いです。
これにより、配列にキャストする必要がなくなり、コードがすっきりします。実際に動かしてみましょう。動けば良いし、動かなければコンソールアプリケーションで試してみましょう。
例えば、次のようにして動作を確認します。let A
と let B
を定義し、この2つが一致するかどうかをプリントすれば分かりますね。
print(A)
print(B)
これで実行すると、どうなるか見てみましょう。
エラーですね。バリューズの定義を忘れていました。バリューズも持ってきて、実行してみます。
結果がコンソールに出ましたが、A
と B
が 0
ですね。これは初期値が 0
だからです。初期値を 1
にしないと掛け算的には全然ダメですね。
次に、ジェネリクスを使って T
型にし、結果 (Result
) と初期値 (InitialValue
) もジェネリクスにします。
func reduceWithGeneric<T>(_ elements: [T], _ initialValue: T, _ combine: (T, T) -> T) -> T
これでコードがコンパイル通るはずです。パーシャルリザルトが T
型である必要があります。
let partialResult: T
次に、文字列を渡して試します。例えば 0x
などとしてリデュースメソッドを呼び出してみます。
let result = elements.reduce(initialValue) { $0 + String($1, radix: 16) }
同様にリデュース2にも渡してみます。結果が一致すれば、ちゃんと動作していると言えます。
let result2 = elements.reduce2(initialValue) { $0 + String($1, radix: 16) }
結果が正しいことを確認できれば、再帰呼び出しが正しく働いていることが分かります。文字列が0で始まっているのは初期値のミスなので、適切に初期化すれば問題ありません。
再帰呼び出しのプロトコル思考やオブジェクト指向的アプローチを書いたのは初めてかもしれませんが、パラメータとして渡すか、レシーバーとして渡すかの違いだけです。
reduceWithGeneric(elements, initialValue, combine: +)
このようにリデュース関数を使って再帰呼び出しを行うことができました。これで今日の勉強会は終わりにします。お疲れ様でした。ありがとうございました。