ヨーダ記法のススメ ─ 比較対象を左辺に記述するメリット

定数を左辺に書くという発想

ここに①「条件式の右辺に定数を書く方法」と②「定数を条件式の左辺に書く方法」があります。

if (encoding == UTF8); // ①
if (UTF8 == encoding); // ②

多くのプログラマは①の方式でコーディングすることが多いと思います。 しかし私は②の方式を採ることが多いです。「結論を先に書く」わけです。結論(定数)に対応する過程(式)の部分が複雑になればなるほど、結論を先に知ることの重要性は高まります。

左辺比較や左辺定数スタイルは前回の3文字幅インデントポインタ型記法と同様に、かなりマイナーで少数派のスタイルかも知れませんが、非常に強力で合理的なメリットを持ち合わせています。

ヨーダ記法

ちなみに定数を左に書くスタイルは俗にヨーダ記法と呼ばれています。(スターウォーズのヨーダは倒置法で話すため。「我々でシスを倒すのじゃ」ではなく、「シスを倒すのじゃ・・・我々でな」となる)

参考:ヨーダ記法とは ─ 定数を左側に書くメリット 流行らない理由

ヨーダ記法は安全性の観点で語られることが多いですが、本記事ではこれをソースリーディング時の効率性やコード解釈時の有効性という観点で考察します。

(変数 == 定数) VS (定数 == 変数)

両者それぞれ特徴があります。

① 変数 == 定数(a is B)

人間の話し言葉のような自然な文章になる。(He is tom, 彼はトムです)

② 定数 == 変数(B is a)

人間が読む文章としては不自然。(tom is he, トムは彼です)

普通に考えれば①のほうが直感的でベストなスタイルだと思うでしょう。 しかしそれはあくまで現実世界の既存の価値観から来た考え方であり、プログラミングの世界では必ずしもこれらに縛られる必要はないと思うのです。

そこで今回は②の「比較対象を左辺に記述する」スタイルの有効性を考察してみます。

なお、ここでいう定数とは、NULLやtrue等の値リテラル、文字列リテラル、整数リテラルを含むものとします(nil, false, "", -1, etc.)。
ヨーダ記法は一般的に「!=, ==」演算子に対して限定的に使われることが多いですが、本記事では「>, >=」演算子等を含めたより広い解釈に基づいたヨーダ記法を扱っていきます。

結論を先に書くことのメリット

思考の切り替えを早い段階で行える

単純なチェック処理の例です。

if (getPost("form-a").getBool("check-1") == false); // ①
if (false == getPost("form-a").getBool("check-1")); // ②
if (!getPost("form-a").getBool("check-1"));         // ③

上記コードは意外にも②や③のほうが直感的です。 false!という単語や記号を目にした時点で、if文内の処理が何かしらのチェック処理であることを推測できるためです。 false以降の右辺の処理も、その前提をもって解釈に取り組むことが出来るため、余計な見返し/二度見も回避できます。

// nullチェック処理
if (null == getPost("form-a").getString("name"));
// エラーチェック
if (-1 == getPost("form-a").getInt("city"));
// マイナスチェック
if (0 > getPost("form-a").getInt("age"));
// 文字比較
if ("CRLF".equals(getPost("form-a").getString("line")));

このようにヨーダ記法は処理の結論や目的を早い段階から読み手に示唆できるというメリットも持ち合わせています。

結論を先に伝えることの重要性

コードは大抵、左から右に向かって読み進めるわけですから、重要な結論は左に書いた方が後のコードの理解もしやすくなりますし、見落としも減ります。なぜコードの理解がしやすくなるのかに関してですが、そもそも人間の脳は結論を前提に物事を考えたほうが効率よく働くのです。

現実世界でも教員や上司に「で、結局何が言いたいの?」と問われることがあると思います。学校や企業のプレゼンテーションでもよく見かける風景です。 あれは結論を先に言わないことが原因であることが多いです。結論を後回しにし、長々と説明をされても聞いている側の頭に入って来ないのです。

教員や上司にしてみれば、長ったらしい説明をされた後に「結果こうなりました」と言われても、それまでの説明はまったく頭に入ってこなかったため「もう一度一から説明してくれ」と頼まざる終えません(先ほど言った見返し/二度見というのはこういうこと)

逆に、結論を先に言ってしまえば、相手の興味を引くことができます。また相手はその結論を前提に、説明やプレゼンを聞いてくれるため、一回の説明だけでも理解してもらえるのです。

コードの世界でもそれは同じ

