今回は SNS で見つけて興味の湧いた「値が範囲内にあるかを判定するときの書き方」について、ちょっとみんなで眺めていく回にしてみますね。どんな書き方がいちばん好みに感じられるか、そんな意見交換ができたらいいなと思ってます。それが終わったら引き続き The Basics
の Assertions and Preconditions
の項を眺めていきます。どうぞよろしくお願いしますね。
今回は参加者の一般参加者を募って、ゆめみの外の人たちも訪れての開催になる見通しです。
—————————————————————————— 熊谷さんのやさしい Swift 勉強会 #192
00:00 開始 00:10 今回の展望 00:54 数値が範囲内にあるかを判定する方法 01:16 if 分でパターンマッチングを用いる方法 01:34 パターンマッチング演算子を使う方法 02:17 条件式を論理演算で組み合わせる方法 02:37 主体を意識した機能としての判定方法 04:17 どの方法が見やすいだろう 05:00 演算子表現の難しさ 07:41 範囲ではなく値に注目したい 10:39 独自演算子で定義してみる 14:29 演算子の優先順位 19:10 独自演算子のコードは美しい? 20:17 目的の値を左辺に集める表現 22:33 a ≤ x ≤ b という書き方 28:08 演算子の優先順位を上書きに注意 ——————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #192
今日は、少し変わったテーマについて話していきますね。Twitterで面白い話題が上がっていたので、それについて見ていきたいと思います。特別なテーマというわけではなく、範囲内の数値の判定方法について話します。Swiftでは様々な書き方ができるようになってきているので、その代表的な幾つかを紹介します。
まず、Twitterで紹介されていた方法が4つありました。Xという変数が1から5の範囲に入るかどうかを判定する方法です。
1つ目の方法はif
文のパターンマッチングを使うものです。個人的にはこれが一番良いと思いました。if case
を使う方法ですね。
2つ目はエクスプレッションパターン用の演算子を使う方法です。この演算子は範囲を使っており、裏ではエクスプレッションパターンが動いています。普段あまり使われないかもしれませんが、紹介されているのは良いことだと思います。
3つ目の方法は、昔ながらのプログラマーが使う方法です。条件式を2つ繋げて書くやり方ですね。具体的には if (1...5).contains(x)
のように書きます。
4つ目の方法はレンジとして1から5
があり、それが変数x
を含むという書き方です。個人的に興味深かったのは、丸括弧の前にレンジと書いていることです。これがお洒落だと感じました。
例えば、自分は以前は丸括弧を書いて1から5
を指定し、その後にcontains
を使って判定していました。もっと簡潔に 1...5 ~= x
のように書くこともできます。これなら手戻りすることもないし、読みやすいです。
いろいろな書き方があって、それぞれに特徴がありますね。こういう違いを意識して書き分けることで、より読みやすいコードを書くことができると思います。 とりあえず一通りこの4つが挙げられていて、まず、こうやって4通り挙げられているところがすごいなと思いました。そして、なかなか興味深い話題なので、特にこれをまず見てみようと思います。
コメントが「3」が多いですね。やっぱり昔からのプログラムとかに慣れていると、または今時でも主流なのか分かんないけど、3で書いたことって結構多いですよね。別に間違いでもないし、それで正しいんですけどね。「4」はなんか分かりやすい気がします。テキストでしっかり説明されているので。ただ、演算子って知ってないとわからないっていう大きな問題があって、専門用語がそういう性格を持っているわけです。
昔、Swiftが演算子を独自に定義できる仕様だったとき、それを気に入った人が独自演算子をやたらと作りまくって、「こんなにきれいに書けるんだ」みたいな感じで言っているコードが、どう考えても読めないものがありました。そんなことがあったりしますので、独自演算子は可能な限り回避したほうがいいと個人的には思います。
その考え方と同じですね。自分の中では、「2」がさっき微妙な感想を言ったのは、~=
なんて演算子、慣れてないでしょ。みんなはいいですけど、一般的にはね。これが=~
だったら、それは例えばR言語の正規表現だよぐらいなら、多少は慣れている人もいるかもしれませんけど、~=
は初めて見たりするものですよね。そんな演算子を見たとき、「これ何やってるんだろう?」って考える場合じゃないですよ。他の書き方もある中で考えたときに、こういうのもありはするけど、なかなか表には見えてこないやつです。
この演算子を知っている人は、パターンマッチングをちゃんと理解していて、エクスプレッションパターンの中でこの演算子を使って比較評価をしているんですよ。だからこの演算子を自分で作るとパターンマッチングを独自に定義できますよ、というくらいに知っている人じゃないと伝わらないわけです。この演算子ね、今のレベルではこれが一般的に広がるようになれば別ですがね。
2番目のパターンはないかなあっていう気がします。でも、3を見ていくと、2番を使っているという人もいるんですよね。間違いではないんで、これが主流になる可能性もないとは言えない。良い悪いはあくまでも主観でしかないというところが大事ですが、今は2はちょっと避けたほうがいいかな、自分の場合はね。
個人的には、自分もXを左側に持ってきたいんですよ。スウィフトの標準ではそういう考え方が無理で、Xを左側に持ってきてくれる演算子を昔作ったりしました。
コメントでいただいているやつもありましたね。確かにそういうアイデアもありますね。Xが右側にあるのはやっぱり気持ち悪いんですよね。普通の数式でもXは左側に来るじゃないですか。全部が一つの集合の中に入っている場合、Xは左側に来ます。それを考えると右側に書くのは変な感じがします。
これを定義したのがこの演算子です。自分で作った演算子です。この記号自体を使えるんじゃないですかね。今ならもうすぐ出るのかな、2番目のリンクにそういうのを作ったんですよ。
そう、そうそう、そのリンクです。それを持ってきちゃってもいいかもしれないです。ええと、打ちながらやりました。この記号はオプションキーと一緒に出ましたっけ?数式のやつは出ないんですよね。3種類だったらオプションとシフトで出ますけど。オプションEとかでは出ないか。なんもってないなあ、そっか。オプション押しながらいろいろ試すの楽しいですよね。
個人的に好きなのは、オプションキーとシフトで特殊文字を作るやり方です。それでやるのがわかります。オプションBもだったかな?オプションと何か他のキーの組み合わせも試してみました。でも、なかなか出ないんですよね。
記事書いた時もめちゃくちゃやらせようとしたんですけど。コメント見てね、おかしいな、エンダーシーの定義を忘れてるな。まず先に書きますか。infix operator
ですね。 これが1から5の範囲だったら、インとウキってことかな。どうでしょうかね。あ、怒られちゃった。レンジエクスプレッション A
に肩にしちゃってるからか。肩にしちゃった。だからこれが U
だっけ?僕も寒いのかな。ダメだ、ここはダメ。
フィファっていうのを U
にしてレンジエクスプレッションにして、それで U
のバウンドと B
が一緒だよってこう書くんだね。ここ書いて…そうか、レンジをここ丸カットしないといけないっていう感じになってるね。
これで動くかな?これで動くね。動いた、動いた。この丸カット、取りたいですね。これを取るにはエンダーが出てるから、普通のインフィックスオペレーターの計算順番を多分定義すればいいはずです。そうですね、だからプレセデンスグループがあって、そこまでじゃなくてもいいのか、独自に既にあるやつ。そう、スネークラスを使えばいいはず。そうですね、順番、順番…。
そのプロポーザルがあって、優先順位が導入されたときのプロポーザルにどんなの作るよって書いてあります。アタイメントが大量のパネル…。開創あるレフォルトロジファル、あの範囲のやつ。範囲を優先度、だから範囲をやるより優先度低くないといけないのか、キャスティングくらい。これを当ててあげれば…。
キャスティングよりも優先度が高いってなってるから、ここでキャスティングぐらいに合わせてあげたらいいんじゃない。アタイメントアイデンティスメーションです。これでもなんか変わってない。もう一個…。とにかくこっちがいいな。
それでできた、できた。こんな感じだ。もしキャスティングの優先度がないのにキャスティングの優先度になると、なんか気持ち悪いなみたいなときにはグループを自分で作ってあげて、プレシデンスってやって、これでロー、パーザン、レンジ、フォーマーション、プレシデンスってやって…。
そして、あげれば動くのかな。もう一個いるかな。動いたね。そうそう。そんな感じで作ってあげればよいというふうに。これで丸括弧の煩わしさが減り、エックスが光にあるかつ…ここはちょっとサンプルわかりますけど、数学の原断詞、しっかりと意味のあるやつが使えるようっていう感じですね。コンペリゾンプレシデンスを使えばいいかな。本当だ、確かに。
確認させてもらいたいので、なるほどね。プレシデンスのところからコピーしてきたが、まあまあ、それはさておき…。どうでしょう、これぐらいでいいかな。コンペリゾンプレシデンスを使えばいいかな。はい、こんな感じで美しいコードが書けました。 何でこれが美しいコードのはずなのに美しく見えないんだろう。自分だけかな、美しく見えてないのは。
全角だからじゃないですか。
そういうことだな、そうかもしれない。反角文字もあるはずですよね。そうですよね。変換かけて出てくるのかな。変換なのか、とりあえずいいや。
もうちょっと小さいのかもしれないですね。確かに何かの区別がありますもんね。こんな感じで実装を作ることが精密にやってくると、こういう感じでできて、あとはそうだな、独自定義っていう観点で他にも見たことなかったかな。まあまあこんなところかな。
あとは、例えば x
が左にあると見やすいという観点でいうと、この3番もそうですね。何かこうやって 0 < x < 5
みたいに書くのが主流な気もするんですけど、どうなんだろうな。こういう書き方の中だったらもしかするとね、if x > 0
今回だと >= 1
のほうかな、<= 5
ってできるんだっけ。これと一緒にできますよね。
こういうふうに書くっていうアイデアもある。x
が 1
以上で x
が 5
以下っていう読み方もできて、好みによりますけどね。if 1 <= x && x <= 5
にしておこうかな。まあどっちでもいいですけどね、一般的にはこういった書き方。
比較してみると好みの問題とかもあるでしょうが、この対象が順番変わると自分の頭の中で混乱する傾向があるようで、好みとしては28行目よりも22行目が好きです。だからここなんかすごくまどろっこしく見えちゃって、何か無駄なことを書いているな、これ一つにまとめたいなとか余計なことを思ってしまうわけですよね。
こっちは数学的に普通な書き方なわけですよ、x
があって x
が 1
以上で x
が 5
以下ってね。だから見やすいんじゃないかと思ったりとかね。これ書けるようにできる人いますか。遊んでみたら書けるようにできるんですよね。なかなか面白かったとか、動画をベースに思いつく人はいるかどうか。
まあ、ゆっくり考えればそんなに難しくなくて、要はこの比較演算子が2個登場してるから、左辺が終わったらこれとこれっていう感じですね。これとこれとこれと比較する、そんな発想になるのかな。それでそう考えたときに、これを満たしていく条件式、演算子を作ればいいんですよね。
比較演算子を実装する例として、まず何らかの型 T
で、T
が Comparable
である必要がある。それを受け取って、ライトハンドサイドは x
なわけですよ、つまり T
ですよね。 例えば、以下のようにコードをリファクタリングします。まず、比較結果を保持するために適切な型を作成し、次に比較関数を実装します。そして、演算子を定義して、その優先順位を指定します。
以下はその手順です。
ComparisonResult
という型を作成します。この型は、比較対象の値と比較結果を保持します。
struct ComparisonResult<T: Comparable> {
let value: T
let result: Bool
}
- 次に、比較関数を実装します。この関数は、2つの値を比較し、その結果を
ComparisonResult
型で返します。
func compare<T: Comparable>(_ lhs: T, _ rhs: T) -> ComparisonResult<T> {
return ComparisonResult(value: rhs, result: lhs < rhs)
}
- 演算子を定義し、その優先順位を設定します。これには、
precedencegroup
を使用します。
precedencegroup MyComparisonPrecedence {
higherThan: LogicalConjunctionPrecedence
}
infix operator <==: MyComparisonPrecedence
func <==<T: Comparable>(lhs: ComparisonResult<T>, rhs: T) -> Bool {
return lhs.value < rhs
}
このようにして、新しい比較演算子を使用できるようになります。
例えば:
let result1 = compare(3, 5)
let result2 = result1 <== 7 // ここで比較が行われる
上記のコードでは、まず3と5を比較し、その結果をresult1
に格納します。次にresult1
と7を比較します。
コードの目的は、より柔軟で読みやすい比較操作を可能にするためのものです。ただし、既存の演算子や優先順位に手を加える際には注意が必要です。一般的には独自の演算子を定義して、既存のものに影響を与えないようにするのが良いでしょう。
この方法を使うことで、ComparisonResult
を活用しつつ、独自の演算子を定義して新しい比較方法を提供できるようになります。