前回は冒頭にさっそく予定していた話題から外れて、ジェネリクス周りの書き方についてみていく感じになりましたので、今回はそのとき最初に予定していた オプショナルバインディング
に複数の条件を組み合わせて使うあたりを眺めていきます。難しい機能ではないものの、もしかすると思いのほか出番の少ない機能かもしれないので、この機会に少しゆっくり眺めてみましょう。それを見終えたら引き続き、自動強制アンラップ周りの機能についておさらいしていく予定です。
今回はゆめみ社外に向けた参加者公募もありまして、ゆめみの外の人も招いた開催になります。どうぞよろしくお願いしますね。
————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #168
00:00 開始 01:19 前回のおさらい 02:12 オプショナルバインディングで複数の条件を指定 02:50 オプショナルバインディングのおさらい 04:00 オプショナルバインディングと条件式の組み合わせ 05:39 判定は条件を満たさなかった時点で打ち切られる 07:01 実際の動きを確かめてみる 10:11 条件が成立しない例 13:09 switch による一括評価と比べてみる 17:43 全部が評価されるとは限らないところに注意 19:04 論理積と論理和の短絡評価 22:28 短絡評価をしない言語も存在する 24:25 命令型プログラミング言語? 26:34 Java での短絡評価の扱い 27:27 C 言語における短絡評価の扱いについて 28:57 C++ における短絡評価の扱い 29:25 Swift における短絡評価 30:44 @autoclosure による短絡評価の実現 34:20 演算子を関数として扱うときの書式について 35:56 @autoclosure の動きの詳細 38:49 クロージング —————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #168
始めていきますね。今日はオプショナルについてのお話です。随分と長い間オプショナルの話をしてきて、ゆめみ社内の方々もずっと聞いていただいていますね。連続してではなくても、つまみつまみ聞いてもらっていると思います。ただ、基本的な内容なので、今日初めて来た方も今日の範囲だけ注目してもらえば大丈夫です。
前回までの話も言えればより広い視野を持てるとは思いますが、全然問題ありません。もし分からないことが出てきたら、聞いてもらえればその都度補足していけますので、遠慮なく質問してください。ほんのささいなことでも、そこから話が広がることもあります。
では、話を進めていきます。今表示中のスライドは前回のお話なので、今回は特に必要ではありません。企画上、動画を順次YouTubeで公開する予定なので、過去の内容も見ることができます。今日はオプショナルバインディングについて、複数の条件を指定できるというお話をします。プログラミング言語の本の次に書いてあった内容ですね。
まずはスライドを読んでみましょう。その前にオプショナルバインディングについて紹介します。オプショナルバインディングとは、オプショナルな変数があった時に、その変数に値があった場合、その値を取り出して if
文の正常ブロックを処理します。値がなかった時には else
ブロックを処理する方法です。
例えば、値がある場合に取り出した値を2倍するなど、いろいろな操作ができます。値がなかった場合には、正常ブロックは処理されず、else
ブロックが動作します。プレイグラウンドを無視して、以下のような感じです。
if let value = optionalValue {
print(value * 2)
} else {
print("値がありません")
}
今日はこの if
文におけるオプショナルバインディングを複数まとめて書けるよ、というお話です。複数のオプショナルバインディングと真偽値による条件式をカンマで区切って必要なだけ含めることができます。ただし、好きなだけ含めるとコンパイラーに制限されると思うので、どこまで可能かやってみましょう。
if let value1 = optionalValue1, let value2 = optionalValue2, value1 > value2 {
print("value1はvalue2より大きい")
} else {
print("条件が満たされません")
}
このように複数の条件を一度に扱えると、コードがより読みやすくなります。簡単な内容ですが、いろいろな発見があって面白いですね。ゆっくり見ていきましょう。 オプショナルバインディングと真偽値による条件式を一緒に書けることは、Swiftのif文の特徴です。意識していなかったが、何度も使っていたという感想を持ちました。この特徴を活用することで、複数の条件を一度にチェックでき、どれか一つでもnilやfalseであれば、if文全体がfalseとみなされます。これにより、if文のtrueブロックには入らず、falseのブロックが実行される形になります。
重要なポイントとして、どれかのオプショナルバインディングの値がnil、もしくは条件式がfalseになった時点で、それ以降の条件判定が打ち切られるということを押さえておきましょう。この仕様により、上から順に条件を判定し、1つでもnilやfalseがあれば即座に条件判定が終了し、全体がfalseとみなされます。
具体的な例を試してみましょう。まず、いくつかの関数を書いてみます。
func a() -> Bool {
print("aが実行されました")
return true
}
func b() -> Int? {
print("bが実行されました")
return 3
}
func c() -> Bool {
print("cが実行されました")
return false
}
func d() -> Int? {
print("dが実行されました")
return nil
}
これらの関数を使って、if文の条件に加えてみます。
if a(), let x = b(), c() {
print("OK")
} else {
print("NG")
}
このコードを実行すると、関数aとbが実行され、aがtrueを返し、bがintのオプショナル型を返します。bの値が存在するのでxに値が入れられます。次にcが実行され、falseを返すので、全体の条件がfalseとみなされ、結果的に「NG」が出力されます。
次に、関数dも加えてみましょう。
if a(), let x = b(), c(), let y = d() {
print("OK")
} else {
print("NG")
}
この場合、dはnilを返すので、やはり全体の条件がfalseとみなされ、「NG」が出力されます。これにより、関数a、b、cが順に実行され、その過程でfalseまたはnilが発生した場合に以降の評価が行われずにブロックの実行が打ち切られることが確認できます。
このようにif文の条件が論理AND(&&)で結ばれた複数の条件式として評価されます。どれか一つでもfalseまたはnilがあれば、その時点で評価が中断され、elseブロックが実行されます。これがSwiftの条件文の大きな特徴の一つです。
スイッチ文に関する指摘もありましたが、こちらもプログラムの読みやすさや扱うデータ構造、特にタプルに関して柔軟に使えるので、場合によっては活用することを考えると良いでしょう。
以上の内容は、Swiftのオプショナルバインディングと条件評価の基本的な動作を理解するための有益な情報です。何か質問があれば、随時対応していきます。ここまでご覧いただき、ありがとうございます。 ここがタプルの部分です。タプルの場合、片方の要素しかないということはありません。タプルとして捉えるときに確実な証拠はありませんが、タプルだと捉えると要はこうやって、えっと、分かりにくいかな。まあいいか。
例えば、Bool
とInt?
(オプショナル型のInt
)のタプルに対してd
とd
を渡すようなとき、d
が失敗したからと言って23行目はその影響を受けないということです。同じ感覚で25行目を見たとき、やはり両方を評価しなければならない雰囲気があります。ここで、読みにくかったり捉えにくかったり、複雑な知識を要求される可能性は確かにありますね。
でもこれは通ります。Optional
バインディングが使われていないから、ここがInt
だとコンパイルエラーになりますけど、Int?
(オプショナル型)のままならコンパイルが通り、同じように評価されます。「if let x」がy
のオプショナルパターンと同等な感じになります。
これがBool
とInt?
のタプルで、例えばInt
でスイッチケースを使って評価する場合を考えます。スイッチダブルでいいですね。以下のような感じです。
switch d {
case (let x, let y?):
print("\\(x), \\(y)")
default:
print("デフォルト")
}
こうすると、基本的に同等な動きを見せます。スイッチを使ったほうがd
まで評価されるので、分かりやすいです。最初のif
文を使ったほうが打ち止めになりますが、スイッチを使った場合はd
まで進みます。当然ですね。
この話は、シンプルなコードですが少しややこしい点があります。意味が不明な方はそのままで大丈夫ですよ。ただ、大きなポイントとしては、順番に評価して途中がfalse
の場合に処理が打ち止めになるという感覚です。この考え方は、非常に一般的なプログラミングの考え方です。
この順序評価の考え方は、プログラミングにおいて非常に重要です。初めての方は意識しておかないと、少し怖いところかもしれません。特に副作用がある関数を呼び出す場合には注意が必要です。この考え方を覚えておくと良いと思います。 他の言語によっては「&&」を使うことがあります。昔はそういうふうに使っていた気がしますが、今は違うんですよね。あんまり途中でやめるのってわかりにくいですよね。ただ、言語によっては「&」を使うことで、全ての条件が満たされたときに真(True)となり、満たされなかったときには偽(False)になります。一方でも条件が満たされない場合、評価を途中で打ち切る方が効率が良いという考え方もあります。
これには専門用語があります。何か覚えている方いますか?例えば、論理演算の「&」が途中で打ち切られることを指す用語です。こうした用語を知っておくと、何かを調べるときに便利なんですよね。
ちょっと手元で検索してみると、「短絡評価」という言葉が出てきました。短絡評価とは、多くのコンピュータープログラミングにおける論理演算子に対して、左辺と右辺の評価の方法です。左辺を評価した段階で式全体の値が定まる場合、右辺の評価を行いません。例えば、論理積「&&」について言えば、左辺が偽(False)であれば全体として偽になるため、右辺の評価をしません。
そして論理和「||」の場合、左辺が真(True)であれば全体として真になるため、右辺の評価を行いません。これは非常に効率的な評価方法です。
重要なポイントとして、多くのプログラミング言語では短絡評価が採用されていますが、そうでない言語もあります。したがって、新しい言語を使う際には、それが短絡評価をサポートしているか確認すると安心です。自分でもついつい短絡評価がされるものだと思い込んでしまうことがありますが、これは危険です。処理系依存の言語では、どうすれば良いのか悩んでしまいます。
また、Adaという言語についても触れておきます。この言語は処理系依存の挙動があり、これはとても怖いです。短絡評価を利用したいとき、または利用したくないときにどうすれば良いのかは非常に難しい問題です。Pascalのようにプラグマディレクティブを使用する方法も考えられますが、処理系依存のためにプログラミングが難しくなることがあります。
結局のところ、短絡評価が処理系依存の言語では、プログラムを書きながらその影響を気にする必要があります。これがかなりのプレッシャーになることもありますね。 とりあえず、命令型プログラミングは手続き型や構造化、モジュラー(モジュール)といった概念を含んでいます。プログラミングのアプローチとして、命令型という考え方があるのは確かです。命令型プログラミングは、プログラムが一連の命令を記述し、それを順次実行するという形で成り立っています。
さて、話をSwiftに戻しましょう。Swiftでは、両方の評価演算子と短絡評価用の演算子があります。これらが両方存在することで、場合によっては便利に感じることもありますが、その一方で、知識が必要な部分もあります。そのため、十分に理解して使わないと逆に混乱することもあるかもしれません。
短絡評価の演算子のほかに、ビット演算の&
と|
がBoolean型にもオーバーロードされており、これらは短絡評価をしません。この点が少し分かりにくいかもしれません。本来、ビット演算子は論理演算と異なる役割を持っていますが、それがBoolean型にもオーバーロードされていることで短絡評価が働かないのです。
例えば、C言語では論理演算が短絡評価を明確に行います。この点について、様々なプログラミング言語の参考書にも記載されています。1978年に標準化される前から、「The C Programming Language」という本などを通じてリファレンス的な存在として認識されていました。デファクトスタンダード(実質的な標準)といった感じです。
C++では、論理演算も短絡評価を行いますが、ユーザー定義型に関するオーバーロードでは短絡評価が行われません。これは非常に興味深い点です。私自身も&&
や||
をオーバーロードした経験がありますが、その際に短絡評価について深く考えたことはありませんでした。
次に、Swiftにおける短絡評価について見ていきましょう。以下のような関数で例を見てみます:
func x() -> Bool {
print("x")
return true
}
func y() -> Bool {
print("y")
return true
}
let result = x() && y()
print(result)
この例では、x
関数とy
関数が定義されています。x
関数が呼び出されると"true"を返し、同様にy
関数も"true"を返します。しかし、短絡評価により、x
関数がfalseを返せばy
関数は評価されません。これは、論理演算子が関数として定義されているため、関数呼び出しと同じように扱われるためです。
Swiftでは、短絡評価が行われていることを確認することができます。論理演算子は、関数として定義されています。そのため、関数呼び出しと同じように、評価が必要なパラメーターが揃って初めて処理が進むことが期待されます。
例えば、次のように書くと:
let result = x() && y()
ここで、y
関数は評価されずに済みます。それぞれの関数の呼び出しは独立して評価され、その結果を基にして次の関数が呼び出されるかどうかが決まります。
全体をまとめると、短絡評価はプログラミングにおいて重要な概念であり、しっかり理解して活用することが求められます。特に、Swiftのようなモダンなプログラミング言語では、その動作を正確に理解して使うことで、効率的でバグのないコードを書くことができます。 オートクロージャーを使うと短絡評価を効果的に活用するためのコツがあります。例えば、ブール値を返す関数を作成する場合を考えてみましょう。関数がand
演算子として機能するとします。以下のようにand
関数に対してx
とy
を渡すと、y
が表示されることから、これは短絡評価では動いていないことがわかります。
func and(_ lhs: Bool, _ rhs: @autoclosure () -> Bool) -> Bool {
return lhs && rhs()
}
短絡評価を理解するには、例えば以下のようなブール演算を考慮するとよいでしょう。このとき、評価されるのは&&
の左側だけで、右側は評価されません。
関数を括弧で囲って引数を渡す形式についてですが、普通に関数名をそのまま使うとパーサー上でうまく扱えないことがあります。そこで括弧を用いることで、名前を付けた関数と同様に扱えるようになります。このようにすることで、プログラム内部で必要な時だけ評価される仕組みを活用できます。
また、@autoclosure
を使うことで、以下のように書いた場合、y
が短絡評価されずに済みます。x
は評価されますが、y
はクロージャーとして渡されるため、実行されるまで評価はされません。
func and(_ lhs: Bool, _ rhs: @autoclosure () -> Bool) -> Bool {
return lhs && { rhs() }()
}
このようにすることで、例えばlefthandside
がtrue
だった場合に初めてrighthandside
が評価されるようになるため、y
が実行されないまま&&
演算が完了します。これにより、関数y
を必要なければ実行しない短絡評価が可能になります。
オートクロージャーを使うことで、API設計者は利用者が通常の引数のように渡すことができ、必要な時だけ評価される短絡実行を実現できます。このような工夫により、コードが読みやすく、効率的になります。
勉強会については、今回はここで終了となります。また次回は金曜日に予定していますので、その際にはぜひご参加ください。お疲れ様でした。ありがとうございました。