コーディングの世界でもそれは同じです。むしろ短時間で大量のコードを読まなければならないコーディング作業だからこそ、そのような発想が重要になります。

結論を先に知れば、後の矛盾に早く気づくことができる

①「美人で高学歴で背の高い吾輩は猫である」
②「吾輩は猫である。美人で高学歴で背も高い」

上記の文章は②のほうが矛盾に気づきやすいはずです。

そもそも①の文章はおもわず見返してしまうのです。ネコに学歴なんて無いわけですから、これは明らかにおかしな文章なのです。 吾輩は猫であるという最後の文節を読んだ時点で、「あれ?人間の話じゃないの? 吾輩なのに女性なの?」という疑問を抱き、自身の目を疑い、再度一から文章を読み返した人も多いでしょう(いわゆる二度見)。

しかし②の文章で二度見することは殆どないはずです。先に左辺で雄ネコであることを強烈なインパクトでもって明かしているため、後の右辺に『美人』『高学歴』という単語が出てきた時点で「あれ? なんかおかしい」ということに気づくためです。

これはつまり、結論を先に明かすことで、後の文章の矛盾を早い段階で気づかせることができるということでもあります。

この手法をコーディング時に応用すれば、余計なバックトラックやダブルテイクを回避することに繋がり、コードリーディングの効率も格段に向上します。思わぬ見落としやバグを早期に発見・回避することにも繋がるでしょう。

結論を先に知れれば、目的のコードを早く見つけられる

コード中の目的の処理を探す際には大抵、処理の結果(特定の数値や文字列、定数など)が既にわかっているものです。

文字列比較以外にもNotFound判定や定数分岐、0より大きいか、エラーコード(-1)・EOFでないか等々……。

ソースリーディング時やデバッグ時にはそれらの結果・結論を探すわけですが、その結論が常に左辺に書かれているという前提があれば、目的のコードを探すのはとても容易になります。

極端な話、ソースの左側だけを目で追っていけば良いわけです。これは速読の助けにもなりますし、眼球運動も最小限に抑えられます。

ヨーダ推論

また、左辺で先に結論(名称や型情報など)を知る事ができれば、その時点で右辺側に目的のコードが有るか無いかを推測することもできます。推測によって目的のロジックと関係がなさそうだと判断できれば、右辺を無視して次の行を読み進めることも可能になります。

スターウォーズにおけるフォース特有の先読みや未来予知にも通じるものがあります。もっともヨーダ推定の場合は先を読むというよりは、あくまで先を推測するだけなのですが。

ちなみに筆者はこの推測技法のことを「ヨーダ推定」や「ヨーダ推論」と呼んでいます。

なお推定の際には定数だけでなくcontinue文やbreak, return等の文やキーワードを活用する場合もあります。ヨーダ推論に関係する話や活用例は後の章でじっくりと紹介します。

ヨーダ記法を使わないとどうなるか

逆に本来のように結果/結論を右側に書いた際のソースリーディングは大変です。ヨーダパターンの逆ならばと、ソースコードの右側だけを目で追っていけば良いと思うかもしれませんが、そうはいきません。

この場合の左辺はメソッドコールやメソッドチェーン、その他実引数の列挙によりコード量が変動しますし、テキスト折り返し時には右辺側の式が画面の左側に折り返します。結果、ソースを左から右までまんべんなく追っていかなければならなくなるのです。ヨーダ記法時のような推測・推定もできなくなります。

またテキストエディタの折り返しを無効にしている場合は更に厄介です。画面から途切れた右辺の式を確認するために、横スクロールが必要になるためです。

これらの問題を回避するために、1行80文字制限や変数化必須、メソッドチェーン禁止を義務化する開発チームやコーディング規約もあります。

ヨーダ記法のデメリット

なんだか凄そうな記法に思えてきたヨーダ記法ですが、この記法にもデメリットがあります。 メソッドチェーンなどにより、メソッドの最終要素と比較対象が離れすぎてしまうのです。

if (getPost("form-a").getBool("female") == false); // ①
if (false == getPost("form-a").getBool("female")); // ②

"false"femaleはコードにおけるもっとも重要な因子ですから、本来両者は①のように隣り合っていたほうが良いのです。

なお、ヨーダ記法利用時のこの問題の対応方法としては、変数化による名前付けが有効です。

bool isFemale = getPost("form-a").getBool("female");
if (false == isFemale);

まとめ

以上がヨーダ記法を安全面ではなく、ソースリーディング時の効率性や有効性という観点で考察した結果です。

ヨーダ記法自体は極めてマイナーな記法であり、業務利用やサンプルコードでの利用は憚られるかと思われます。しかし非常に斬新な発想であり、メリットも多いため、個人的な開発で利用してみるのも良いかと思います。

ヨーダ記法のメリットまとめ

  • 結論を先に知ることできる
  • 結論を前提にその後の処理や目的を推測できる
  • 使い方次第ではコードリーディングの負担や効率が向上させることもできる

ヨーダ推論のためのコーディングルール

ヨーダ推定はなにもヨーダ記法でのみ使える技法というわけではありません。ブロックの記述方法や修飾子の使い方を工夫するだけでも推定の助けになります。

continue, break, return文はワンライン化しない

if文内の条件が長く複雑になる場合はワンライン化/インライン化を避けましょう。 continue文やbreak文、return文は定数と同様に重要な結末です。できるだけ早い段階から読み手の目(視界)に留まるよう意識し、ワンライン化を避けるべきです。

// Good!
if (self.hasMark && locationInRange(self.markedRange.location, range))
   continue;

// BAD
if (self.hasMark && locationInRange(self.markedRange.location, range)) continue;

早い段階からcontinueやbreak, return文が目に付くことで、読み手に対してif文内の処理がガード処理やイレギュラー判定処理であることを示唆することができます。

ガード節を積極的に用いる

パターン1

このコードは良くないコードです。

// BAD
bool method(bool flag) {
   if (flag) {
      /* do something */
      /* do something */
   }
   return false;
}

return false;という重要な結末はできるだけ最初の方にも記述したいところです。「return false;」処理の実行を決定づける判定部if (flag)と離れすぎている点がよろしくありません(特にブロック内(/* do something */)の行数が数10行に渡るような場合は注意が必要です)

以下のように結末をロジックの先頭に記述しましょう。 ネストが浅くなるというメリットも生まれます。

// Good
bool method(bool flag) {
   if (!flag)
      return false;
   /* do something */
   /* do something */
   return false;
}

パターン2

次のコードも良くない例です。if文を抜けた後の処理やelse句が存在する可能性が疑われるためです。これはthen句の記述が数10行に渡るような場合に問題となります。

// BAD
void method(bool flag) {
   if (!flag) {
      // then句
      // (この先25行ほど続く...)
      
      /* do something */
      /* do something */
      // 画面スクロールしないと見えない
   }
   // ここに別の処理があるかもしれない
}

以下のように書き換えれば、「!flagパターンの場合はもうなにも処理が行われない」ということを読み手に伝えることができます。

// Good!
void method(bool flag) {
   if (!flag) return; // 即時リターン
   /* do something */
   /* do something */
}

パターン3

以下の改善パターンは# パターン1発想に近いものです。似たような処理を一箇所にまとめる効果もあります。

// BAD
void method(bool flag) {
   if (flag) {
      print("yes");
      /* do something */
      /* do something */
   } else {
      print("no");
   }
}

// Good!
void method(bool flag) {
   if (!flag) {
      print("no");
   } else {
      print("yes");
      /* do something */
      /* do something */
   }
}

番外編

// Good!
def method(flag)
   if !flag return false
   /* do something */
end

// Pretty Good!
def method(flag)
   return false if !flag
   /* do something */
end

Ruby言語特有のreturn if記法は、ある意味ヨーダ記法と言えなくもありません。

ちなみにSwift言語の場合はguard文やif let記法を使うようにすると良いです。

guard let a = x else { return }
/* do something */

if let a = x { /* do something */ }
return;

guard文は記法が冗長的ですが、慣れればif nil != x { return }if let _ = x { return }よりも目的や役割が明確になります。アンラップもできるため一石二鳥の機能です。

const修飾子を後方に宣言する

// Before
const List list = getList();
// After
List const list = getList();

ソースは左から右へ読むものであることを考えると、型タイプは極力左側に書いたほうが相手の目にも止まりやすくなります。

これは型属性と定数属性のどちらを重視するかにもよるのですが、「const修飾子はあくまで型や変数に付図する追加的な情報でしかない」という認識を持っているプログラマであれば、後者のほうが直感的な記法になるはずです。

ただし、関数パラメーター宣言時には、型タイプよりも定数かどうかが重視されるケースがあります。

// constが付いているほうがコピー元ソースで
// constが付いていないほうがコピー先であることが直感的に理解できる
char *strcpy(char *, const char *);
// こちらの記法だと理解にワンテンポの遅れが生じる恐れがある
char *strcpy(char *, char const *);

またポインタ利用時は、型名とポインタ変数の距離が極端に離れてしまうため、判読性が下がります。

List const *list = getList();
const List *list = getList();
const StringRef& ref = stringRef();
StringRef const& ref = stringRef();

typedefよりもusingを使う

C++ではtypedefの代わりにusingを使うと、定義内容をより規則的・直感的に表現できます。

// Before //
// ごちゃごちゃ
typedef char16_t Char16;
typedef char32_t Char32;
typedef std::basic_string<Char16> String;
typedef std::vector<String> Strings;
typedef my::parser<my::lexer<Char32>, my::node<Char32, Char32>> Parser;

// After //
// きちんと整備されていて読みやすい
// 規則性も見出しやすくなる
// コード揃えも最小限で行える
// とてもスマート
using Char16 = char16_t;
using Char32 = char32_t;
using String  = std::basic_string<Char16>;
using Strings = std::vector<String>;
using Parser = my::parser<my::lexer<Char32>, my::node<Char32, Char32>>;

比較関数の第一引数には定数を記述する

streq("UTF-8", encoding)
max(0, this.calc() - that.calc());

また、オブジェクト指向言語の場合は定数ベースのメソッド呼び出しを行うようにします。

"UTF-8".equals(encoding)

静的な値は前方の引数で受け取るようにする

定数値を受け取る可能性のある引数は前方に記述するようにします。

String gsub(Pattern pattern, String replacement, String target);

String string = "a b";
gsub("\\s", "-", string.toUpperCase()); // "A-B"

gsub("\\s", "-",の部分を強調する意図があります。因子となるgsub, "\\s", "-"と要素となるstring.toUpperCase()を明確に区分けする意図もあります。

左辺値基準のAPI設計

// Before
["a", "b"].join(",") // "a,b"
// After
",".join(["a", "b"]) // "a,b"

",".joinの部分を先に示すことで、処理の目的を速い段階で伝えられます。Before側の設計は、配列の要素数が多くなるほど目的の部分.join(",")が遠く目につかなくなるため注意が必要です。

グローバル関数/フリー関数を使う

// Before
"string".length()
[3, 2, 1, 0].sort()
// After
length("string")
sort([3, 2, 1, 0])

lengthsortという目的の部分を先に示す意図があります。

以下ようにコードがコンパクトに保たれる効果もあります。

length(a + b)
(a + b).length()

情報量が少なく、実に明瞭な記述です。

この設計はオーバーロードをサポートした言語でなおかつ型付けの強いプログラミング言語あれば有効に活用できます。

先程のjoin関数もフリー関数化すると良いでしょう。また左辺値重視の仮引数宣言は、可変長引数版への対応が容易に行えるとう特徴もあります。

join(",", [3, 2, 1]) // 配列版
join(",", 3, 2, 1)   // 可変長引数版

ソースを末尾に記述する設計では、可変長引数への対応が難しくなります。

join([3, 2, 1], ",") // 最初の内は問題にならない
join(3, 2, 1, ",")   // 実現不可能

// 名前付き引数を使うしかない
join(items: 3, 2, 1, separator: ",")

関数の引数はsrc, distやfrom, toの順に宣言する

// コピー元ソースを先に記述できるようすると
// 目的の値が目に止まりやすくなる
// またメソッドチェーンによってソース(定数)が押しやられることもない
my_strcpy("from", &(this->container().to));
// 統一関数呼び出し記法との相性も良い
"from".my_strcpy(&(this->container().to));

/* ヨーダ推定技法の例外 */
// この手の書き方はオススメできない
ArrayGetValueAtIndex(int index, Array *ref);
MapGetValueWithKey(String key, Map *ref);
// こちらのほうがAPI設計としても優れている
ArrayGetValueAtIndex(Array *ref, int index);
MapGetValueWithKey(Map *ref, String key);
// コンパイラマジックの恩恵を受けられる
ArrayGetValueAtIndex(ref, 9); // before
ref->GetValueAtIndex(9);      // after

/* 余談 */
// 引数内でもっとも重要なソースやコンテキストを左に書くと
// 引数が増えた際や、可変長引数の対応がしやすくなる
executeQuery(DB *database, Query query, Param... params);
広告

ヨーダ記法のススメ ─ 比較対象を左辺に記述するメリット」への1件のフィードバック

  1. ピンバック: ヨーダ記法とは 〜定数を左辺に記述するメリットと流行らない理由〜 | MaryCore

コメントは停止中です。